updated localization
parent
dd21bff29d
commit
a9b452e713
@ -0,0 +1,25 @@
|
|||||||
|
package game
|
||||||
|
|
||||||
|
import "github.com/denisovdennis/autohero/internal/model"
|
||||||
|
|
||||||
|
// combatLogPhraseKey maps combat swing to a client phrase key (see frontend adventureLog phrases).
|
||||||
|
func combatLogPhraseKey(source, outcome string) string {
|
||||||
|
switch source {
|
||||||
|
case "hero":
|
||||||
|
switch outcome {
|
||||||
|
case attackOutcomeStun:
|
||||||
|
return model.LogPhraseCombatHeroStun
|
||||||
|
case attackOutcomeDodge:
|
||||||
|
return model.LogPhraseCombatHeroDodge
|
||||||
|
default:
|
||||||
|
return model.LogPhraseCombatHeroHit
|
||||||
|
}
|
||||||
|
case "enemy":
|
||||||
|
if outcome == attackOutcomeBlock {
|
||||||
|
return model.LogPhraseCombatEnemyBlock
|
||||||
|
}
|
||||||
|
return model.LogPhraseCombatEnemyHit
|
||||||
|
default:
|
||||||
|
return model.LogPhraseCombatHeroHit
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,82 @@
|
|||||||
|
// Resident hero policy (engine memory):
|
||||||
|
// - After the last WebSocket disconnect, the hero stays in Engine.movements; the world keeps ticking.
|
||||||
|
// - Cold start: ListHeroesForEngineBootstrap loads rows with ws_disconnected_at set (cap 500 default in main).
|
||||||
|
// - Full hero row is saved every offlineDisconnectedFullSaveInterval while heroSubscriber reports false.
|
||||||
|
package game
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log/slog"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/denisovdennis/autohero/internal/model"
|
||||||
|
"github.com/denisovdennis/autohero/internal/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BootstrapResidentHeroes loads heroes whose WebSocket session had ended before this process started,
|
||||||
|
// catches up wall time using the same batch path as server-downtime recovery, then registers them
|
||||||
|
// in the engine so movement and combat continue without a live subscriber.
|
||||||
|
func BootstrapResidentHeroes(ctx context.Context, e *Engine, heroStore *storage.HeroStore, sim *OfflineSimulator, limit int, logger *slog.Logger) {
|
||||||
|
if e == nil || heroStore == nil || sim == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
heroes, err := heroStore.ListHeroesForEngineBootstrap(ctx, limit)
|
||||||
|
if err != nil {
|
||||||
|
if logger != nil {
|
||||||
|
logger.Error("engine bootstrap: list heroes", "error", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
now := time.Now()
|
||||||
|
for _, h := range heroes {
|
||||||
|
if h == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
e.mu.Lock()
|
||||||
|
_, already := e.movements[h.ID]
|
||||||
|
rg := e.roadGraph
|
||||||
|
e.mu.Unlock()
|
||||||
|
if already || rg == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
e.mergeTownSessionFromRedis(h)
|
||||||
|
|
||||||
|
if err := sim.SimulateHeroAt(ctx, h, now, true); err != nil {
|
||||||
|
if logger != nil {
|
||||||
|
logger.Error("engine bootstrap: catch-up sim", "hero_id", h.ID, "error", err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
e.mu.Lock()
|
||||||
|
if e.roadGraph == nil {
|
||||||
|
e.mu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, taken := e.movements[h.ID]; taken {
|
||||||
|
e.mu.Unlock()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
hm := NewHeroMovement(h, e.roadGraph, now)
|
||||||
|
e.movements[h.ID] = hm
|
||||||
|
hm.MarkTownPausePersisted(hm.townPausePersistSignature())
|
||||||
|
hm.SyncToHero()
|
||||||
|
if hm.State == model.StateFighting {
|
||||||
|
if _, exists := e.combats[h.ID]; !exists {
|
||||||
|
en := PickEnemyForLevel(h.Level)
|
||||||
|
if en.Slug != "" {
|
||||||
|
e.startCombatLocked(hm.Hero, &en)
|
||||||
|
} else {
|
||||||
|
hm.State = model.StateWalking
|
||||||
|
hm.Hero.State = model.StateWalking
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e.mu.Unlock()
|
||||||
|
|
||||||
|
if logger != nil {
|
||||||
|
logger.Info("engine bootstrap: resident hero registered", "hero_id", h.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,63 @@
|
|||||||
|
package game
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log/slog"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/denisovdennis/autohero/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHeroSocketDetachedKeepsMovement(t *testing.T) {
|
||||||
|
e := NewEngine(100*time.Millisecond, make(chan model.CombatEvent, 8), slog.Default())
|
||||||
|
e.SetRoadGraph(testGraph())
|
||||||
|
|
||||||
|
h := &model.Hero{
|
||||||
|
ID: 1, State: model.StateWalking, HP: 10, MaxHP: 10, Level: 1,
|
||||||
|
PositionX: 1, PositionY: 1,
|
||||||
|
}
|
||||||
|
hm := NewHeroMovement(h, testGraph(), time.Now())
|
||||||
|
e.mu.Lock()
|
||||||
|
e.movements[1] = hm
|
||||||
|
e.mu.Unlock()
|
||||||
|
|
||||||
|
disconnectAt := time.Date(2026, 4, 1, 12, 0, 0, 0, time.UTC)
|
||||||
|
e.HeroSocketDetached(1, true, disconnectAt)
|
||||||
|
|
||||||
|
e.mu.RLock()
|
||||||
|
_, ok := e.movements[1]
|
||||||
|
e.mu.RUnlock()
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("expected hero to remain resident in engine after last WS disconnect")
|
||||||
|
}
|
||||||
|
if h.WsDisconnectedAt == nil || !h.WsDisconnectedAt.Equal(disconnectAt) {
|
||||||
|
t.Fatalf("expected WsDisconnectedAt on in-memory hero, got %v", h.WsDisconnectedAt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeResidentHeroState(t *testing.T) {
|
||||||
|
e := NewEngine(100*time.Millisecond, make(chan model.CombatEvent, 8), slog.Default())
|
||||||
|
e.SetRoadGraph(testGraph())
|
||||||
|
|
||||||
|
dst := &model.Hero{ID: 7, State: model.StateWalking, HP: 5, MaxHP: 10, Level: 2}
|
||||||
|
if e.MergeResidentHeroState(dst) {
|
||||||
|
t.Fatal("expected false when hero not resident")
|
||||||
|
}
|
||||||
|
|
||||||
|
h := &model.Hero{
|
||||||
|
ID: 7, State: model.StateWalking, HP: 9, MaxHP: 10, Level: 3,
|
||||||
|
PositionX: 2, PositionY: 3,
|
||||||
|
}
|
||||||
|
hm := NewHeroMovement(h, testGraph(), time.Now())
|
||||||
|
e.mu.Lock()
|
||||||
|
e.movements[7] = hm
|
||||||
|
e.mu.Unlock()
|
||||||
|
|
||||||
|
dst2 := &model.Hero{ID: 7, HP: 1, Level: 1}
|
||||||
|
if !e.MergeResidentHeroState(dst2) {
|
||||||
|
t.Fatal("expected true when resident")
|
||||||
|
}
|
||||||
|
if dst2.HP != 9 || dst2.Level != 3 {
|
||||||
|
t.Fatalf("expected engine stats copied, got hp=%d level=%d", dst2.HP, dst2.Level)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,285 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
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,91 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
// Phrase keys for adventure_log.event_code / WS adventure_log_line.event.code.
|
||||||
|
// No human-readable text on the server — only keys and structured args.
|
||||||
|
|
||||||
|
const (
|
||||||
|
LogPhraseDefeatedEnemy = "log.defeated_enemy"
|
||||||
|
LogPhraseLeveledUp = "log.leveled_up"
|
||||||
|
LogPhraseEquippedNew = "log.equipped_new"
|
||||||
|
LogPhraseInventoryFullDropped = "log.inventory_full_dropped"
|
||||||
|
LogPhraseBuffActivated = "log.buff_activated"
|
||||||
|
LogPhraseHeroRevived = "log.hero_revived"
|
||||||
|
LogPhraseWanderingMerchant = "log.wandering_merchant_encounter"
|
||||||
|
LogPhraseEncounteredEnemy = "log.encountered_enemy"
|
||||||
|
LogPhraseDiedFighting = "log.died_fighting"
|
||||||
|
LogPhraseAutoReviveHours = "log.auto_revive_hours"
|
||||||
|
LogPhraseAutoReviveAfterSec = "log.auto_revive_after_sec"
|
||||||
|
LogPhrasePurchasedBuffRefill = "log.purchased_buff_refill"
|
||||||
|
LogPhrasePurchasedBuffRefillRub = "log.purchased_buff_refill_rub"
|
||||||
|
LogPhraseSubscribed = "log.subscribed"
|
||||||
|
LogPhraseUsedHealingPotion = "log.used_healing_potion"
|
||||||
|
LogPhraseAchievementUnlocked = "log.achievement_unlocked"
|
||||||
|
LogPhraseMetNPC = "log.met_npc"
|
||||||
|
LogPhraseWanderingAlmsEquipped = "log.wandering_alms_equipped"
|
||||||
|
LogPhraseWanderingAlmsDropped = "log.wandering_alms_dropped"
|
||||||
|
LogPhraseWanderingAlmsStashed = "log.wandering_alms_stashed"
|
||||||
|
LogPhraseHealedFullTown = "log.healed_full_town"
|
||||||
|
LogPhraseBoughtPotionTown = "log.bought_potion_town"
|
||||||
|
LogPhraseSoldItemsMerchant = "log.sold_items_merchant"
|
||||||
|
LogPhraseNPCSkippedVisit = "log.npc_skipped_visit"
|
||||||
|
LogPhrasePurchasedPotionFromNPC = "log.purchased_potion_from_npc"
|
||||||
|
LogPhrasePaidHealerFull = "log.paid_healer_full"
|
||||||
|
LogPhraseQuestGiverChecked = "log.quest_giver_checked"
|
||||||
|
LogPhraseQuestAccepted = "log.quest_accepted"
|
||||||
|
LogPhraseCombatHeroHit = "log.combat.hero_hit"
|
||||||
|
LogPhraseCombatHeroDodge = "log.combat.hero_dodge"
|
||||||
|
LogPhraseCombatHeroStun = "log.combat.hero_stun"
|
||||||
|
LogPhraseCombatEnemyHit = "log.combat.enemy_hit"
|
||||||
|
LogPhraseCombatEnemyBlock = "log.combat.enemy_block"
|
||||||
|
LogPhraseCombatDebuffSuffix = "log.combat.debuff_suffix"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Town visit line slugs per NPC kind (order = timed line 0..5). Unknown npcType uses generic slugs with key prefix "generic".
|
||||||
|
var townVisitLineSlugs = map[string][]string{
|
||||||
|
"merchant": {
|
||||||
|
"crates_in_shade",
|
||||||
|
"practiced_tired_smile",
|
||||||
|
"chalk_prices_twice",
|
||||||
|
"rumors_bandits_carts",
|
||||||
|
"bell_traveler_pack",
|
||||||
|
"step_back_tally_gold",
|
||||||
|
},
|
||||||
|
"healer": {
|
||||||
|
"linens_herbs_tent",
|
||||||
|
"professional_frown_onceover",
|
||||||
|
"slept_badly_nod",
|
||||||
|
"tonic_steams_table",
|
||||||
|
"blessings_salves_bandages",
|
||||||
|
"lighter_under_canvas",
|
||||||
|
},
|
||||||
|
"quest_giver": {
|
||||||
|
"scrolls_wax_desk",
|
||||||
|
"ink_stained_map_tap",
|
||||||
|
"busy_roads_noncommittal",
|
||||||
|
"draft_parchment_smell",
|
||||||
|
"squint_spine_legend",
|
||||||
|
"promise_listen_worth_it",
|
||||||
|
},
|
||||||
|
"generic": {
|
||||||
|
"town_noise_blanket",
|
||||||
|
"grain_prices_argument",
|
||||||
|
"dust_sunbeam_time",
|
||||||
|
"strap_tighten_pretend",
|
||||||
|
"dog_boring_sleeps",
|
||||||
|
"breathe_ready_move_on",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// TownVisitPhraseKey returns e.g. town_visit.merchant.bell_traveler_pack (lineIdx 0..5).
|
||||||
|
func TownVisitPhraseKey(npcType string, lineIdx int) string {
|
||||||
|
slugs, ok := townVisitLineSlugs[npcType]
|
||||||
|
keyType := npcType
|
||||||
|
if !ok {
|
||||||
|
slugs = townVisitLineSlugs["generic"]
|
||||||
|
keyType = "generic"
|
||||||
|
}
|
||||||
|
if lineIdx < 0 || lineIdx >= len(slugs) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return "town_visit." + keyType + "." + slugs[lineIdx]
|
||||||
|
}
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRoadsideSlugsWellFormed(t *testing.T) {
|
||||||
|
if len(RoadsideSlugs) == 0 {
|
||||||
|
t.Fatal("RoadsideSlugs empty")
|
||||||
|
}
|
||||||
|
for _, s := range RoadsideSlugs {
|
||||||
|
if strings.Contains(s, ".") {
|
||||||
|
t.Fatalf("roadside slug must not contain dot: %q", s)
|
||||||
|
}
|
||||||
|
if RoadsidePhraseKey(s) != "roadside."+s {
|
||||||
|
t.Fatalf("RoadsidePhraseKey(%q)=%q want roadside.%s", s, RoadsidePhraseKey(s), s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTownVisitPhraseKeyUsesSlugs(t *testing.T) {
|
||||||
|
k := TownVisitPhraseKey("merchant", 4)
|
||||||
|
if k != "town_visit.merchant.bell_traveler_pack" {
|
||||||
|
t.Fatalf("got %q", k)
|
||||||
|
}
|
||||||
|
k2 := TownVisitPhraseKey("unknown_npc", 0)
|
||||||
|
if k2 != "town_visit.generic.town_noise_blanket" {
|
||||||
|
t.Fatalf("unknown type should use generic slugs, got %q", k2)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,62 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
// RoadsideSlugs are stable suffixes; full event codes are "roadside." + slug (matches en.yml / ru.yml keys under `roadside:`).
|
||||||
|
var RoadsideSlugs = []string{
|
||||||
|
"nothing_matters_crit",
|
||||||
|
"road_chose_you",
|
||||||
|
"coin_heavier_than_sword",
|
||||||
|
"consciousness_buff",
|
||||||
|
"grass_philosophical",
|
||||||
|
"braver_tomorrow",
|
||||||
|
"hero_job_or_tax",
|
||||||
|
"scars_bookmarks",
|
||||||
|
"breaths_on_purpose",
|
||||||
|
"universe_simulation_texture",
|
||||||
|
"resting_cheating_alive",
|
||||||
|
"real_loot_npcs",
|
||||||
|
"memoir_soup",
|
||||||
|
"time_circle_hp",
|
||||||
|
"silence_loading",
|
||||||
|
"meaning_lunch_later",
|
||||||
|
"trees_gossip_breaks",
|
||||||
|
"courage_silly_face",
|
||||||
|
"miss_never_met",
|
||||||
|
"wind_advice_ignore",
|
||||||
|
"gold_boots_happiness",
|
||||||
|
"gratitude_not_dummy",
|
||||||
|
"legend_sat_tired",
|
||||||
|
"fear_debuff_curiosity",
|
||||||
|
"slimes_electric_sheep",
|
||||||
|
"patience_skill_tree",
|
||||||
|
"road_crooked_stand",
|
||||||
|
"narrate_life_xp",
|
||||||
|
"gods_patch_notes",
|
||||||
|
"rock_throne_dramatic",
|
||||||
|
"forgive_panic_roll",
|
||||||
|
"love_side_quest",
|
||||||
|
"thoughts_loot_encumbered",
|
||||||
|
"sun_sets_optimize",
|
||||||
|
"fate_bad_ui",
|
||||||
|
"wounded_poetic_upgrade",
|
||||||
|
"heroic_pose_nobody",
|
||||||
|
"wisdom_stop_swinging",
|
||||||
|
"endgame_good_chair",
|
||||||
|
"doubt_armor_unkillable",
|
||||||
|
"world_spinning_pause",
|
||||||
|
"bird_screams_relate",
|
||||||
|
"regrets_shorter_list",
|
||||||
|
"hope_hp_cynical_patch",
|
||||||
|
"courage_stubborn_pr",
|
||||||
|
"merchants_fixed_prices",
|
||||||
|
"pause_rebellion_grind",
|
||||||
|
"dirt_nails_showed_up",
|
||||||
|
"meaning_hammer",
|
||||||
|
"smile_nothing_helps",
|
||||||
|
"tomorrow_walk_tonight_breathe",
|
||||||
|
"grind_volume_down",
|
||||||
|
}
|
||||||
|
|
||||||
|
// RoadsidePhraseKey returns the full phrase code for a slug suffix.
|
||||||
|
func RoadsidePhraseKey(slug string) string {
|
||||||
|
return "roadside." + slug
|
||||||
|
}
|
||||||
@ -1,225 +1,2 @@
|
|||||||
export const en = {
|
export { en } from './loadLocales';
|
||||||
// General
|
export type { TranslationKey, Translations } from './types';
|
||||||
loading: 'Loading hero...',
|
|
||||||
close: 'Close',
|
|
||||||
cancel: 'Cancel',
|
|
||||||
confirm: 'Confirm',
|
|
||||||
empty: 'Empty',
|
|
||||||
none: 'None',
|
|
||||||
error: 'Error',
|
|
||||||
back: 'Back',
|
|
||||||
|
|
||||||
// Stats
|
|
||||||
hp: 'HP',
|
|
||||||
atk: 'ATK',
|
|
||||||
def: 'DEF',
|
|
||||||
spd: 'Speed',
|
|
||||||
moveSpd: 'Move SPD',
|
|
||||||
str: 'STR',
|
|
||||||
con: 'CON',
|
|
||||||
agi: 'AGI',
|
|
||||||
luck: 'LUCK',
|
|
||||||
xp: 'XP',
|
|
||||||
gold: 'Gold',
|
|
||||||
level: 'Lv',
|
|
||||||
stat: 'STAT',
|
|
||||||
|
|
||||||
// Hero Panel
|
|
||||||
heroStats: 'Hero Stats',
|
|
||||||
experience: 'Experience',
|
|
||||||
activeBuffs: 'Active Buffs',
|
|
||||||
activeDebuffs: 'Active Debuffs',
|
|
||||||
|
|
||||||
// Equipment
|
|
||||||
equipment: 'Equipment',
|
|
||||||
slotWeapon: 'Weapon',
|
|
||||||
slotOffHand: 'Off Hand',
|
|
||||||
slotHead: 'Head',
|
|
||||||
slotChest: 'Chest',
|
|
||||||
slotLegs: 'Legs',
|
|
||||||
slotFeet: 'Feet',
|
|
||||||
slotCloak: 'Cloak',
|
|
||||||
slotNeck: 'Neck',
|
|
||||||
slotRing: 'Ring',
|
|
||||||
slotWrist: 'Wrist',
|
|
||||||
slotHands: 'Hands',
|
|
||||||
slotQuiver: 'Quiver',
|
|
||||||
inventory: 'Inventory',
|
|
||||||
|
|
||||||
// Rarity
|
|
||||||
common: 'Common',
|
|
||||||
uncommon: 'Uncommon',
|
|
||||||
rare: 'Rare',
|
|
||||||
epic: 'Epic',
|
|
||||||
legendary: 'Legendary',
|
|
||||||
|
|
||||||
// Buff names
|
|
||||||
buffRush: 'Rush',
|
|
||||||
buffRage: 'Rage',
|
|
||||||
buffShield: 'Shield',
|
|
||||||
buffLuck: 'Luck',
|
|
||||||
buffResurrection: 'Resurrect',
|
|
||||||
buffHeal: 'Heal',
|
|
||||||
buffPowerPotion: 'Power',
|
|
||||||
buffWarCry: 'War Cry',
|
|
||||||
|
|
||||||
// Buff descriptions
|
|
||||||
buffRushDesc: '+50% movement speed',
|
|
||||||
buffRageDesc: '+100% damage',
|
|
||||||
buffShieldDesc: '-50% incoming damage',
|
|
||||||
buffLuckDesc: 'x2.5 loot drops',
|
|
||||||
buffResurrectionDesc: 'Revive at 50% HP',
|
|
||||||
buffHealDesc: '+50% HP instant',
|
|
||||||
buffPowerPotionDesc: '+150% damage',
|
|
||||||
buffWarCryDesc: '+100% attack speed',
|
|
||||||
|
|
||||||
// Buff UI
|
|
||||||
charges: 'Charges',
|
|
||||||
refillsAt: 'Refills at',
|
|
||||||
refill: 'Refill',
|
|
||||||
refillQuestion: 'Refill {label}?',
|
|
||||||
noChargesLeft: 'No charges left for {label}',
|
|
||||||
|
|
||||||
// Debuff names
|
|
||||||
debuffPoison: 'Poison',
|
|
||||||
debuffFreeze: 'Freeze',
|
|
||||||
debuffBurn: 'Burn',
|
|
||||||
debuffStun: 'Stun',
|
|
||||||
debuffSlow: 'Slow',
|
|
||||||
debuffWeaken: 'Weaken',
|
|
||||||
debuffIceSlow: 'Ice Slow',
|
|
||||||
|
|
||||||
// Quest system
|
|
||||||
questLog: 'Quest Log',
|
|
||||||
noActiveQuests: 'No active quests. Visit an NPC to accept quests!',
|
|
||||||
claimRewards: 'Claim Rewards',
|
|
||||||
claimRewardsDisabledDead: 'Revive to claim quest rewards',
|
|
||||||
questDestination: 'Destination',
|
|
||||||
abandon: 'Abandon',
|
|
||||||
acceptQuest: 'Accept',
|
|
||||||
questAccepted: 'Quest accepted!',
|
|
||||||
inProgressQuests: 'In Progress',
|
|
||||||
availableQuestsSection: 'Available Quests',
|
|
||||||
loadingQuests: 'Loading quests...',
|
|
||||||
noQuestsRightNow: 'No quests available right now.',
|
|
||||||
yourGoldLabel: 'Your gold: {amount}',
|
|
||||||
servicesSection: 'Services',
|
|
||||||
questRewardsClaimed: 'Quest rewards claimed!',
|
|
||||||
questAbandoned: 'Quest abandoned',
|
|
||||||
failedToAcceptQuest: 'Failed to accept quest',
|
|
||||||
failedToClaimRewards: 'Failed to claim rewards',
|
|
||||||
failedToAbandonQuest: 'Failed to abandon quest',
|
|
||||||
completed: 'Completed',
|
|
||||||
|
|
||||||
// NPC
|
|
||||||
questGiver: 'Quest Giver',
|
|
||||||
merchant: 'Merchant',
|
|
||||||
healer: 'Healer',
|
|
||||||
npc: 'NPC',
|
|
||||||
buyPotion: 'Buy Potion',
|
|
||||||
buyPotionForGold: 'Buy Potion ({cost}g)',
|
|
||||||
healToFull: 'Heal to Full',
|
|
||||||
healToFullForGold: 'Heal to Full ({cost}g)',
|
|
||||||
viewQuests: 'View Quests',
|
|
||||||
npcInteractTalk: 'Talk',
|
|
||||||
shopHealingPotionName: 'Healing Potion',
|
|
||||||
shopHealingPotionDesc: 'Restores health. Always handy in a pinch.',
|
|
||||||
shopFullHealName: 'Full Heal',
|
|
||||||
shopFullHealDesc: 'Restore hero to full HP.',
|
|
||||||
boughtPotion: 'Bought a potion for {cost} gold',
|
|
||||||
healedToFull: 'Healed to full HP!',
|
|
||||||
notEnoughGold: 'Not enough gold!',
|
|
||||||
failedToBuyPotion: 'Failed to buy potion',
|
|
||||||
failedToHeal: 'Failed to heal',
|
|
||||||
|
|
||||||
// Wandering NPC
|
|
||||||
giveGoldForItem: 'Give {cost} gold for a mysterious item?',
|
|
||||||
accept: 'Accept',
|
|
||||||
decline: 'Decline',
|
|
||||||
giving: 'Giving...',
|
|
||||||
|
|
||||||
// Death screen
|
|
||||||
youDied: 'YOU DIED',
|
|
||||||
reviveNow: 'REVIVE NOW',
|
|
||||||
freeRevivesLeft: 'Free revives left: {count}',
|
|
||||||
revivesUnlimitedSubscription: 'Unlimited revives (subscription)',
|
|
||||||
reviveNowWithCount: 'REVIVE NOW ({count})',
|
|
||||||
autoReviveIn: 'Auto-revive in {timer}s',
|
|
||||||
noFreeRevives: 'No free revives left \u2014 subscription required',
|
|
||||||
|
|
||||||
// Name entry
|
|
||||||
chooseHeroName: 'Choose Your Hero Name',
|
|
||||||
enterName: 'Enter a name...',
|
|
||||||
continue: 'Continue',
|
|
||||||
saving: 'Saving...',
|
|
||||||
nameTaken: 'Name already taken, try another',
|
|
||||||
invalidName: 'Invalid name',
|
|
||||||
serverError: 'Server error ({status})',
|
|
||||||
connectionFailed: 'Connection failed, please retry',
|
|
||||||
|
|
||||||
// Offline report
|
|
||||||
whileYouWereAway: 'While you were away...',
|
|
||||||
killedMonsters: 'Killed {count} monster(s)',
|
|
||||||
gainedXP: '+{xp} XP',
|
|
||||||
gainedGold: '+{gold} gold',
|
|
||||||
gainedLevels: 'Gained {levels} level(s)!',
|
|
||||||
offlineDeaths: 'Deaths: {count}',
|
|
||||||
offlineRevives: 'Auto-revives: {count}',
|
|
||||||
offlineLootFound: 'Loot:',
|
|
||||||
tapToDismiss: 'Tap anywhere to dismiss',
|
|
||||||
|
|
||||||
// Toasts
|
|
||||||
achievementUnlockedToast: 'Achievement unlocked: {title}!',
|
|
||||||
toastGainedXp: '+{xp} XP',
|
|
||||||
toastGainedGold: '+{gold} gold',
|
|
||||||
toastFoundItem: 'Found {name}',
|
|
||||||
levelUp: 'Level up! Now level {level}',
|
|
||||||
heroRevived: 'Hero revived!',
|
|
||||||
entering: 'Entering {townName}',
|
|
||||||
newEquipment: 'New {slot}: {itemName}',
|
|
||||||
potionsCollected: '+{count} potion(s)',
|
|
||||||
questProgress: '{title} ({current}/{target})',
|
|
||||||
questCompleted: 'Quest completed: {title}!',
|
|
||||||
buffLimitReached: 'Buff limit reached',
|
|
||||||
reviveNotAllowed: 'Revive not allowed',
|
|
||||||
dailyTaskClaimed: 'Daily task reward claimed!',
|
|
||||||
failedToClaimReward: 'Failed to claim reward',
|
|
||||||
|
|
||||||
// Minimap
|
|
||||||
map: 'MAP',
|
|
||||||
|
|
||||||
// Adventure log
|
|
||||||
noEventsYet: 'No events yet...',
|
|
||||||
combatLogTitle: 'Combat',
|
|
||||||
logEnteredTown: 'Entered {town}.',
|
|
||||||
logDeclinedWanderingMerchant: 'Declined the wandering merchant.',
|
|
||||||
logMerchantMovedOn: 'The wandering merchant moved on.',
|
|
||||||
|
|
||||||
// Misc
|
|
||||||
adventureLog: 'Adventure Log',
|
|
||||||
shopLabel: 'Shop',
|
|
||||||
healerLabel: 'Healer',
|
|
||||||
questLabel: 'Quest',
|
|
||||||
|
|
||||||
// Hero Sheet tabs
|
|
||||||
heroSheetQuestBadgeAria: 'Quests ready to turn in: {count}',
|
|
||||||
stats: 'Stats',
|
|
||||||
character: 'Char',
|
|
||||||
journal: 'Journal',
|
|
||||||
quests: 'Quests',
|
|
||||||
hero: 'Hero',
|
|
||||||
|
|
||||||
// Changelog (server release notes)
|
|
||||||
changelogTitle: "What's new",
|
|
||||||
changelogOk: 'Got it',
|
|
||||||
changelogVersion: 'Version {version}',
|
|
||||||
|
|
||||||
// Settings
|
|
||||||
settings: 'Settings',
|
|
||||||
language: 'Language',
|
|
||||||
english: 'English',
|
|
||||||
russian: 'Russian',
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export type TranslationKey = keyof typeof en;
|
|
||||||
export type Translations = Record<TranslationKey, string>;
|
|
||||||
|
|||||||
@ -1,80 +0,0 @@
|
|||||||
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();
|
|
||||||
@ -1,226 +0,0 @@
|
|||||||
/**
|
|
||||||
* `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,157 @@
|
|||||||
|
import { parse } from 'yaml';
|
||||||
|
import type { Locale } from './localeCodes';
|
||||||
|
import type { LocaleYamlDoc, Translations } from './types';
|
||||||
|
|
||||||
|
import enRaw from './en.yml?raw';
|
||||||
|
import ruRaw from './ru.yml?raw';
|
||||||
|
|
||||||
|
/** Matches backend townVisitLineSlugs (legacy town_visit.type.N → Nth slug). */
|
||||||
|
export const TOWN_VISIT_SLUG_ORDER: Record<string, readonly string[]> = {
|
||||||
|
merchant: [
|
||||||
|
'crates_in_shade',
|
||||||
|
'practiced_tired_smile',
|
||||||
|
'chalk_prices_twice',
|
||||||
|
'rumors_bandits_carts',
|
||||||
|
'bell_traveler_pack',
|
||||||
|
'step_back_tally_gold',
|
||||||
|
],
|
||||||
|
healer: [
|
||||||
|
'linens_herbs_tent',
|
||||||
|
'professional_frown_onceover',
|
||||||
|
'slept_badly_nod',
|
||||||
|
'tonic_steams_table',
|
||||||
|
'blessings_salves_bandages',
|
||||||
|
'lighter_under_canvas',
|
||||||
|
],
|
||||||
|
quest_giver: [
|
||||||
|
'scrolls_wax_desk',
|
||||||
|
'ink_stained_map_tap',
|
||||||
|
'busy_roads_noncommittal',
|
||||||
|
'draft_parchment_smell',
|
||||||
|
'squint_spine_legend',
|
||||||
|
'promise_listen_worth_it',
|
||||||
|
],
|
||||||
|
generic: [
|
||||||
|
'town_noise_blanket',
|
||||||
|
'grain_prices_argument',
|
||||||
|
'dust_sunbeam_time',
|
||||||
|
'strap_tighten_pretend',
|
||||||
|
'dog_boring_sleeps',
|
||||||
|
'breathe_ready_move_on',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
function assertRecordKeysMatch(name: string, a: Record<string, string>, b: Record<string, string>): void {
|
||||||
|
const ak = Object.keys(a).sort();
|
||||||
|
const bk = Object.keys(b).sort();
|
||||||
|
if (ak.length !== bk.length) {
|
||||||
|
throw new Error(`${name}: key count mismatch en=${ak.length} ru=${bk.length}`);
|
||||||
|
}
|
||||||
|
for (let i = 0; i < ak.length; i++) {
|
||||||
|
if (ak[i] !== bk[i]) {
|
||||||
|
throw new Error(`${name}: sorted keys differ at ${i}: en=${ak[i]} ru=${bk[i]}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadDoc(raw: string): LocaleYamlDoc {
|
||||||
|
const doc = parse(raw) as unknown;
|
||||||
|
if (!doc || typeof doc !== 'object') {
|
||||||
|
throw new Error('Invalid locale YAML root');
|
||||||
|
}
|
||||||
|
const d = doc as Record<string, unknown>;
|
||||||
|
const ui = d.ui as Translations | undefined;
|
||||||
|
const adventure_log = d.adventure_log as Record<string, string> | undefined;
|
||||||
|
const achievements = d.achievements as Record<string, string> | undefined;
|
||||||
|
const roadside = d.roadside as Record<string, string> | undefined;
|
||||||
|
const town_npc_visit = d.town_npc_visit as Record<string, Record<string, string>> | undefined;
|
||||||
|
const enemy_types = d.enemy_types as Record<string, string> | undefined;
|
||||||
|
if (!ui || !adventure_log || !achievements || !roadside || !town_npc_visit || !enemy_types) {
|
||||||
|
throw new Error(
|
||||||
|
'Locale YAML missing required sections (ui, adventure_log, achievements, roadside, town_npc_visit, enemy_types)',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (const [k, v] of Object.entries(enemy_types)) {
|
||||||
|
if (typeof v !== 'string' || !v.trim()) {
|
||||||
|
throw new Error(`Locale YAML empty enemy_types.${k}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const [k, v] of Object.entries(roadside)) {
|
||||||
|
if (typeof v !== 'string' || !v.trim()) {
|
||||||
|
throw new Error(`Locale YAML empty roadside.${k}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const npcType of Object.keys(TOWN_VISIT_SLUG_ORDER)) {
|
||||||
|
const pack = town_npc_visit[npcType];
|
||||||
|
if (!pack || typeof pack !== 'object') {
|
||||||
|
throw new Error(`Locale YAML missing town_npc_visit.${npcType}`);
|
||||||
|
}
|
||||||
|
for (const slug of TOWN_VISIT_SLUG_ORDER[npcType]!) {
|
||||||
|
if (typeof pack[slug] !== 'string' || !pack[slug]) {
|
||||||
|
throw new Error(`Locale YAML missing town_npc_visit.${npcType}.${slug}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { ui, adventure_log, achievements, roadside, town_npc_visit, enemy_types };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const enDoc = loadDoc(enRaw);
|
||||||
|
export const ruDoc = loadDoc(ruRaw);
|
||||||
|
assertRecordKeysMatch('enemy_types', enDoc.enemy_types, ruDoc.enemy_types);
|
||||||
|
assertRecordKeysMatch('roadside', enDoc.roadside, ruDoc.roadside);
|
||||||
|
|
||||||
|
export const en: Translations = enDoc.ui;
|
||||||
|
export const ru: Translations = ruDoc.ui;
|
||||||
|
|
||||||
|
export function adventureLogTemplate(locale: Locale, code: string): string | undefined {
|
||||||
|
const map = locale === 'ru' ? ruDoc.adventure_log : enDoc.adventure_log;
|
||||||
|
return map[code];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function achievementLogTitle(locale: Locale, achievementId: string): string {
|
||||||
|
if (!achievementId) return '';
|
||||||
|
const m = locale === 'ru' ? ruDoc.achievements : enDoc.achievements;
|
||||||
|
return m[achievementId] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Localized display name for DB `enemies.type` slug; empty if unknown. */
|
||||||
|
export function enemyTypeLabel(locale: Locale, enemyTypeSlug: string): string {
|
||||||
|
const slug = enemyTypeSlug?.trim() ?? '';
|
||||||
|
if (!slug) return '';
|
||||||
|
const map = locale === 'ru' ? ruDoc.enemy_types : enDoc.enemy_types;
|
||||||
|
return map[slug] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Phrase code `roadside.<slug>` → localized line. */
|
||||||
|
export function roadsidePhraseText(locale: Locale, phraseCode: string): string {
|
||||||
|
if (!phraseCode.startsWith('roadside.')) return '';
|
||||||
|
const slug = phraseCode.slice('roadside.'.length);
|
||||||
|
if (!slug || /^\d+$/.test(slug)) return '';
|
||||||
|
const map = locale === 'ru' ? ruDoc.roadside : enDoc.roadside;
|
||||||
|
return map[slug] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Full phrase code `town_visit.<type>.<slug>` or legacy `town_visit.<type>.<lineIdx>`. */
|
||||||
|
export function townNpcVisitPhraseText(locale: Locale, phraseCode: string): string {
|
||||||
|
if (!phraseCode.startsWith('town_visit.')) return '';
|
||||||
|
const rest = phraseCode.slice('town_visit.'.length);
|
||||||
|
const lastDot = rest.lastIndexOf('.');
|
||||||
|
if (lastDot < 0) return '';
|
||||||
|
const codeNpcType = rest.slice(0, lastDot);
|
||||||
|
const tail = rest.slice(lastDot + 1);
|
||||||
|
const packAll = locale === 'ru' ? ruDoc.town_npc_visit : enDoc.town_npc_visit;
|
||||||
|
const pack = packAll[codeNpcType] ?? packAll.generic;
|
||||||
|
if (!pack) return '';
|
||||||
|
const line = parseInt(tail, 10);
|
||||||
|
if (Number.isFinite(line) && String(line) === tail) {
|
||||||
|
const slugOrderKey = packAll[codeNpcType] != null ? codeNpcType : 'generic';
|
||||||
|
const byType = TOWN_VISIT_SLUG_ORDER as Record<string, readonly string[] | undefined>;
|
||||||
|
const picked = byType[slugOrderKey];
|
||||||
|
const order: readonly string[] =
|
||||||
|
picked !== undefined ? picked : (TOWN_VISIT_SLUG_ORDER as { generic: readonly string[] }).generic;
|
||||||
|
const i = Math.max(0, Math.min(order.length - 1, line));
|
||||||
|
const slug = order[i] as string;
|
||||||
|
return pack[slug] ?? '';
|
||||||
|
}
|
||||||
|
return pack[tail] ?? '';
|
||||||
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export type Locale = 'en' | 'ru';
|
||||||
@ -1,226 +1 @@
|
|||||||
import type { Translations } from './en';
|
export { ru } from './loadLocales';
|
||||||
|
|
||||||
export const ru: Translations = {
|
|
||||||
// General
|
|
||||||
loading: '\u0417\u0430\u0433\u0440\u0443\u0437\u043a\u0430 \u0433\u0435\u0440\u043e\u044f...',
|
|
||||||
close: '\u0417\u0430\u043a\u0440\u044b\u0442\u044c',
|
|
||||||
cancel: '\u041e\u0442\u043c\u0435\u043d\u0430',
|
|
||||||
confirm: '\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c',
|
|
||||||
empty: '\u041f\u0443\u0441\u0442\u043e',
|
|
||||||
none: '\u041d\u0435\u0442',
|
|
||||||
error: '\u041e\u0448\u0438\u0431\u043a\u0430',
|
|
||||||
back: '\u041d\u0430\u0437\u0430\u0434',
|
|
||||||
|
|
||||||
// Stats
|
|
||||||
hp: 'HP',
|
|
||||||
atk: '\u0410\u0422\u041a',
|
|
||||||
def: '\u0417\u0410\u0429',
|
|
||||||
spd: '\u0421\u043a\u043e\u0440.',
|
|
||||||
moveSpd: '\u0421\u043a\u043e\u0440. \u0434\u0432\u0438\u0436.',
|
|
||||||
str: '\u0421\u0418\u041b',
|
|
||||||
con: '\u0412\u042b\u041d',
|
|
||||||
agi: '\u041b\u041e\u0412',
|
|
||||||
luck: '\u0423\u0414\u0410\u0427',
|
|
||||||
xp: '\u041e\u041f',
|
|
||||||
gold: '\u0417\u043e\u043b\u043e\u0442\u043e',
|
|
||||||
level: '\u0423\u0440',
|
|
||||||
stat: '\u0421\u0422\u0410\u0422',
|
|
||||||
|
|
||||||
// Hero Panel
|
|
||||||
heroStats: '\u0421\u0442\u0430\u0442\u044b \u0433\u0435\u0440\u043e\u044f',
|
|
||||||
experience: '\u041e\u043f\u044b\u0442',
|
|
||||||
activeBuffs: '\u0410\u043a\u0442\u0438\u0432\u043d\u044b\u0435 \u0431\u0430\u0444\u044b',
|
|
||||||
activeDebuffs: '\u0410\u043a\u0442\u0438\u0432\u043d\u044b\u0435 \u0434\u0435\u0431\u0430\u0444\u044b',
|
|
||||||
|
|
||||||
// Equipment
|
|
||||||
equipment: '\u0421\u043d\u0430\u0440\u044f\u0436\u0435\u043d\u0438\u0435',
|
|
||||||
slotWeapon: '\u041e\u0440\u0443\u0436\u0438\u0435',
|
|
||||||
slotOffHand: '\u041b\u0435\u0432\u0430\u044f \u0440\u0443\u043a\u0430',
|
|
||||||
slotHead: '\u0413\u043e\u043b\u043e\u0432\u0430',
|
|
||||||
slotChest: '\u041d\u0430\u0433\u0440\u0443\u0434\u043d\u0438\u043a',
|
|
||||||
slotLegs: '\u041d\u043e\u0433\u0438',
|
|
||||||
slotFeet: '\u041e\u0431\u0443\u0432\u044c',
|
|
||||||
slotCloak: '\u041f\u043b\u0430\u0449',
|
|
||||||
slotNeck: '\u0428\u0435\u044f',
|
|
||||||
slotRing: '\u041a\u043e\u043b\u044c\u0446\u043e',
|
|
||||||
slotWrist: '\u0417\u0430\u043f\u044f\u0441\u0442\u044c\u0435',
|
|
||||||
slotHands: '\u0420\u0443\u043a\u0438',
|
|
||||||
slotQuiver: '\u041a\u043e\u043b\u0447\u0430\u043d',
|
|
||||||
inventory: '\u0418\u043d\u0432\u0435\u043d\u0442\u0430\u0440\u044c',
|
|
||||||
|
|
||||||
// Rarity
|
|
||||||
common: '\u041e\u0431\u044b\u0447\u043d\u043e\u0435',
|
|
||||||
uncommon: '\u041d\u0435\u043e\u0431\u044b\u0447\u043d\u043e\u0435',
|
|
||||||
rare: '\u0420\u0435\u0434\u043a\u043e\u0435',
|
|
||||||
epic: '\u042d\u043f\u0438\u0447\u0435\u0441\u043a\u043e\u0435',
|
|
||||||
legendary: '\u041b\u0435\u0433\u0435\u043d\u0434\u0430\u0440\u043d\u043e\u0435',
|
|
||||||
|
|
||||||
// Buff names
|
|
||||||
buffRush: '\u0420\u044b\u0432\u043e\u043a',
|
|
||||||
buffRage: '\u042f\u0440\u043e\u0441\u0442\u044c',
|
|
||||||
buffShield: '\u0429\u0438\u0442',
|
|
||||||
buffLuck: '\u0423\u0434\u0430\u0447\u0430',
|
|
||||||
buffResurrection: '\u0412\u043e\u0441\u043a\u0440\u0435\u0448\u0435\u043d\u0438\u0435',
|
|
||||||
buffHeal: '\u0418\u0441\u0446\u0435\u043b\u0435\u043d\u0438\u0435',
|
|
||||||
buffPowerPotion: '\u0421\u0438\u043b\u0430',
|
|
||||||
buffWarCry: '\u041a\u043b\u0438\u0447',
|
|
||||||
|
|
||||||
// Buff descriptions
|
|
||||||
buffRushDesc: '+50% \u043a \u0441\u043a\u043e\u0440\u043e\u0441\u0442\u0438 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u044f',
|
|
||||||
buffRageDesc: '+100% \u043a \u0443\u0440\u043e\u043d\u0443',
|
|
||||||
buffShieldDesc: '-50% \u0432\u0445\u043e\u0434\u044f\u0449\u0435\u0433\u043e \u0443\u0440\u043e\u043d\u0430',
|
|
||||||
buffLuckDesc: 'x2.5 \u0434\u0440\u043e\u043f \u043f\u0440\u0435\u0434\u043c\u0435\u0442\u043e\u0432',
|
|
||||||
buffResurrectionDesc: '\u0412\u043e\u0441\u043a\u0440\u0435\u0448\u0435\u043d\u0438\u0435 \u0441 50% HP',
|
|
||||||
buffHealDesc: '+50% HP \u043c\u0433\u043d\u043e\u0432\u0435\u043d\u043d\u043e',
|
|
||||||
buffPowerPotionDesc: '+150% \u043a \u0443\u0440\u043e\u043d\u0443',
|
|
||||||
buffWarCryDesc: '+100% \u043a \u0441\u043a\u043e\u0440\u043e\u0441\u0442\u0438 \u0430\u0442\u0430\u043a\u0438',
|
|
||||||
|
|
||||||
// Buff UI
|
|
||||||
charges: '\u0417\u0430\u0440\u044f\u0434\u044b',
|
|
||||||
refillsAt: '\u041e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u0432',
|
|
||||||
refill: '\u041f\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u044c',
|
|
||||||
refillQuestion: '\u041f\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u044c {label}?',
|
|
||||||
noChargesLeft: '\u041d\u0435\u0442 \u0437\u0430\u0440\u044f\u0434\u043e\u0432 \u0434\u043b\u044f {label}',
|
|
||||||
|
|
||||||
// Debuff names
|
|
||||||
debuffPoison: '\u042f\u0434',
|
|
||||||
debuffFreeze: '\u0417\u0430\u043c\u043e\u0440\u043e\u0437\u043a\u0430',
|
|
||||||
debuffBurn: '\u041e\u0436\u043e\u0433',
|
|
||||||
debuffStun: '\u041e\u0433\u043b\u0443\u0448\u0435\u043d\u0438\u0435',
|
|
||||||
debuffSlow: '\u0417\u0430\u043c\u0435\u0434\u043b\u0435\u043d\u0438\u0435',
|
|
||||||
debuffWeaken: '\u041e\u0441\u043b\u0430\u0431\u043b\u0435\u043d\u0438\u0435',
|
|
||||||
debuffIceSlow: '\u041b\u0435\u0434\u044f\u043d\u043e\u0435 \u0437\u0430\u043c\u0435\u0434\u043b\u0435\u043d\u0438\u0435',
|
|
||||||
|
|
||||||
// Quest system
|
|
||||||
questLog: '\u0416\u0443\u0440\u043d\u0430\u043b \u0437\u0430\u0434\u0430\u043d\u0438\u0439',
|
|
||||||
noActiveQuests: '\u041d\u0435\u0442 \u0430\u043a\u0442\u0438\u0432\u043d\u044b\u0445 \u0437\u0430\u0434\u0430\u043d\u0438\u0439. \u041f\u043e\u0433\u043e\u0432\u043e\u0440\u0438\u0442\u0435 \u0441 NPC!',
|
|
||||||
claimRewards: '\u041f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043d\u0430\u0433\u0440\u0430\u0434\u0443',
|
|
||||||
claimRewardsDisabledDead:
|
|
||||||
'\u0412\u043e\u0441\u043a\u0440\u0435\u0441\u043d\u0438\u0442\u0435, \u0447\u0442\u043e\u0431\u044b \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043d\u0430\u0433\u0440\u0430\u0434\u044b \u0437\u0430 \u0437\u0430\u0434\u0430\u043d\u0438\u044f',
|
|
||||||
questDestination: '\u041f\u0443\u043d\u043a\u0442 \u043d\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f',
|
|
||||||
abandon: '\u041e\u0442\u043a\u0430\u0437\u0430\u0442\u044c\u0441\u044f',
|
|
||||||
acceptQuest: '\u041f\u0440\u0438\u043d\u044f\u0442\u044c',
|
|
||||||
questAccepted: '\u0417\u0430\u0434\u0430\u043d\u0438\u0435 \u043f\u0440\u0438\u043d\u044f\u0442\u043e!',
|
|
||||||
inProgressQuests: '\u0412 \u043f\u0440\u043e\u0446\u0435\u0441\u0441\u0435',
|
|
||||||
availableQuestsSection: '\u0414\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u0435 \u043a\u0432\u0435\u0441\u0442\u044b',
|
|
||||||
loadingQuests: '\u0417\u0430\u0433\u0440\u0443\u0437\u043a\u0430 \u043a\u0432\u0435\u0441\u0442\u043e\u0432...',
|
|
||||||
noQuestsRightNow: '\u0421\u0435\u0439\u0447\u0430\u0441 \u043d\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u0445 \u043a\u0432\u0435\u0441\u0442\u043e\u0432.',
|
|
||||||
yourGoldLabel: '\u0412\u0430\u0448\u0435 \u0437\u043e\u043b\u043e\u0442\u043e: {amount}',
|
|
||||||
servicesSection: '\u0423\u0441\u043b\u0443\u0433\u0438',
|
|
||||||
questRewardsClaimed: '\u041d\u0430\u0433\u0440\u0430\u0434\u0430 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0430!',
|
|
||||||
questAbandoned: '\u0417\u0430\u0434\u0430\u043d\u0438\u0435 \u043e\u0442\u043c\u0435\u043d\u0435\u043d\u043e',
|
|
||||||
failedToAcceptQuest: '\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u0440\u0438\u043d\u044f\u0442\u044c \u0437\u0430\u0434\u0430\u043d\u0438\u0435',
|
|
||||||
failedToClaimRewards: '\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043d\u0430\u0433\u0440\u0430\u0434\u0443',
|
|
||||||
failedToAbandonQuest: '\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u0442\u043c\u0435\u043d\u0438\u0442\u044c \u0437\u0430\u0434\u0430\u043d\u0438\u0435',
|
|
||||||
completed: '\u0417\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e',
|
|
||||||
|
|
||||||
// NPC
|
|
||||||
questGiver: '\u041a\u0432\u0435\u0441\u0442\u043e\u0434\u0430\u0442\u0435\u043b\u044c',
|
|
||||||
merchant: '\u0422\u043e\u0440\u0433\u043e\u0432\u0435\u0446',
|
|
||||||
healer: '\u0426\u0435\u043b\u0438\u0442\u0435\u043b\u044c',
|
|
||||||
npc: 'NPC',
|
|
||||||
buyPotion: '\u041a\u0443\u043f\u0438\u0442\u044c \u0437\u0435\u043b\u044c\u0435',
|
|
||||||
buyPotionForGold: '\u041a\u0443\u043f\u0438\u0442\u044c \u0437\u0435\u043b\u044c\u0435 ({cost}\u0437)',
|
|
||||||
healToFull: '\u0418\u0441\u0446\u0435\u043b\u0438\u0442\u044c \u043f\u043e\u043b\u043d\u043e\u0441\u0442\u044c\u044e',
|
|
||||||
healToFullForGold: '\u041f\u043e\u043b\u043d\u043e\u0435 \u043b\u0435\u0447\u0435\u043d\u0438\u0435 ({cost}\u0437)',
|
|
||||||
viewQuests: '\u041a\u0432\u0435\u0441\u0442\u044b',
|
|
||||||
npcInteractTalk: '\u041f\u043e\u0433\u043e\u0432\u043e\u0440\u0438\u0442\u044c',
|
|
||||||
shopHealingPotionName: '\u0417\u0435\u043b\u044c\u0435 \u043b\u0435\u0447\u0435\u043d\u0438\u044f',
|
|
||||||
shopHealingPotionDesc: '\u0412\u043e\u0441\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u0435\u0442 \u0437\u0434\u043e\u0440\u043e\u0432\u044c\u0435. \u0412\u0441\u0435\u0433\u0434\u0430 \u043f\u043e\u043b\u0435\u0437\u043d\u043e.',
|
|
||||||
shopFullHealName: '\u041f\u043e\u043b\u043d\u043e\u0435 \u043b\u0435\u0447\u0435\u043d\u0438\u0435',
|
|
||||||
shopFullHealDesc: '\u0412\u043e\u0441\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u0417\u0414 \u0434\u043e \u043c\u0430\u043a\u0441\u0438\u043c\u0443\u043c\u0430.',
|
|
||||||
boughtPotion: '\u041a\u0443\u043f\u043b\u0435\u043d\u043e \u0437\u0435\u043b\u044c\u0435 \u0437\u0430 {cost} \u0437\u043e\u043b\u043e\u0442\u0430',
|
|
||||||
healedToFull: '\u0417\u0434\u043e\u0440\u043e\u0432\u044c\u0435 \u0432\u043e\u0441\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043e!',
|
|
||||||
notEnoughGold: '\u041d\u0435\u0434\u043e\u0441\u0442\u0430\u0442\u043e\u0447\u043d\u043e \u0437\u043e\u043b\u043e\u0442\u0430!',
|
|
||||||
failedToBuyPotion: '\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043a\u0443\u043f\u0438\u0442\u044c \u0437\u0435\u043b\u044c\u0435',
|
|
||||||
failedToHeal: '\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0438\u0441\u0446\u0435\u043b\u0438\u0442\u044c',
|
|
||||||
|
|
||||||
// Wandering NPC
|
|
||||||
giveGoldForItem: '\u041e\u0442\u0434\u0430\u0442\u044c {cost} \u0437\u043e\u043b\u043e\u0442\u0430 \u0437\u0430 \u0437\u0430\u0433\u0430\u0434\u043e\u0447\u043d\u044b\u0439 \u043f\u0440\u0435\u0434\u043c\u0435\u0442?',
|
|
||||||
accept: '\u041f\u0440\u0438\u043d\u044f\u0442\u044c',
|
|
||||||
decline: '\u041e\u0442\u043a\u043b\u043e\u043d\u0438\u0442\u044c',
|
|
||||||
giving: '\u041e\u0442\u0434\u0430\u044e...',
|
|
||||||
|
|
||||||
// Death screen
|
|
||||||
youDied: '\u0412\u042b \u041f\u041e\u0413\u0418\u0411\u041b\u0418',
|
|
||||||
reviveNow: '\u0412\u041e\u0421\u041a\u0420\u0415\u0421\u0418\u0422\u042c',
|
|
||||||
freeRevivesLeft: '\u0411\u0435\u0441\u043f\u043b\u0430\u0442\u043d\u044b\u0445 \u0432\u043e\u0441\u043a\u0440\u0435\u0448\u0435\u043d\u0438\u0439: {count}',
|
|
||||||
revivesUnlimitedSubscription: '\u0411\u0435\u0437\u043b\u0438\u043c\u0438\u0442\u043d\u044b\u0435 \u0432\u043e\u0441\u043a\u0440\u0435\u0448\u0435\u043d\u0438\u044f (\u043f\u043e\u0434\u043f\u0438\u0441\u043a\u0430)',
|
|
||||||
reviveNowWithCount: '\u0412\u041e\u0421\u041a\u0420\u0415\u0421\u0418\u0422\u042c ({count})',
|
|
||||||
autoReviveIn: '\u0410\u0432\u0442\u043e-\u0432\u043e\u0441\u043a\u0440\u0435\u0448\u0435\u043d\u0438\u0435 \u0447\u0435\u0440\u0435\u0437 {timer}\u0441',
|
|
||||||
noFreeRevives: '\u041d\u0435\u0442 \u0431\u0435\u0441\u043f\u043b\u0430\u0442\u043d\u044b\u0445 \u0432\u043e\u0441\u043a\u0440\u0435\u0448\u0435\u043d\u0438\u0439 \u2014 \u043d\u0443\u0436\u043d\u0430 \u043f\u043e\u0434\u043f\u0438\u0441\u043a\u0430',
|
|
||||||
|
|
||||||
// Name entry
|
|
||||||
chooseHeroName: '\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0438\u043c\u044f \u0433\u0435\u0440\u043e\u044f',
|
|
||||||
enterName: '\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043c\u044f...',
|
|
||||||
continue: '\u041f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u044c',
|
|
||||||
saving: '\u0421\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u0435...',
|
|
||||||
nameTaken: '\u0418\u043c\u044f \u0443\u0436\u0435 \u0437\u0430\u043d\u044f\u0442\u043e, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0434\u0440\u0443\u0433\u043e\u0435',
|
|
||||||
invalidName: '\u041d\u0435\u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u043e\u0435 \u0438\u043c\u044f',
|
|
||||||
serverError: '\u041e\u0448\u0438\u0431\u043a\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 ({status})',
|
|
||||||
connectionFailed: '\u041e\u0448\u0438\u0431\u043a\u0430 \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430',
|
|
||||||
|
|
||||||
// Offline report
|
|
||||||
whileYouWereAway: '\u041f\u043e\u043a\u0430 \u0432\u0430\u0441 \u043d\u0435 \u0431\u044b\u043b\u043e...',
|
|
||||||
killedMonsters: '\u0423\u0431\u0438\u0442\u043e \u043c\u043e\u043d\u0441\u0442\u0440\u043e\u0432: {count}',
|
|
||||||
gainedXP: '+{xp} \u041e\u041f',
|
|
||||||
gainedGold: '+{gold} \u0437\u043e\u043b\u043e\u0442\u0430',
|
|
||||||
gainedLevels: '\u041f\u043e\u043b\u0443\u0447\u0435\u043d\u043e \u0443\u0440\u043e\u0432\u043d\u0435\u0439: {levels}!',
|
|
||||||
offlineDeaths: '\u0421\u043c\u0435\u0440\u0442\u0435\u0439: {count}',
|
|
||||||
offlineRevives: '\u0410\u0432\u0442\u043e-\u0432\u043e\u0441\u043a\u0440\u0435\u0448\u0435\u043d\u0438\u0439: {count}',
|
|
||||||
offlineLootFound: '\u0414\u043e\u0431\u044b\u0447\u0430:',
|
|
||||||
tapToDismiss: '\u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u0434\u043b\u044f \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0435\u043d\u0438\u044f',
|
|
||||||
|
|
||||||
// Toasts
|
|
||||||
achievementUnlockedToast: '\u0414\u043e\u0441\u0442\u0438\u0436\u0435\u043d\u0438\u0435: {title}!',
|
|
||||||
toastGainedXp: '+{xp} \u041e\u041f',
|
|
||||||
toastGainedGold: '+{gold} \u0437\u043e\u043b\u043e\u0442\u0430',
|
|
||||||
toastFoundItem: '\u041d\u0430\u0439\u0434\u0435\u043d\u043e: {name}',
|
|
||||||
levelUp: '\u041d\u043e\u0432\u044b\u0439 \u0443\u0440\u043e\u0432\u0435\u043d\u044c: {level}!',
|
|
||||||
heroRevived: '\u0413\u0435\u0440\u043e\u0439 \u0432\u043e\u0441\u043a\u0440\u0435\u0448\u0435\u043d!',
|
|
||||||
entering: '\u0412\u0445\u043e\u0434 \u0432 {townName}',
|
|
||||||
newEquipment: '\u041d\u043e\u0432\u043e\u0435 {slot}: {itemName}',
|
|
||||||
potionsCollected: '+{count} \u0437\u0435\u043b\u044c\u0435(\u0439)',
|
|
||||||
questProgress: '{title} ({current}/{target})',
|
|
||||||
questCompleted: '\u0417\u0430\u0434\u0430\u043d\u0438\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u043e: {title}!',
|
|
||||||
buffLimitReached: '\u041b\u0438\u043c\u0438\u0442 \u0431\u0430\u0444\u043e\u0432 \u0434\u043e\u0441\u0442\u0438\u0433\u043d\u0443\u0442',
|
|
||||||
reviveNotAllowed: '\u0412\u043e\u0441\u043a\u0440\u0435\u0448\u0435\u043d\u0438\u0435 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e',
|
|
||||||
dailyTaskClaimed: '\u041d\u0430\u0433\u0440\u0430\u0434\u0430 \u0437\u0430 \u0437\u0430\u0434\u0430\u043d\u0438\u0435 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0430!',
|
|
||||||
failedToClaimReward: '\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043d\u0430\u0433\u0440\u0430\u0434\u0443',
|
|
||||||
|
|
||||||
// Minimap
|
|
||||||
map: '\u041a\u0410\u0420\u0422\u0410',
|
|
||||||
|
|
||||||
// Adventure log
|
|
||||||
noEventsYet: '\u041f\u043e\u043a\u0430 \u043d\u0435\u0442 \u0441\u043e\u0431\u044b\u0442\u0438\u0439...',
|
|
||||||
combatLogTitle: '\u0411\u043e\u0439',
|
|
||||||
logEnteredTown: '\u0412\u0445\u043e\u0434 \u0432 {town}.',
|
|
||||||
logDeclinedWanderingMerchant: '\u0412\u044b \u043e\u0442\u043a\u043b\u043e\u043d\u0438\u043b\u0438 \u0431\u0440\u043e\u0434\u044f\u0447\u0435\u0433\u043e \u0442\u043e\u0440\u0433\u043e\u0432\u0446\u0430.',
|
|
||||||
logMerchantMovedOn: '\u0411\u0440\u043e\u0434\u044f\u0447\u0438\u0439 \u0442\u043e\u0440\u0433\u043e\u0432\u0435\u0446 \u0443\u0448\u0451\u043b.',
|
|
||||||
|
|
||||||
// Misc
|
|
||||||
adventureLog: '\u0416\u0443\u0440\u043d\u0430\u043b \u043f\u0440\u0438\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0439',
|
|
||||||
shopLabel: '\u041c\u0430\u0433\u0430\u0437\u0438\u043d',
|
|
||||||
healerLabel: '\u0426\u0435\u043b\u0438\u0442\u0435\u043b\u044c',
|
|
||||||
questLabel: '\u041a\u0432\u0435\u0441\u0442',
|
|
||||||
|
|
||||||
// Hero Sheet tabs
|
|
||||||
heroSheetQuestBadgeAria:
|
|
||||||
'\u041a\u0432\u0435\u0441\u0442\u044b \u043a \u0441\u0434\u0430\u0447\u0435: {count}',
|
|
||||||
stats: '\u0421\u0442\u0430\u0442\u044b',
|
|
||||||
character: '\u041f\u0435\u0440\u0441.',
|
|
||||||
journal: '\u0416\u0443\u0440\u043d\u0430\u043b',
|
|
||||||
quests: '\u041a\u0432\u0435\u0441\u0442\u044b',
|
|
||||||
hero: '\u0413\u0435\u0440\u043e\u0439',
|
|
||||||
|
|
||||||
// Changelog
|
|
||||||
changelogTitle: '\u0427\u0442\u043e \u043d\u043e\u0432\u043e\u0433\u043e',
|
|
||||||
changelogOk: '\u041f\u043e\u043d\u044f\u0442\u043d\u043e',
|
|
||||||
changelogVersion: '\u0412\u0435\u0440\u0441\u0438\u044f {version}',
|
|
||||||
|
|
||||||
// Settings
|
|
||||||
settings: '\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438',
|
|
||||||
language: '\u042f\u0437\u044b\u043a',
|
|
||||||
english: 'English',
|
|
||||||
russian: '\u0420\u0443\u0441\u0441\u043a\u0438\u0439',
|
|
||||||
};
|
|
||||||
|
|||||||
@ -0,0 +1,194 @@
|
|||||||
|
/** UI strings loaded from en.yml / ru.yml (must match `ui` keys in those files). */
|
||||||
|
export interface Translations {
|
||||||
|
loading: string;
|
||||||
|
close: string;
|
||||||
|
cancel: string;
|
||||||
|
confirm: string;
|
||||||
|
empty: string;
|
||||||
|
none: string;
|
||||||
|
error: string;
|
||||||
|
back: string;
|
||||||
|
hp: string;
|
||||||
|
atk: string;
|
||||||
|
def: string;
|
||||||
|
spd: string;
|
||||||
|
moveSpd: string;
|
||||||
|
str: string;
|
||||||
|
con: string;
|
||||||
|
agi: string;
|
||||||
|
luck: string;
|
||||||
|
xp: string;
|
||||||
|
gold: string;
|
||||||
|
level: string;
|
||||||
|
stat: string;
|
||||||
|
heroStats: string;
|
||||||
|
experience: string;
|
||||||
|
activeBuffs: string;
|
||||||
|
activeDebuffs: string;
|
||||||
|
equipment: string;
|
||||||
|
slotWeapon: string;
|
||||||
|
slotOffHand: string;
|
||||||
|
slotHead: string;
|
||||||
|
slotChest: string;
|
||||||
|
slotLegs: string;
|
||||||
|
slotFeet: string;
|
||||||
|
slotCloak: string;
|
||||||
|
slotNeck: string;
|
||||||
|
slotRing: string;
|
||||||
|
slotWrist: string;
|
||||||
|
slotHands: string;
|
||||||
|
slotQuiver: string;
|
||||||
|
inventory: string;
|
||||||
|
common: string;
|
||||||
|
uncommon: string;
|
||||||
|
rare: string;
|
||||||
|
epic: string;
|
||||||
|
legendary: string;
|
||||||
|
buffRush: string;
|
||||||
|
buffRage: string;
|
||||||
|
buffShield: string;
|
||||||
|
buffLuck: string;
|
||||||
|
buffResurrection: string;
|
||||||
|
buffHeal: string;
|
||||||
|
buffPowerPotion: string;
|
||||||
|
buffWarCry: string;
|
||||||
|
buffRushDesc: string;
|
||||||
|
buffRageDesc: string;
|
||||||
|
buffShieldDesc: string;
|
||||||
|
buffLuckDesc: string;
|
||||||
|
buffResurrectionDesc: string;
|
||||||
|
buffHealDesc: string;
|
||||||
|
buffPowerPotionDesc: string;
|
||||||
|
buffWarCryDesc: string;
|
||||||
|
charges: string;
|
||||||
|
refillsAt: string;
|
||||||
|
refill: string;
|
||||||
|
refillQuestion: string;
|
||||||
|
noChargesLeft: string;
|
||||||
|
debuffPoison: string;
|
||||||
|
debuffFreeze: string;
|
||||||
|
debuffBurn: string;
|
||||||
|
debuffStun: string;
|
||||||
|
debuffSlow: string;
|
||||||
|
debuffWeaken: string;
|
||||||
|
debuffIceSlow: string;
|
||||||
|
questLog: string;
|
||||||
|
noActiveQuests: string;
|
||||||
|
claimRewards: string;
|
||||||
|
claimRewardsDisabledDead: string;
|
||||||
|
questDestination: string;
|
||||||
|
abandon: string;
|
||||||
|
acceptQuest: string;
|
||||||
|
questAccepted: string;
|
||||||
|
inProgressQuests: string;
|
||||||
|
availableQuestsSection: string;
|
||||||
|
loadingQuests: string;
|
||||||
|
noQuestsRightNow: string;
|
||||||
|
yourGoldLabel: string;
|
||||||
|
servicesSection: string;
|
||||||
|
questRewardsClaimed: string;
|
||||||
|
questAbandoned: string;
|
||||||
|
failedToAcceptQuest: string;
|
||||||
|
failedToClaimRewards: string;
|
||||||
|
failedToAbandonQuest: string;
|
||||||
|
completed: string;
|
||||||
|
questGiver: string;
|
||||||
|
merchant: string;
|
||||||
|
healer: string;
|
||||||
|
npc: string;
|
||||||
|
buyPotion: string;
|
||||||
|
buyPotionForGold: string;
|
||||||
|
healToFull: string;
|
||||||
|
healToFullForGold: string;
|
||||||
|
viewQuests: string;
|
||||||
|
npcInteractTalk: string;
|
||||||
|
shopHealingPotionName: string;
|
||||||
|
shopHealingPotionDesc: string;
|
||||||
|
shopFullHealName: string;
|
||||||
|
shopFullHealDesc: string;
|
||||||
|
boughtPotion: string;
|
||||||
|
healedToFull: string;
|
||||||
|
notEnoughGold: string;
|
||||||
|
failedToBuyPotion: string;
|
||||||
|
failedToHeal: string;
|
||||||
|
giveGoldForItem: string;
|
||||||
|
accept: string;
|
||||||
|
decline: string;
|
||||||
|
giving: string;
|
||||||
|
youDied: string;
|
||||||
|
reviveNow: string;
|
||||||
|
freeRevivesLeft: string;
|
||||||
|
revivesUnlimitedSubscription: string;
|
||||||
|
reviveNowWithCount: string;
|
||||||
|
autoReviveIn: string;
|
||||||
|
noFreeRevives: string;
|
||||||
|
chooseHeroName: string;
|
||||||
|
enterName: string;
|
||||||
|
continue: string;
|
||||||
|
saving: string;
|
||||||
|
nameTaken: string;
|
||||||
|
invalidName: string;
|
||||||
|
serverError: string;
|
||||||
|
connectionFailed: string;
|
||||||
|
whileYouWereAway: string;
|
||||||
|
killedMonsters: string;
|
||||||
|
gainedXP: string;
|
||||||
|
gainedGold: string;
|
||||||
|
gainedLevels: string;
|
||||||
|
offlineDeaths: string;
|
||||||
|
offlineRevives: string;
|
||||||
|
offlineLootFound: string;
|
||||||
|
tapToDismiss: string;
|
||||||
|
achievementUnlockedToast: string;
|
||||||
|
toastGainedXp: string;
|
||||||
|
toastGainedGold: string;
|
||||||
|
toastFoundItem: string;
|
||||||
|
levelUp: string;
|
||||||
|
heroRevived: string;
|
||||||
|
entering: string;
|
||||||
|
newEquipment: string;
|
||||||
|
potionsCollected: string;
|
||||||
|
questProgress: string;
|
||||||
|
questCompleted: string;
|
||||||
|
buffLimitReached: string;
|
||||||
|
reviveNotAllowed: string;
|
||||||
|
dailyTaskClaimed: string;
|
||||||
|
failedToClaimReward: string;
|
||||||
|
map: string;
|
||||||
|
noEventsYet: string;
|
||||||
|
combatLogTitle: string;
|
||||||
|
logEnteredTown: string;
|
||||||
|
logDeclinedWanderingMerchant: string;
|
||||||
|
logMerchantMovedOn: string;
|
||||||
|
adventureLog: string;
|
||||||
|
shopLabel: string;
|
||||||
|
healerLabel: string;
|
||||||
|
questLabel: string;
|
||||||
|
heroSheetQuestBadgeAria: string;
|
||||||
|
stats: string;
|
||||||
|
character: string;
|
||||||
|
journal: string;
|
||||||
|
quests: string;
|
||||||
|
hero: string;
|
||||||
|
changelogTitle: string;
|
||||||
|
changelogOk: string;
|
||||||
|
changelogVersion: string;
|
||||||
|
settings: string;
|
||||||
|
language: string;
|
||||||
|
english: string;
|
||||||
|
russian: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TranslationKey = keyof Translations;
|
||||||
|
|
||||||
|
export interface LocaleYamlDoc {
|
||||||
|
ui: Translations;
|
||||||
|
adventure_log: Record<string, string>;
|
||||||
|
achievements: Record<string, string>;
|
||||||
|
/** Slug → line text; phrase codes are roadside.<slug>. */
|
||||||
|
roadside: Record<string, string>;
|
||||||
|
/** npcType → slug → line text; matches backend town_visit.<type>.<slug>. */
|
||||||
|
town_npc_visit: Record<string, Record<string, string>>;
|
||||||
|
/** DB `enemies.type` slug → display name (en / ru). */
|
||||||
|
enemy_types: Record<string, string>;
|
||||||
|
}
|
||||||
@ -1 +1,6 @@
|
|||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
declare module '*.yml?raw' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue