localization
parent
22e6b3fac4
commit
4f97cd2b98
@ -0,0 +1,285 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EnglishAdventureLogFallback returns a readable English line for SQL/admin when Message is empty.
|
||||||
|
// Keep roughly aligned with frontend adventureLogFormat.ts (EN branch). Sync new event codes here.
|
||||||
|
func EnglishAdventureLogFallback(ev *AdventureLogEvent) string {
|
||||||
|
if ev == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
a := ev.Args
|
||||||
|
switch ev.Code {
|
||||||
|
case LogCombatSwing:
|
||||||
|
return englishCombatSwing(a)
|
||||||
|
case LogDefeatedEnemy:
|
||||||
|
return fmt.Sprintf("Defeated %s (+%v XP, +%v gold).", englishEnemyLogName(a), logArgFloat(a, "xp"), logArgFloat(a, "gold"))
|
||||||
|
case LogLeveledUp:
|
||||||
|
return fmt.Sprintf("Reached level %d!", logArgInt(a, "level"))
|
||||||
|
case LogEquippedNew:
|
||||||
|
return fmt.Sprintf("Equipped new %s: %s.", englishSlotName(logArgStr(a, "slot")), logArgStr(a, "itemName"))
|
||||||
|
case LogInventoryFullDropped:
|
||||||
|
return fmt.Sprintf("Inventory full — dropped %s (%s).", logArgStr(a, "itemName"), englishRarityName(logArgStr(a, "rarity")))
|
||||||
|
case LogBuffActivated:
|
||||||
|
return fmt.Sprintf("%s activated.", englishBuffName(logArgStr(a, "buffType")))
|
||||||
|
case LogHeroRevived:
|
||||||
|
return "You revived."
|
||||||
|
case LogWanderingMerchant:
|
||||||
|
return "A hooded merchant blocks your path, jingling a pouch of odd trinkets."
|
||||||
|
case LogEncounteredEnemy:
|
||||||
|
return fmt.Sprintf("You encounter %s.", englishEnemyLogName(a))
|
||||||
|
case LogDiedFighting:
|
||||||
|
return fmt.Sprintf("You died fighting %s.", englishEnemyLogName(a))
|
||||||
|
case LogAutoReviveHours:
|
||||||
|
return "Hours passed; you revived in town."
|
||||||
|
case LogAutoReviveAfterSec:
|
||||||
|
return fmt.Sprintf("Auto-revived after %ds offline.", logArgInt(a, "seconds"))
|
||||||
|
case LogPurchasedBuffRefill:
|
||||||
|
return fmt.Sprintf("Refilled charges: %s.", englishBuffName(logArgStr(a, "buffType")))
|
||||||
|
case LogPurchasedBuffRefillRub:
|
||||||
|
return fmt.Sprintf("Purchased refill for %s (%d RUB).", englishBuffName(logArgStr(a, "buffType")), logArgInt(a, "priceRub"))
|
||||||
|
case LogSubscribed:
|
||||||
|
dk := logArgStr(a, "durationKey")
|
||||||
|
price := logArgInt(a, "priceRub")
|
||||||
|
dur := dk
|
||||||
|
if dk == "subscription.week" {
|
||||||
|
dur = "one week of subscription"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("Subscribed: %s (%d RUB).", dur, price)
|
||||||
|
case LogUsedHealingPotion:
|
||||||
|
return fmt.Sprintf("Used healing potion (+%d HP).", logArgInt(a, "amount"))
|
||||||
|
case LogAchievementUnlocked:
|
||||||
|
title := logArgStr(a, "title")
|
||||||
|
rt := logArgStr(a, "rewardType")
|
||||||
|
ra := logArgInt(a, "rewardAmount")
|
||||||
|
switch rt {
|
||||||
|
case "gold":
|
||||||
|
return fmt.Sprintf("Achievement: %s (+%d gold).", title, ra)
|
||||||
|
case "potion":
|
||||||
|
return fmt.Sprintf("Achievement: %s (+%d potions).", title, ra)
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("Achievement: %s.", title)
|
||||||
|
}
|
||||||
|
case LogMetNPC:
|
||||||
|
return fmt.Sprintf("Met %s in %s.", logArgStr(a, "npcKey"), logArgStr(a, "townKey"))
|
||||||
|
case LogWanderingAlmsEquipped:
|
||||||
|
return fmt.Sprintf("Equipped from the merchant: %s.", logArgStr(a, "itemName"))
|
||||||
|
case LogWanderingAlmsDropped:
|
||||||
|
return fmt.Sprintf("Dropped %s (%s) — no room.", logArgStr(a, "itemName"), englishRarityName(logArgStr(a, "rarity")))
|
||||||
|
case LogWanderingAlmsStashed:
|
||||||
|
return fmt.Sprintf("Stashed %s in your inventory.", logArgStr(a, "itemName"))
|
||||||
|
case LogHealedFullTown:
|
||||||
|
return "Paid for a full heal."
|
||||||
|
case LogBoughtPotionTown:
|
||||||
|
return "Bought a potion in town."
|
||||||
|
case LogSoldItemsMerchant:
|
||||||
|
return fmt.Sprintf("Sold %d items to %s (+%v gold).", logArgInt(a, "count"), logArgStr(a, "npcKey"), logArgFloat(a, "gold"))
|
||||||
|
case LogNPCSkippedVisit:
|
||||||
|
return fmt.Sprintf("Skipped visiting %s.", logArgStr(a, "npcKey"))
|
||||||
|
case LogThoughtRoadside:
|
||||||
|
idx := logArgInt(a, "idx")
|
||||||
|
return fmt.Sprintf("Roadside thought (%d).", idx)
|
||||||
|
case LogPurchasedPotionFromNPC:
|
||||||
|
return fmt.Sprintf("Bought a potion from %s.", logArgStr(a, "npcKey"))
|
||||||
|
case LogPaidHealerFull:
|
||||||
|
return fmt.Sprintf("Paid %s for a full heal.", logArgStr(a, "npcKey"))
|
||||||
|
case LogQuestGiverChecked:
|
||||||
|
return fmt.Sprintf("Checked in with %s — no new quests.", logArgStr(a, "npcKey"))
|
||||||
|
case LogQuestAccepted:
|
||||||
|
return fmt.Sprintf("Accepted quest: %s.", logArgStr(a, "title"))
|
||||||
|
case LogTownNPCVisitLine:
|
||||||
|
npcType := logArgStr(a, "npcType")
|
||||||
|
line := logArgInt(a, "line")
|
||||||
|
return fmt.Sprintf("Town visit (%s): beat %d/6.", npcType, line+1)
|
||||||
|
default:
|
||||||
|
if ev.Code != "" {
|
||||||
|
return fmt.Sprintf("[%s]", ev.Code)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func englishCombatSwing(a map[string]any) string {
|
||||||
|
source := logArgStr(a, "source")
|
||||||
|
outcome := logArgStr(a, "outcome")
|
||||||
|
damage := logArgInt(a, "damage")
|
||||||
|
isCrit := logArgBool(a, "isCrit")
|
||||||
|
enemyName := englishEnemyLogName(a)
|
||||||
|
critSuffix := ""
|
||||||
|
if isCrit {
|
||||||
|
critSuffix = " (crit)"
|
||||||
|
}
|
||||||
|
var msg string
|
||||||
|
switch source {
|
||||||
|
case "hero":
|
||||||
|
switch outcome {
|
||||||
|
case "stun":
|
||||||
|
msg = "You are stunned and cannot attack."
|
||||||
|
case "dodge":
|
||||||
|
msg = enemyName + " dodged your attack."
|
||||||
|
default:
|
||||||
|
msg = fmt.Sprintf("You hit %s for %d damage%s.", enemyName, damage, critSuffix)
|
||||||
|
}
|
||||||
|
case "enemy":
|
||||||
|
switch outcome {
|
||||||
|
case "block":
|
||||||
|
msg = fmt.Sprintf("You block %s's attack.", enemyName)
|
||||||
|
default:
|
||||||
|
msg = fmt.Sprintf("%s hits you for %d damage%s.", enemyName, damage, critSuffix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
debuff := logArgStr(a, "debuffType")
|
||||||
|
if debuff != "" {
|
||||||
|
msg += " " + englishDebuffName(debuff) + " applied."
|
||||||
|
}
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
|
||||||
|
// englishEnemyLogName prefers arg enemyName (DB) when present; else template name from slug.
|
||||||
|
func englishEnemyLogName(a map[string]any) string {
|
||||||
|
if a == nil {
|
||||||
|
return "enemy"
|
||||||
|
}
|
||||||
|
if n := strings.TrimSpace(logArgStr(a, "enemyName")); n != "" {
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
return englishEnemyDisplayName(logArgStr(a, "enemyType"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func englishEnemyDisplayName(slug string) string {
|
||||||
|
slug = strings.TrimSpace(slug)
|
||||||
|
if slug == "" {
|
||||||
|
return "enemy"
|
||||||
|
}
|
||||||
|
if e, ok := EnemyBySlug(slug); ok && strings.TrimSpace(e.Name) != "" {
|
||||||
|
return e.Name
|
||||||
|
}
|
||||||
|
return slug
|
||||||
|
}
|
||||||
|
|
||||||
|
func englishDebuffName(debuffType string) string {
|
||||||
|
dt, ok := ValidDebuffType(debuffType)
|
||||||
|
if !ok {
|
||||||
|
return debuffType
|
||||||
|
}
|
||||||
|
if def, ok := DebuffDefinition(dt); ok && def.Name != "" {
|
||||||
|
return def.Name
|
||||||
|
}
|
||||||
|
return debuffType
|
||||||
|
}
|
||||||
|
|
||||||
|
func englishBuffName(raw string) string {
|
||||||
|
raw = strings.ToLower(strings.TrimSpace(raw))
|
||||||
|
bt, ok := ValidBuffType(raw)
|
||||||
|
if !ok {
|
||||||
|
return raw
|
||||||
|
}
|
||||||
|
if b, ok := BuffDefinition(bt); ok && b.Name != "" {
|
||||||
|
return b.Name
|
||||||
|
}
|
||||||
|
return raw
|
||||||
|
}
|
||||||
|
|
||||||
|
func englishSlotName(raw string) string {
|
||||||
|
raw = strings.ToLower(strings.TrimSpace(raw))
|
||||||
|
m := map[string]string{
|
||||||
|
"main_hand": "weapon",
|
||||||
|
"off_hand": "off-hand",
|
||||||
|
"head": "head",
|
||||||
|
"chest": "chest",
|
||||||
|
"legs": "legs",
|
||||||
|
"feet": "feet",
|
||||||
|
"cloak": "cloak",
|
||||||
|
"neck": "neck",
|
||||||
|
"finger": "ring",
|
||||||
|
"wrist": "wrist",
|
||||||
|
"hands": "hands",
|
||||||
|
"quiver": "quiver",
|
||||||
|
}
|
||||||
|
if s, ok := m[raw]; ok {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return raw
|
||||||
|
}
|
||||||
|
|
||||||
|
func englishRarityName(raw string) string {
|
||||||
|
raw = strings.ToLower(strings.TrimSpace(raw))
|
||||||
|
m := map[string]string{
|
||||||
|
"common": "common",
|
||||||
|
"uncommon": "uncommon",
|
||||||
|
"rare": "rare",
|
||||||
|
"epic": "epic",
|
||||||
|
"legendary": "legendary",
|
||||||
|
}
|
||||||
|
if s, ok := m[raw]; ok {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return raw
|
||||||
|
}
|
||||||
|
|
||||||
|
func logArgStr(a map[string]any, key string) string {
|
||||||
|
if a == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
v, ok := a[key]
|
||||||
|
if !ok || v == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
switch x := v.(type) {
|
||||||
|
case string:
|
||||||
|
return x
|
||||||
|
case fmt.Stringer:
|
||||||
|
return x.String()
|
||||||
|
default:
|
||||||
|
return fmt.Sprint(x)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func logArgFloat(a map[string]any, key string) float64 {
|
||||||
|
if a == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
v, ok := a[key]
|
||||||
|
if !ok || v == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
switch x := v.(type) {
|
||||||
|
case float64:
|
||||||
|
return x
|
||||||
|
case float32:
|
||||||
|
return float64(x)
|
||||||
|
case int:
|
||||||
|
return float64(x)
|
||||||
|
case int64:
|
||||||
|
return float64(x)
|
||||||
|
case uint64:
|
||||||
|
return float64(x)
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func logArgInt(a map[string]any, key string) int {
|
||||||
|
return int(logArgFloat(a, key))
|
||||||
|
}
|
||||||
|
|
||||||
|
func logArgBool(a map[string]any, key string) bool {
|
||||||
|
if a == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
v, ok := a[key]
|
||||||
|
if !ok || v == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
switch x := v.(type) {
|
||||||
|
case bool:
|
||||||
|
return x
|
||||||
|
case string:
|
||||||
|
return strings.EqualFold(x, "true") || x == "1"
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestEnglishAdventureLogFallback_combatSwing(t *testing.T) {
|
||||||
|
got := EnglishAdventureLogFallback(&AdventureLogEvent{
|
||||||
|
Code: LogCombatSwing,
|
||||||
|
Args: map[string]any{
|
||||||
|
"source": "hero",
|
||||||
|
"outcome": "hit",
|
||||||
|
"damage": 12,
|
||||||
|
"isCrit": true,
|
||||||
|
"enemyType": "wolf_test_slug",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
want := "You hit wolf_test_slug for 12 damage (crit)."
|
||||||
|
if got != want {
|
||||||
|
t.Fatalf("got %q want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnglishAdventureLogFallback_heroRevived(t *testing.T) {
|
||||||
|
got := EnglishAdventureLogFallback(&AdventureLogEvent{Code: LogHeroRevived})
|
||||||
|
if got != "You revived." {
|
||||||
|
t.Fatalf("got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnglishAdventureLogFallback_unknownCode(t *testing.T) {
|
||||||
|
got := EnglishAdventureLogFallback(&AdventureLogEvent{Code: "future_event_xyz"})
|
||||||
|
if got != "[future_event_xyz]" {
|
||||||
|
t.Fatalf("got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,61 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
// AdventureLogEvent is a machine-readable log line; the client maps Code + Args to localized text.
|
||||||
|
type AdventureLogEvent struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
Args map[string]any `json:"args,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdventureLogLine is written to the DB and sent over WebSocket.
|
||||||
|
// Legacy rows have only Message; new rows set Event and optional Message (English fallback).
|
||||||
|
type AdventureLogLine struct {
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
Event *AdventureLogEvent `json:"event,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adventure log event codes (keep in sync with frontend adventureLogFormat.ts).
|
||||||
|
//
|
||||||
|
// For events with arg "enemyType" (enemies.type slug), also send "enemyName" when known:
|
||||||
|
// English display name from DB for client fallback and SQL message column.
|
||||||
|
const (
|
||||||
|
LogDefeatedEnemy = "defeated_enemy"
|
||||||
|
LogLeveledUp = "leveled_up"
|
||||||
|
LogEquippedNew = "equipped_new"
|
||||||
|
LogInventoryFullDropped = "inventory_full_dropped"
|
||||||
|
LogBuffActivated = "buff_activated"
|
||||||
|
LogHeroRevived = "hero_revived"
|
||||||
|
LogWanderingMerchant = "wandering_merchant_encounter"
|
||||||
|
LogEncounteredEnemy = "encountered_enemy"
|
||||||
|
LogDiedFighting = "died_fighting"
|
||||||
|
LogAutoReviveHours = "auto_revive_hours"
|
||||||
|
LogAutoReviveAfterSec = "auto_revive_after_sec"
|
||||||
|
LogPurchasedBuffRefill = "purchased_buff_refill"
|
||||||
|
LogPurchasedBuffRefillRub = "purchased_buff_refill_rub"
|
||||||
|
LogSubscribed = "subscribed"
|
||||||
|
LogUsedHealingPotion = "used_healing_potion"
|
||||||
|
LogAchievementUnlocked = "achievement_unlocked"
|
||||||
|
LogMetNPC = "met_npc"
|
||||||
|
LogWanderingAlmsEquipped = "wandering_alms_equipped"
|
||||||
|
LogWanderingAlmsDropped = "wandering_alms_dropped"
|
||||||
|
LogWanderingAlmsStashed = "wandering_alms_stashed"
|
||||||
|
LogHealedFullTown = "healed_full_town"
|
||||||
|
LogBoughtPotionTown = "bought_potion_town"
|
||||||
|
LogSoldItemsMerchant = "sold_items_merchant"
|
||||||
|
LogNPCSkippedVisit = "npc_skipped_visit"
|
||||||
|
LogThoughtRoadside = "thought_roadside"
|
||||||
|
LogPurchasedPotionFromNPC = "purchased_potion_from_npc"
|
||||||
|
LogPaidHealerFull = "paid_healer_full"
|
||||||
|
LogQuestGiverChecked = "quest_giver_checked"
|
||||||
|
LogQuestAccepted = "quest_accepted"
|
||||||
|
LogTownNPCVisitLine = "town_npc_visit_line"
|
||||||
|
LogCombatSwing = "combat_swing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RoadsideThoughtCount must match len(roadsideThoughtsEn) on the frontend.
|
||||||
|
const RoadsideThoughtCount = 52
|
||||||
|
|
||||||
|
// Wandering merchant (road encounter) — stable keys for client i18n.
|
||||||
|
const (
|
||||||
|
WanderingMerchantNPCKey = "npc.wandering_merchant.v1"
|
||||||
|
WanderingMerchantDialogueKey = "npc.wandering_merchant.dialogue.v1"
|
||||||
|
)
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAdventureLogLine_JSON_roundTrip(t *testing.T) {
|
||||||
|
line := AdventureLogLine{
|
||||||
|
Message: "legacy",
|
||||||
|
Event: &AdventureLogEvent{
|
||||||
|
Code: LogDefeatedEnemy,
|
||||||
|
Args: map[string]any{"enemyType": "wolf_l1_1_meadow", "xp": float64(10), "gold": float64(5)},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
b, err := json.Marshal(line)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var got AdventureLogLine
|
||||||
|
if err := json.Unmarshal(b, &got); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got.Message != line.Message {
|
||||||
|
t.Fatalf("message: got %q want %q", got.Message, line.Message)
|
||||||
|
}
|
||||||
|
if got.Event == nil || got.Event.Code != LogDefeatedEnemy {
|
||||||
|
t.Fatalf("event code: %+v", got.Event)
|
||||||
|
}
|
||||||
|
if got.Event.Args["enemyType"] != "wolf_l1_1_meadow" {
|
||||||
|
t.Fatalf("args: %+v", got.Event.Args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAdventureLogLine_JSON_legacyMessageOnly(t *testing.T) {
|
||||||
|
raw := `{"message":"hello"}`
|
||||||
|
var got AdventureLogLine
|
||||||
|
if err := json.Unmarshal([]byte(raw), &got); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got.Message != "hello" || got.Event != nil {
|
||||||
|
t.Fatalf("got %+v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
-- Adventure log structured events (client-localized).
|
||||||
|
ALTER TABLE adventure_log
|
||||||
|
ADD COLUMN IF NOT EXISTS event_code TEXT,
|
||||||
|
ADD COLUMN IF NOT EXISTS event_args JSONB;
|
||||||
|
|
||||||
|
-- Stable keys for UI localization (towns / NPCs).
|
||||||
|
ALTER TABLE towns
|
||||||
|
ADD COLUMN IF NOT EXISTS name_key TEXT;
|
||||||
|
|
||||||
|
UPDATE towns SET name_key = v.k
|
||||||
|
FROM (VALUES
|
||||||
|
(1, 'town.willowdale.v1'),
|
||||||
|
(2, 'town.thornwatch.v1'),
|
||||||
|
(3, 'town.ashengard.v1'),
|
||||||
|
(4, 'town.redcliff.v1'),
|
||||||
|
(5, 'town.boghollow.v1'),
|
||||||
|
(6, 'town.cinderkeep.v1'),
|
||||||
|
(7, 'town.starfall.v1'),
|
||||||
|
(8, 'town.mossharbor.v1'),
|
||||||
|
(9, 'town.emberwell.v1'),
|
||||||
|
(10, 'town.frostmark.v1'),
|
||||||
|
(11, 'town.duskwatch.v1')
|
||||||
|
) AS v(id, k)
|
||||||
|
WHERE towns.id = v.id AND towns.name_key IS NULL;
|
||||||
|
|
||||||
|
ALTER TABLE npcs
|
||||||
|
ADD COLUMN IF NOT EXISTS name_key TEXT;
|
||||||
|
|
||||||
|
UPDATE npcs SET name_key = v.k
|
||||||
|
FROM (VALUES
|
||||||
|
(1, 'npc.elder_maren.v1'),
|
||||||
|
(2, 'npc.peddler_finn.v1'),
|
||||||
|
(3, 'npc.sister_asha.v1'),
|
||||||
|
(4, 'npc.guard_halric.v1'),
|
||||||
|
(5, 'npc.trader_wynn.v1'),
|
||||||
|
(6, 'npc.scholar_orin.v1'),
|
||||||
|
(7, 'npc.bone_merchant.v1'),
|
||||||
|
(8, 'npc.priestess_liora.v1')
|
||||||
|
) AS v(id, k)
|
||||||
|
WHERE npcs.id = v.id AND npcs.name_key IS NULL;
|
||||||
|
|
||||||
|
-- Quest localization keys (fallback: quest.<id> set in app if null).
|
||||||
|
ALTER TABLE quests
|
||||||
|
ADD COLUMN IF NOT EXISTS quest_key TEXT;
|
||||||
|
|
||||||
|
UPDATE quests SET quest_key = 'quest.' || id::text WHERE quest_key IS NULL;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_quests_quest_key ON quests (quest_key);
|
||||||
@ -0,0 +1,80 @@
|
|||||||
|
import { ENEMY_TYPE_NAME_ROWS } from './enemyTypeLabelsData';
|
||||||
|
|
||||||
|
export type EnemyTypeBilingual = { en: string; ru: string };
|
||||||
|
|
||||||
|
/** Longest English creature titles first (suffix match). */
|
||||||
|
const CREATURE_EN_RU: [string, string][] = [
|
||||||
|
['Bone Sovereign', 'владыка костей'],
|
||||||
|
['Elemental', 'элементаль'],
|
||||||
|
['Scaleback', 'чешуеспин'],
|
||||||
|
['Manticore', 'мантикора'],
|
||||||
|
['Basilisk', 'василиск'],
|
||||||
|
['Skeleton', 'скелет'],
|
||||||
|
['Wyvern', 'виверна'],
|
||||||
|
['Cultist', 'культист'],
|
||||||
|
['Treant', 'древень'],
|
||||||
|
['Harpy', 'гарпия'],
|
||||||
|
['Warden', 'страж'],
|
||||||
|
['Wraith', 'призрак'],
|
||||||
|
['Spider', 'паук'],
|
||||||
|
['Zombie', 'зомби'],
|
||||||
|
['Golem', 'голем'],
|
||||||
|
['Bandit', 'разбойник'],
|
||||||
|
['Demon', 'демон'],
|
||||||
|
['Shade', 'тень'],
|
||||||
|
['Titan', 'титан'],
|
||||||
|
['Orc', 'орк'],
|
||||||
|
['Boar', 'кабан'],
|
||||||
|
['Wolf', 'волк'],
|
||||||
|
];
|
||||||
|
|
||||||
|
function capitalizeTitleRu(s: string): string {
|
||||||
|
if (!s) return s;
|
||||||
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Russian gloss for procedural English names from `000006b_enemy_data.sql`. */
|
||||||
|
function ruFromEnglishDisplayName(en: string): string {
|
||||||
|
let creatureEn = '';
|
||||||
|
let creatureRu = '';
|
||||||
|
for (const [e, r] of CREATURE_EN_RU) {
|
||||||
|
if (en.endsWith(e)) {
|
||||||
|
creatureEn = e;
|
||||||
|
creatureRu = r;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!creatureEn) return en;
|
||||||
|
const prefix = en.slice(0, en.length - creatureEn.length).trimEnd();
|
||||||
|
|
||||||
|
if (prefix === 'Rift Lost') {
|
||||||
|
return capitalizeTitleRu(creatureRu) + ' из разлома';
|
||||||
|
}
|
||||||
|
if (prefix === 'Cursed Rift') {
|
||||||
|
return capitalizeTitleRu('Проклятый ' + creatureRu + ' разлома');
|
||||||
|
}
|
||||||
|
const prefixMap: Record<string, string> = {
|
||||||
|
'Elder Verdant': 'Древний зелёный',
|
||||||
|
'Woodland Elder': 'Старый лесной',
|
||||||
|
'Young Woodland': 'Молодой лесной',
|
||||||
|
'Forgotten Young': 'Юный забытый',
|
||||||
|
'Lost Forgotten': 'Потерянный забытый',
|
||||||
|
'Bog Cursed': 'Болотный проклятый',
|
||||||
|
'Rogue Ember': 'Угольный бродячий',
|
||||||
|
'Astral Rogue': 'Астральный бродячий',
|
||||||
|
};
|
||||||
|
const p = prefixMap[prefix];
|
||||||
|
if (!p) return en;
|
||||||
|
return capitalizeTitleRu(p + ' ' + creatureRu);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildEnemyTypeLabels(): Record<string, EnemyTypeBilingual> {
|
||||||
|
const out: Record<string, EnemyTypeBilingual> = {};
|
||||||
|
for (const [slug, en] of ENEMY_TYPE_NAME_ROWS) {
|
||||||
|
out[slug] = { en, ru: ruFromEnglishDisplayName(en) };
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Localized display per DB `enemies.type` (220 rows from migration seed). */
|
||||||
|
export const ENEMY_TYPE_LABELS: Record<string, EnemyTypeBilingual> = buildEnemyTypeLabels();
|
||||||
@ -0,0 +1,226 @@
|
|||||||
|
/**
|
||||||
|
* `enemies.type` → English display name from migration `000006b_enemy_data.sql`.
|
||||||
|
* Keep in sync when DB enemy names change.
|
||||||
|
*/
|
||||||
|
export const ENEMY_TYPE_NAME_ROWS = [
|
||||||
|
['wolf_l1_1_meadow', 'Elder Verdant Wolf'],
|
||||||
|
['wolf_l1_1_forest', 'Woodland Elder Wolf'],
|
||||||
|
['wolf_l2_2_forest', 'Young Woodland Wolf'],
|
||||||
|
['wolf_l2_2_ruins', 'Forgotten Young Wolf'],
|
||||||
|
['wolf_l3_3_ruins', 'Lost Forgotten Wolf'],
|
||||||
|
['wolf_l3_3_canyon', 'Rift Lost Wolf'],
|
||||||
|
['wolf_l4_4_canyon', 'Cursed Rift Wolf'],
|
||||||
|
['wolf_l4_4_swamp', 'Bog Cursed Wolf'],
|
||||||
|
['wolf_l5_5_volcanic', 'Rogue Ember Wolf'],
|
||||||
|
['wolf_l5_5_astral', 'Astral Rogue Wolf'],
|
||||||
|
['boar_l2_2_meadow', 'Elder Verdant Boar'],
|
||||||
|
['boar_l2_2_forest', 'Woodland Elder Boar'],
|
||||||
|
['boar_l3_3_forest', 'Young Woodland Boar'],
|
||||||
|
['boar_l3_3_ruins', 'Forgotten Young Boar'],
|
||||||
|
['boar_l4_4_ruins', 'Lost Forgotten Boar'],
|
||||||
|
['boar_l4_4_canyon', 'Rift Lost Boar'],
|
||||||
|
['boar_l5_5_canyon', 'Cursed Rift Boar'],
|
||||||
|
['boar_l5_5_swamp', 'Bog Cursed Boar'],
|
||||||
|
['boar_l6_6_volcanic', 'Rogue Ember Boar'],
|
||||||
|
['boar_l6_6_astral', 'Astral Rogue Boar'],
|
||||||
|
['zombie_l3_4_meadow', 'Elder Verdant Zombie'],
|
||||||
|
['zombie_l3_4_forest', 'Woodland Elder Zombie'],
|
||||||
|
['zombie_l5_5_forest', 'Young Woodland Zombie'],
|
||||||
|
['zombie_l5_5_ruins', 'Forgotten Young Zombie'],
|
||||||
|
['zombie_l6_6_ruins', 'Lost Forgotten Zombie'],
|
||||||
|
['zombie_l6_6_canyon', 'Rift Lost Zombie'],
|
||||||
|
['zombie_l7_7_canyon', 'Cursed Rift Zombie'],
|
||||||
|
['zombie_l7_7_swamp', 'Bog Cursed Zombie'],
|
||||||
|
['zombie_l8_8_volcanic', 'Rogue Ember Zombie'],
|
||||||
|
['zombie_l8_8_astral', 'Astral Rogue Zombie'],
|
||||||
|
['spider_l4_5_meadow', 'Elder Verdant Spider'],
|
||||||
|
['spider_l4_5_forest', 'Woodland Elder Spider'],
|
||||||
|
['spider_l6_6_forest', 'Young Woodland Spider'],
|
||||||
|
['spider_l6_6_ruins', 'Forgotten Young Spider'],
|
||||||
|
['spider_l7_7_ruins', 'Lost Forgotten Spider'],
|
||||||
|
['spider_l7_7_canyon', 'Rift Lost Spider'],
|
||||||
|
['spider_l8_8_canyon', 'Cursed Rift Spider'],
|
||||||
|
['spider_l8_8_swamp', 'Bog Cursed Spider'],
|
||||||
|
['spider_l9_9_volcanic', 'Rogue Ember Spider'],
|
||||||
|
['spider_l9_9_astral', 'Astral Rogue Spider'],
|
||||||
|
['orc_l5_6_meadow', 'Elder Verdant Orc'],
|
||||||
|
['orc_l5_6_forest', 'Woodland Elder Orc'],
|
||||||
|
['orc_l7_8_forest', 'Young Woodland Orc'],
|
||||||
|
['orc_l7_8_ruins', 'Forgotten Young Orc'],
|
||||||
|
['orc_l9_10_ruins', 'Lost Forgotten Orc'],
|
||||||
|
['orc_l9_10_canyon', 'Rift Lost Orc'],
|
||||||
|
['orc_l11_11_canyon', 'Cursed Rift Orc'],
|
||||||
|
['orc_l11_11_swamp', 'Bog Cursed Orc'],
|
||||||
|
['orc_l12_12_volcanic', 'Rogue Ember Orc'],
|
||||||
|
['orc_l12_12_astral', 'Astral Rogue Orc'],
|
||||||
|
['skeleton_l6_7_meadow', 'Elder Verdant Skeleton'],
|
||||||
|
['skeleton_l6_7_forest', 'Woodland Elder Skeleton'],
|
||||||
|
['skeleton_l8_9_forest', 'Young Woodland Skeleton'],
|
||||||
|
['skeleton_l8_9_ruins', 'Forgotten Young Skeleton'],
|
||||||
|
['skeleton_l10_11_ruins', 'Lost Forgotten Skeleton'],
|
||||||
|
['skeleton_l10_11_canyon', 'Rift Lost Skeleton'],
|
||||||
|
['skeleton_l12_13_canyon', 'Cursed Rift Skeleton'],
|
||||||
|
['skeleton_l12_13_swamp', 'Bog Cursed Skeleton'],
|
||||||
|
['skeleton_l14_14_volcanic', 'Rogue Ember Skeleton'],
|
||||||
|
['skeleton_l14_14_astral', 'Astral Rogue Skeleton'],
|
||||||
|
['battle_lizard_l7_8_meadow', 'Elder Verdant Scaleback'],
|
||||||
|
['battle_lizard_l7_8_forest', 'Woodland Elder Scaleback'],
|
||||||
|
['battle_lizard_l9_10_forest', 'Young Woodland Scaleback'],
|
||||||
|
['battle_lizard_l9_10_ruins', 'Forgotten Young Scaleback'],
|
||||||
|
['battle_lizard_l11_12_ruins', 'Lost Forgotten Scaleback'],
|
||||||
|
['battle_lizard_l11_12_canyon', 'Rift Lost Scaleback'],
|
||||||
|
['battle_lizard_l13_14_canyon', 'Cursed Rift Scaleback'],
|
||||||
|
['battle_lizard_l13_14_swamp', 'Bog Cursed Scaleback'],
|
||||||
|
['battle_lizard_l15_15_volcanic', 'Rogue Ember Scaleback'],
|
||||||
|
['battle_lizard_l15_15_astral', 'Astral Rogue Scaleback'],
|
||||||
|
['element_l18_20_meadow', 'Elder Verdant Elemental'],
|
||||||
|
['element_l12_14_forest', 'Woodland Elder Elemental'],
|
||||||
|
['element_l21_22_forest', 'Young Woodland Elemental'],
|
||||||
|
['element_l15_16_ruins', 'Forgotten Young Elemental'],
|
||||||
|
['element_l23_24_ruins', 'Lost Forgotten Elemental'],
|
||||||
|
['element_l17_18_canyon', 'Rift Lost Elemental'],
|
||||||
|
['element_l25_26_canyon', 'Cursed Rift Elemental'],
|
||||||
|
['element_l19_20_swamp', 'Bog Cursed Elemental'],
|
||||||
|
['element_l27_28_volcanic', 'Rogue Ember Elemental'],
|
||||||
|
['element_l21_22_astral', 'Astral Rogue Elemental'],
|
||||||
|
['demon_l10_12_meadow', 'Elder Verdant Demon'],
|
||||||
|
['demon_l10_12_forest', 'Woodland Elder Demon'],
|
||||||
|
['demon_l13_14_forest', 'Young Woodland Demon'],
|
||||||
|
['demon_l13_14_ruins', 'Forgotten Young Demon'],
|
||||||
|
['demon_l15_16_ruins', 'Lost Forgotten Demon'],
|
||||||
|
['demon_l15_16_canyon', 'Rift Lost Demon'],
|
||||||
|
['demon_l17_18_canyon', 'Cursed Rift Demon'],
|
||||||
|
['demon_l17_18_swamp', 'Bog Cursed Demon'],
|
||||||
|
['demon_l19_20_volcanic', 'Rogue Ember Demon'],
|
||||||
|
['demon_l19_20_astral', 'Astral Rogue Demon'],
|
||||||
|
['skeleton_king_l15_17_meadow', 'Elder Verdant Bone Sovereign'],
|
||||||
|
['skeleton_king_l15_17_forest', 'Woodland Elder Bone Sovereign'],
|
||||||
|
['skeleton_king_l18_19_forest', 'Young Woodland Bone Sovereign'],
|
||||||
|
['skeleton_king_l18_19_ruins', 'Forgotten Young Bone Sovereign'],
|
||||||
|
['skeleton_king_l20_21_ruins', 'Lost Forgotten Bone Sovereign'],
|
||||||
|
['skeleton_king_l20_21_canyon', 'Rift Lost Bone Sovereign'],
|
||||||
|
['skeleton_king_l22_23_canyon', 'Cursed Rift Bone Sovereign'],
|
||||||
|
['skeleton_king_l22_23_swamp', 'Bog Cursed Bone Sovereign'],
|
||||||
|
['skeleton_king_l24_25_volcanic', 'Rogue Ember Bone Sovereign'],
|
||||||
|
['skeleton_king_l24_25_astral', 'Astral Rogue Bone Sovereign'],
|
||||||
|
['forest_warden_l20_22_meadow', 'Elder Verdant Warden'],
|
||||||
|
['forest_warden_l20_22_forest', 'Woodland Elder Warden'],
|
||||||
|
['forest_warden_l23_24_forest', 'Young Woodland Warden'],
|
||||||
|
['forest_warden_l23_24_ruins', 'Forgotten Young Warden'],
|
||||||
|
['forest_warden_l25_26_ruins', 'Lost Forgotten Warden'],
|
||||||
|
['forest_warden_l25_26_canyon', 'Rift Lost Warden'],
|
||||||
|
['forest_warden_l27_28_canyon', 'Cursed Rift Warden'],
|
||||||
|
['forest_warden_l27_28_swamp', 'Bog Cursed Warden'],
|
||||||
|
['forest_warden_l29_30_volcanic', 'Rogue Ember Warden'],
|
||||||
|
['forest_warden_l29_30_astral', 'Astral Rogue Warden'],
|
||||||
|
['titan_l25_27_meadow', 'Elder Verdant Titan'],
|
||||||
|
['titan_l25_27_forest', 'Woodland Elder Titan'],
|
||||||
|
['titan_l28_29_forest', 'Young Woodland Titan'],
|
||||||
|
['titan_l28_29_ruins', 'Forgotten Young Titan'],
|
||||||
|
['titan_l30_31_ruins', 'Lost Forgotten Titan'],
|
||||||
|
['titan_l30_31_canyon', 'Rift Lost Titan'],
|
||||||
|
['titan_l32_33_canyon', 'Cursed Rift Titan'],
|
||||||
|
['titan_l32_33_swamp', 'Bog Cursed Titan'],
|
||||||
|
['titan_l34_35_volcanic', 'Rogue Ember Titan'],
|
||||||
|
['titan_l34_35_astral', 'Astral Rogue Titan'],
|
||||||
|
['golem_l8_10_meadow', 'Elder Verdant Golem'],
|
||||||
|
['golem_l8_10_forest', 'Woodland Elder Golem'],
|
||||||
|
['golem_l11_12_forest', 'Young Woodland Golem'],
|
||||||
|
['golem_l11_12_ruins', 'Forgotten Young Golem'],
|
||||||
|
['golem_l13_14_ruins', 'Lost Forgotten Golem'],
|
||||||
|
['golem_l13_14_canyon', 'Rift Lost Golem'],
|
||||||
|
['golem_l15_16_canyon', 'Cursed Rift Golem'],
|
||||||
|
['golem_l15_16_swamp', 'Bog Cursed Golem'],
|
||||||
|
['golem_l17_18_volcanic', 'Rogue Ember Golem'],
|
||||||
|
['golem_l17_18_astral', 'Astral Rogue Golem'],
|
||||||
|
['wraith_l5_6_meadow', 'Elder Verdant Wraith'],
|
||||||
|
['wraith_l5_6_forest', 'Woodland Elder Wraith'],
|
||||||
|
['wraith_l7_8_forest', 'Young Woodland Wraith'],
|
||||||
|
['wraith_l7_8_ruins', 'Forgotten Young Wraith'],
|
||||||
|
['wraith_l9_10_ruins', 'Lost Forgotten Wraith'],
|
||||||
|
['wraith_l9_10_canyon', 'Rift Lost Wraith'],
|
||||||
|
['wraith_l11_12_canyon', 'Cursed Rift Wraith'],
|
||||||
|
['wraith_l11_12_swamp', 'Bog Cursed Wraith'],
|
||||||
|
['wraith_l13_14_volcanic', 'Rogue Ember Wraith'],
|
||||||
|
['wraith_l13_14_astral', 'Astral Rogue Wraith'],
|
||||||
|
['bandit_l4_5_meadow', 'Elder Verdant Bandit'],
|
||||||
|
['bandit_l4_5_forest', 'Woodland Elder Bandit'],
|
||||||
|
['bandit_l6_7_forest', 'Young Woodland Bandit'],
|
||||||
|
['bandit_l6_7_ruins', 'Forgotten Young Bandit'],
|
||||||
|
['bandit_l8_9_ruins', 'Lost Forgotten Bandit'],
|
||||||
|
['bandit_l8_9_canyon', 'Rift Lost Bandit'],
|
||||||
|
['bandit_l10_11_canyon', 'Cursed Rift Bandit'],
|
||||||
|
['bandit_l10_11_swamp', 'Bog Cursed Bandit'],
|
||||||
|
['bandit_l12_12_volcanic', 'Rogue Ember Bandit'],
|
||||||
|
['bandit_l12_12_astral', 'Astral Rogue Bandit'],
|
||||||
|
['cultist_l6_8_meadow', 'Elder Verdant Cultist'],
|
||||||
|
['cultist_l6_8_forest', 'Woodland Elder Cultist'],
|
||||||
|
['cultist_l9_10_forest', 'Young Woodland Cultist'],
|
||||||
|
['cultist_l9_10_ruins', 'Forgotten Young Cultist'],
|
||||||
|
['cultist_l11_12_ruins', 'Lost Forgotten Cultist'],
|
||||||
|
['cultist_l11_12_canyon', 'Rift Lost Cultist'],
|
||||||
|
['cultist_l13_14_canyon', 'Cursed Rift Cultist'],
|
||||||
|
['cultist_l13_14_swamp', 'Bog Cursed Cultist'],
|
||||||
|
['cultist_l15_16_volcanic', 'Rogue Ember Cultist'],
|
||||||
|
['cultist_l15_16_astral', 'Astral Rogue Cultist'],
|
||||||
|
['treant_l18_20_meadow', 'Elder Verdant Treant'],
|
||||||
|
['treant_l18_20_forest', 'Woodland Elder Treant'],
|
||||||
|
['treant_l21_23_forest', 'Young Woodland Treant'],
|
||||||
|
['treant_l21_23_ruins', 'Forgotten Young Treant'],
|
||||||
|
['treant_l24_26_ruins', 'Lost Forgotten Treant'],
|
||||||
|
['treant_l24_26_canyon', 'Rift Lost Treant'],
|
||||||
|
['treant_l27_28_canyon', 'Cursed Rift Treant'],
|
||||||
|
['treant_l27_28_swamp', 'Bog Cursed Treant'],
|
||||||
|
['treant_l29_30_volcanic', 'Rogue Ember Treant'],
|
||||||
|
['treant_l29_30_astral', 'Astral Rogue Treant'],
|
||||||
|
['basilisk_l9_11_meadow', 'Elder Verdant Basilisk'],
|
||||||
|
['basilisk_l9_11_forest', 'Woodland Elder Basilisk'],
|
||||||
|
['basilisk_l12_13_forest', 'Young Woodland Basilisk'],
|
||||||
|
['basilisk_l12_13_ruins', 'Forgotten Young Basilisk'],
|
||||||
|
['basilisk_l14_15_ruins', 'Lost Forgotten Basilisk'],
|
||||||
|
['basilisk_l14_15_canyon', 'Rift Lost Basilisk'],
|
||||||
|
['basilisk_l16_17_canyon', 'Cursed Rift Basilisk'],
|
||||||
|
['basilisk_l16_17_swamp', 'Bog Cursed Basilisk'],
|
||||||
|
['basilisk_l18_19_volcanic', 'Rogue Ember Basilisk'],
|
||||||
|
['basilisk_l18_19_astral', 'Astral Rogue Basilisk'],
|
||||||
|
['wyvern_l12_14_meadow', 'Elder Verdant Wyvern'],
|
||||||
|
['wyvern_l12_14_forest', 'Woodland Elder Wyvern'],
|
||||||
|
['wyvern_l15_17_forest', 'Young Woodland Wyvern'],
|
||||||
|
['wyvern_l15_17_ruins', 'Forgotten Young Wyvern'],
|
||||||
|
['wyvern_l18_20_ruins', 'Lost Forgotten Wyvern'],
|
||||||
|
['wyvern_l18_20_canyon', 'Rift Lost Wyvern'],
|
||||||
|
['wyvern_l21_22_canyon', 'Cursed Rift Wyvern'],
|
||||||
|
['wyvern_l21_22_swamp', 'Bog Cursed Wyvern'],
|
||||||
|
['wyvern_l23_24_volcanic', 'Rogue Ember Wyvern'],
|
||||||
|
['wyvern_l23_24_astral', 'Astral Rogue Wyvern'],
|
||||||
|
['harpy_l6_7_meadow', 'Elder Verdant Harpy'],
|
||||||
|
['harpy_l6_7_forest', 'Woodland Elder Harpy'],
|
||||||
|
['harpy_l8_9_forest', 'Young Woodland Harpy'],
|
||||||
|
['harpy_l8_9_ruins', 'Forgotten Young Harpy'],
|
||||||
|
['harpy_l10_11_ruins', 'Lost Forgotten Harpy'],
|
||||||
|
['harpy_l10_11_canyon', 'Rift Lost Harpy'],
|
||||||
|
['harpy_l12_13_canyon', 'Cursed Rift Harpy'],
|
||||||
|
['harpy_l12_13_swamp', 'Bog Cursed Harpy'],
|
||||||
|
['harpy_l14_15_volcanic', 'Rogue Ember Harpy'],
|
||||||
|
['harpy_l14_15_astral', 'Astral Rogue Harpy'],
|
||||||
|
['manticore_l14_16_meadow', 'Elder Verdant Manticore'],
|
||||||
|
['manticore_l14_16_forest', 'Woodland Elder Manticore'],
|
||||||
|
['manticore_l17_19_forest', 'Young Woodland Manticore'],
|
||||||
|
['manticore_l17_19_ruins', 'Forgotten Young Manticore'],
|
||||||
|
['manticore_l20_22_ruins', 'Lost Forgotten Manticore'],
|
||||||
|
['manticore_l20_22_canyon', 'Rift Lost Manticore'],
|
||||||
|
['manticore_l23_24_canyon', 'Cursed Rift Manticore'],
|
||||||
|
['manticore_l23_24_swamp', 'Bog Cursed Manticore'],
|
||||||
|
['manticore_l25_26_volcanic', 'Rogue Ember Manticore'],
|
||||||
|
['manticore_l25_26_astral', 'Astral Rogue Manticore'],
|
||||||
|
['shade_l10_12_meadow', 'Elder Verdant Shade'],
|
||||||
|
['shade_l10_12_forest', 'Woodland Elder Shade'],
|
||||||
|
['shade_l13_15_forest', 'Young Woodland Shade'],
|
||||||
|
['shade_l13_15_ruins', 'Forgotten Young Shade'],
|
||||||
|
['shade_l16_18_ruins', 'Lost Forgotten Shade'],
|
||||||
|
['shade_l16_18_canyon', 'Rift Lost Shade'],
|
||||||
|
['shade_l19_20_canyon', 'Cursed Rift Shade'],
|
||||||
|
['shade_l19_20_swamp', 'Bog Cursed Shade'],
|
||||||
|
['shade_l21_22_volcanic', 'Rogue Ember Shade'],
|
||||||
|
['shade_l21_22_astral', 'Astral Rogue Shade'],
|
||||||
|
] as const;
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
import type { Locale } from './index';
|
||||||
|
|
||||||
|
/** Optional per-quest overrides; keys match `quest_key` from DB (e.g. quest.12). */
|
||||||
|
const BUNDLES: Record<
|
||||||
|
string,
|
||||||
|
{ title: { en: string; ru: string }; description: { en: string; ru: string } }
|
||||||
|
> = {};
|
||||||
|
|
||||||
|
export function localizedQuestText(
|
||||||
|
locale: Locale,
|
||||||
|
questKey: string | undefined,
|
||||||
|
part: 'title' | 'description',
|
||||||
|
fallback: string,
|
||||||
|
): string {
|
||||||
|
if (!questKey) return fallback;
|
||||||
|
const b = BUNDLES[questKey];
|
||||||
|
if (!b) return fallback;
|
||||||
|
const piece = b[part];
|
||||||
|
return locale === 'ru' ? (piece.ru || fallback) : (piece.en || fallback);
|
||||||
|
}
|
||||||
@ -1,16 +1,29 @@
|
|||||||
import { BuffType } from '../game/types';
|
import { BuffType } from '../game/types';
|
||||||
|
import type { Translations } from '../i18n/en';
|
||||||
|
|
||||||
/** Icons and colors for buff UI (BuffBar buttons + BuffStatusStrip). */
|
/** Icons and colors for buff UI (labels/descriptions come from i18n like HeroPanel). */
|
||||||
export const BUFF_META: Record<
|
export const BUFF_VISUAL: Record<BuffType, { icon: string; color: string }> = {
|
||||||
BuffType,
|
[BuffType.Rush]: { icon: '\u26A1', color: '#44aaff' },
|
||||||
{ icon: string; label: string; color: string; desc: string }
|
[BuffType.Rage]: { icon: '\u2694\uFE0F', color: '#ff4444' },
|
||||||
> = {
|
[BuffType.Shield]: { icon: '\uD83D\uDEE1\uFE0F', color: '#aaaaff' },
|
||||||
[BuffType.Rush]: { icon: '\u26A1', label: 'Rush', color: '#44aaff', desc: '+50% movement speed' },
|
[BuffType.Luck]: { icon: '\uD83C\uDF40', color: '#44ff44' },
|
||||||
[BuffType.Rage]: { icon: '\u2694\uFE0F', label: 'Rage', color: '#ff4444', desc: '+100% damage' },
|
[BuffType.Resurrection]: { icon: '\uD83D\uDD2E', color: '#ffaa44' },
|
||||||
[BuffType.Shield]: { icon: '\uD83D\uDEE1\uFE0F', label: 'Shield', color: '#aaaaff', desc: '-50% incoming damage' },
|
[BuffType.Heal]: { icon: '\u2764\uFE0F', color: '#ff6699' },
|
||||||
[BuffType.Luck]: { icon: '\uD83C\uDF40', label: 'Luck', color: '#44ff44', desc: 'x2.5 loot drops' },
|
[BuffType.PowerPotion]: { icon: '\uD83E\uDDEA', color: '#dd44ff' },
|
||||||
[BuffType.Resurrection]: { icon: '\uD83D\uDD2E', label: 'Resurrect', color: '#ffaa44', desc: 'Revive at 50% HP' },
|
[BuffType.WarCry]: { icon: '\uD83D\uDCE3', color: '#ffcc00' },
|
||||||
[BuffType.Heal]: { icon: '\u2764\uFE0F', label: 'Heal', color: '#ff6699', desc: '+50% HP instant' },
|
|
||||||
[BuffType.PowerPotion]: { icon: '\uD83E\uDDEA', label: 'Power', color: '#dd44ff', desc: '+150% damage' },
|
|
||||||
[BuffType.WarCry]: { icon: '\uD83D\uDCE3', label: 'WarCry', color: '#ffcc00', desc: '+100% attack speed' },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function buffUiStrings(tr: Translations, type: BuffType): { label: string; desc: string } {
|
||||||
|
const keys: Record<BuffType, [keyof Translations, keyof Translations]> = {
|
||||||
|
[BuffType.Rush]: ['buffRush', 'buffRushDesc'],
|
||||||
|
[BuffType.Rage]: ['buffRage', 'buffRageDesc'],
|
||||||
|
[BuffType.Shield]: ['buffShield', 'buffShieldDesc'],
|
||||||
|
[BuffType.Luck]: ['buffLuck', 'buffLuckDesc'],
|
||||||
|
[BuffType.Resurrection]: ['buffResurrection', 'buffResurrectionDesc'],
|
||||||
|
[BuffType.Heal]: ['buffHeal', 'buffHealDesc'],
|
||||||
|
[BuffType.PowerPotion]: ['buffPowerPotion', 'buffPowerPotionDesc'],
|
||||||
|
[BuffType.WarCry]: ['buffWarCry', 'buffWarCryDesc'],
|
||||||
|
};
|
||||||
|
const [lk, dk] = keys[type];
|
||||||
|
return { label: tr[lk], desc: tr[dk] };
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue