feat: localize all UI text to Korean

Translate all user-facing strings to Korean across 25 files:
- UI screens: title, nickname, lobby, class select, waiting, game,
  shop, result, help, leaderboard, achievements, codex, stats
- Game logic: combat logs, events, achievements, mutations, emotes,
  lobby errors, session messages
- Keep English for: class names, monster names, item names, relic names

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-25 23:47:27 +09:00
parent 206ac522c5
commit f28160d4da
26 changed files with 286 additions and 286 deletions

View File

@@ -27,7 +27,7 @@ var comboDefs = []ComboDef{
{Class: entity.ClassMage, ActionType: "skill"}, {Class: entity.ClassMage, ActionType: "skill"},
{Class: entity.ClassWarrior, ActionType: "attack"}, {Class: entity.ClassWarrior, ActionType: "attack"},
}, },
Effect: ComboEffect{DamageMultiplier: 1.5, Message: "💥 ICE SHATTER! Frozen enemies shatter!"}, Effect: ComboEffect{DamageMultiplier: 1.5, Message: "💥 ICE SHATTER! 동결된 적이 산산조각!"},
}, },
{ {
Name: "Holy Assault", Name: "Holy Assault",
@@ -35,7 +35,7 @@ var comboDefs = []ComboDef{
{Class: entity.ClassHealer, ActionType: "skill"}, {Class: entity.ClassHealer, ActionType: "skill"},
{Class: entity.ClassWarrior, ActionType: "attack"}, {Class: entity.ClassWarrior, ActionType: "attack"},
}, },
Effect: ComboEffect{DamageMultiplier: 1.3, HealAll: 10, Message: "✨ HOLY ASSAULT! Blessed strikes heal the party!"}, Effect: ComboEffect{DamageMultiplier: 1.3, HealAll: 10, Message: "✨ HOLY ASSAULT! 축복받은 공격이 파티를 치유!"},
}, },
{ {
Name: "Shadow Strike", Name: "Shadow Strike",
@@ -43,7 +43,7 @@ var comboDefs = []ComboDef{
{Class: entity.ClassRogue, ActionType: "skill"}, {Class: entity.ClassRogue, ActionType: "skill"},
{Class: entity.ClassMage, ActionType: "attack"}, {Class: entity.ClassMage, ActionType: "attack"},
}, },
Effect: ComboEffect{DamageMultiplier: 1.4, Message: "🗡️ SHADOW STRIKE! Magical shadows amplify the attack!"}, Effect: ComboEffect{DamageMultiplier: 1.4, Message: "🗡️ SHADOW STRIKE! 마법의 그림자가 공격을 증폭!"},
}, },
{ {
Name: "Full Assault", Name: "Full Assault",
@@ -52,7 +52,7 @@ var comboDefs = []ComboDef{
{Class: entity.ClassMage, ActionType: "attack"}, {Class: entity.ClassMage, ActionType: "attack"},
{Class: entity.ClassRogue, ActionType: "attack"}, {Class: entity.ClassRogue, ActionType: "attack"},
}, },
Effect: ComboEffect{DamageMultiplier: 1.3, BonusDamage: 5, Message: "⚔️ FULL ASSAULT! Combined attack overwhelms!"}, Effect: ComboEffect{DamageMultiplier: 1.3, BonusDamage: 5, Message: "⚔️ FULL ASSAULT! 합동 공격으로 압도!"},
}, },
{ {
Name: "Restoration", Name: "Restoration",
@@ -60,7 +60,7 @@ var comboDefs = []ComboDef{
{Class: entity.ClassHealer, ActionType: "skill"}, {Class: entity.ClassHealer, ActionType: "skill"},
{Class: entity.ClassRogue, ActionType: "item"}, {Class: entity.ClassRogue, ActionType: "item"},
}, },
Effect: ComboEffect{HealAll: 20, Message: "💚 RESTORATION! Combined healing surges!"}, Effect: ComboEffect{HealAll: 20, Message: "💚 RESTORATION! 합동 치유가 폭발적으로 발동!"},
}, },
} }

View File

@@ -16,7 +16,7 @@ const (
) )
func (r RoomType) String() string { func (r RoomType) String() string {
return [...]string{"Combat", "Treasure", "Shop", "Event", "Empty", "Boss", "Secret", "MiniBoss"}[r] return [...]string{"전투", "보물", "상점", "이벤트", "빈 방", "보스", "비밀", "미니보스"}[r]
} }
type Tile int type Tile int

View File

@@ -179,22 +179,22 @@ func (p *Player) TickEffects() []string {
if p.HP <= 0 { if p.HP <= 0 {
p.HP = 1 // Poison can't kill, leaves at 1 HP p.HP = 1 // Poison can't kill, leaves at 1 HP
} }
msgs = append(msgs, fmt.Sprintf("%s takes %d poison damage", p.Name, e.Value)) msgs = append(msgs, fmt.Sprintf("%s 독 피해 %d", p.Name, e.Value))
case StatusBurn: case StatusBurn:
p.HP -= e.Value p.HP -= e.Value
if p.HP <= 0 { if p.HP <= 0 {
p.HP = 0 p.HP = 0
p.Dead = true p.Dead = true
} }
msgs = append(msgs, fmt.Sprintf("%s takes %d burn damage", p.Name, e.Value)) msgs = append(msgs, fmt.Sprintf("%s 화상 피해 %d", p.Name, e.Value))
case StatusFreeze: case StatusFreeze:
msgs = append(msgs, fmt.Sprintf("%s is frozen!", p.Name)) msgs = append(msgs, fmt.Sprintf("%s 동결됨!", p.Name))
case StatusBleed: case StatusBleed:
p.HP -= e.Value p.HP -= e.Value
msgs = append(msgs, fmt.Sprintf("%s takes %d bleed damage", p.Name, e.Value)) msgs = append(msgs, fmt.Sprintf("%s 출혈 피해 %d", p.Name, e.Value))
e.Value++ // Bleed intensifies each turn e.Value++ // Bleed intensifies each turn
case StatusCurse: case StatusCurse:
msgs = append(msgs, fmt.Sprintf("%s is cursed! Healing reduced", p.Name)) msgs = append(msgs, fmt.Sprintf("%s 저주 상태! 회복량 감소", p.Name))
} }
if p.HP < 0 { if p.HP < 0 {
p.HP = 0 p.HP = 0

View File

@@ -200,7 +200,7 @@ func TestBleedEffect(t *testing.T) {
p.AddEffect(ActiveEffect{Type: StatusBleed, Duration: 3, Value: 2}) p.AddEffect(ActiveEffect{Type: StatusBleed, Duration: 3, Value: 2})
msgs := p.TickEffects() msgs := p.TickEffects()
if len(msgs) == 0 || !strings.Contains(msgs[0], "bleed") { if len(msgs) == 0 || !strings.Contains(msgs[0], "출혈") {
t.Error("expected bleed damage message") t.Error("expected bleed damage message")
} }
if p.HP != startHP-2 { if p.HP != startHP-2 {
@@ -231,7 +231,7 @@ func TestFreezeTickMessage(t *testing.T) {
p := NewPlayer("Test", ClassMage) p := NewPlayer("Test", ClassMage)
p.AddEffect(ActiveEffect{Type: StatusFreeze, Duration: 1, Value: 0}) p.AddEffect(ActiveEffect{Type: StatusFreeze, Duration: 1, Value: 0})
msgs := p.TickEffects() msgs := p.TickEffects()
if len(msgs) == 0 || !strings.Contains(msgs[0], "frozen") { if len(msgs) == 0 || !strings.Contains(msgs[0], "동결") {
t.Error("expected freeze message") t.Error("expected freeze message")
} }
// Freeze duration 1 -> removed after tick // Freeze duration 1 -> removed after tick

View File

@@ -1,11 +1,11 @@
package game package game
var emotes = map[string]string{ var emotes = map[string]string{
"/hi": "👋 waves hello!", "/hi": "👋 인사합니다!",
"/gg": "🎉 says GG!", "/gg": "🎉 GG!",
"/go": "⚔️ says Let's go!", "/go": "⚔️ 가자!",
"/wait": "✋ says Wait!", "/wait": "✋ 기다려!",
"/help": "🆘 calls for help!", "/help": "🆘 도움 요청!",
} }
func ParseEmote(input string) (string, bool) { func ParseEmote(input string) (string, bool) {

View File

@@ -8,11 +8,11 @@ func TestParseEmote(t *testing.T) {
isEmote bool isEmote bool
expected string expected string
}{ }{
{"/hi", true, "👋 waves hello!"}, {"/hi", true, "👋 인사합니다!"},
{"/gg", true, "🎉 says GG!"}, {"/gg", true, "🎉 GG!"},
{"/go", true, "⚔️ says Let's go!"}, {"/go", true, "⚔️ 가자!"},
{"/wait", true, "✋ says Wait!"}, {"/wait", true, "✋ 기다려!"},
{"/help", true, "🆘 calls for help!"}, {"/help", true, "🆘 도움 요청!"},
{"/unknown", false, ""}, {"/unknown", false, ""},
{"hello", false, ""}, {"hello", false, ""},
{"", false, ""}, {"", false, ""},

View File

@@ -41,7 +41,7 @@ func (s *GameSession) EnterRoom(roomIdx int) {
s.signalCombat() s.signalCombat()
case dungeon.RoomShop: case dungeon.RoomShop:
if s.hasMutation("no_shop") { if s.hasMutation("no_shop") {
s.addLog("The shop is closed! (Weekly mutation)") s.addLog("상점이 닫혔습니다! (주간 변이)")
room.Cleared = true room.Cleared = true
return return
} }
@@ -169,7 +169,7 @@ func (s *GameSession) grantTreasure() {
floor := s.state.FloorNum floor := s.state.FloorNum
for _, p := range s.state.Players { for _, p := range s.state.Players {
if len(p.Inventory) >= s.cfg.Game.InventoryLimit { if len(p.Inventory) >= s.cfg.Game.InventoryLimit {
s.addLog(fmt.Sprintf("%s's inventory is full!", p.Name)) s.addLog(fmt.Sprintf("%s의 인벤토리가 가득 찼습니다!", p.Name))
continue continue
} }
if rand.Float64() < 0.5 { if rand.Float64() < 0.5 {
@@ -178,14 +178,14 @@ func (s *GameSession) grantTreasure() {
Name: weaponName(floor), Type: entity.ItemWeapon, Bonus: bonus, Name: weaponName(floor), Type: entity.ItemWeapon, Bonus: bonus,
} }
p.Inventory = append(p.Inventory, item) p.Inventory = append(p.Inventory, item)
s.addLog(fmt.Sprintf("%s found %s (ATK+%d)", p.Name, item.Name, item.Bonus)) s.addLog(fmt.Sprintf("%s %s 발견 (ATK+%d)", p.Name, item.Name, item.Bonus))
} else { } else {
bonus := 2 + rand.Intn(4) + floor/4 bonus := 2 + rand.Intn(4) + floor/4
item := entity.Item{ item := entity.Item{
Name: armorName(floor), Type: entity.ItemArmor, Bonus: bonus, Name: armorName(floor), Type: entity.ItemArmor, Bonus: bonus,
} }
p.Inventory = append(p.Inventory, item) p.Inventory = append(p.Inventory, item)
s.addLog(fmt.Sprintf("%s found %s (DEF+%d)", p.Name, item.Name, item.Bonus)) s.addLog(fmt.Sprintf("%s %s 발견 (DEF+%d)", p.Name, item.Name, item.Bonus))
} }
} }
} }
@@ -245,7 +245,7 @@ func armorName(floor int) string {
func (s *GameSession) triggerEvent() { func (s *GameSession) triggerEvent() {
event := PickRandomEvent() event := PickRandomEvent()
s.state.LastEventName = event.Name s.state.LastEventName = event.Name
s.addLog(fmt.Sprintf("Event: %s — %s", event.Name, event.Description)) s.addLog(fmt.Sprintf("이벤트: %s — %s", event.Name, event.Description))
// Auto-resolve with a random choice // Auto-resolve with a random choice
choice := event.Choices[rand.Intn(len(event.Choices))] choice := event.Choices[rand.Intn(len(event.Choices))]
@@ -267,10 +267,10 @@ func (s *GameSession) triggerEvent() {
if outcome.HPChange > 0 { if outcome.HPChange > 0 {
before := target.HP before := target.HP
target.Heal(outcome.HPChange) target.Heal(outcome.HPChange)
s.addLog(fmt.Sprintf(" %s heals %d HP", target.Name, target.HP-before)) s.addLog(fmt.Sprintf(" %s HP %d 회복", target.Name, target.HP-before))
} else if outcome.HPChange < 0 { } else if outcome.HPChange < 0 {
target.TakeDamage(-outcome.HPChange) target.TakeDamage(-outcome.HPChange)
s.addLog(fmt.Sprintf(" %s takes %d damage", target.Name, -outcome.HPChange)) s.addLog(fmt.Sprintf(" %s %d 피해를 받음", target.Name, -outcome.HPChange))
} }
if outcome.GoldChange != 0 { if outcome.GoldChange != 0 {
@@ -279,9 +279,9 @@ func (s *GameSession) triggerEvent() {
target.Gold = 0 target.Gold = 0
} }
if outcome.GoldChange > 0 { if outcome.GoldChange > 0 {
s.addLog(fmt.Sprintf(" %s gains %d gold", target.Name, outcome.GoldChange)) s.addLog(fmt.Sprintf(" %s 골드 %d 획득", target.Name, outcome.GoldChange))
} else { } else {
s.addLog(fmt.Sprintf(" %s loses %d gold", target.Name, -outcome.GoldChange)) s.addLog(fmt.Sprintf(" %s 골드 %d 잃음", target.Name, -outcome.GoldChange))
} }
} }
@@ -292,27 +292,27 @@ func (s *GameSession) triggerEvent() {
bonus := 3 + rand.Intn(6) + floor/3 bonus := 3 + rand.Intn(6) + floor/3
item := entity.Item{Name: weaponName(floor), Type: entity.ItemWeapon, Bonus: bonus} item := entity.Item{Name: weaponName(floor), Type: entity.ItemWeapon, Bonus: bonus}
target.Inventory = append(target.Inventory, item) target.Inventory = append(target.Inventory, item)
s.addLog(fmt.Sprintf(" %s found %s (ATK+%d)", target.Name, item.Name, item.Bonus)) s.addLog(fmt.Sprintf(" %s %s 발견 (ATK+%d)", target.Name, item.Name, item.Bonus))
} else { } else {
bonus := 2 + rand.Intn(4) + floor/4 bonus := 2 + rand.Intn(4) + floor/4
item := entity.Item{Name: armorName(floor), Type: entity.ItemArmor, Bonus: bonus} item := entity.Item{Name: armorName(floor), Type: entity.ItemArmor, Bonus: bonus}
target.Inventory = append(target.Inventory, item) target.Inventory = append(target.Inventory, item)
s.addLog(fmt.Sprintf(" %s found %s (DEF+%d)", target.Name, item.Name, item.Bonus)) s.addLog(fmt.Sprintf(" %s %s 발견 (DEF+%d)", target.Name, item.Name, item.Bonus))
} }
} else { } else {
s.addLog(fmt.Sprintf(" %s's inventory is full!", target.Name)) s.addLog(fmt.Sprintf(" %s의 인벤토리가 가득 찼습니다!", target.Name))
} }
} }
} }
func (s *GameSession) grantSecretTreasure() { func (s *GameSession) grantSecretTreasure() {
s.addLog("You discovered a secret room filled with treasure!") s.addLog("보물로 가득 찬 비밀의 방을 발견했습니다!")
floor := s.state.FloorNum floor := s.state.FloorNum
// Double treasure: grant two items per player // Double treasure: grant two items per player
for _, p := range s.state.Players { for _, p := range s.state.Players {
for i := 0; i < 2; i++ { for i := 0; i < 2; i++ {
if len(p.Inventory) >= s.cfg.Game.InventoryLimit { if len(p.Inventory) >= s.cfg.Game.InventoryLimit {
s.addLog(fmt.Sprintf("%s's inventory is full!", p.Name)) s.addLog(fmt.Sprintf("%s의 인벤토리가 가득 찼습니다!", p.Name))
break break
} }
if rand.Float64() < 0.5 { if rand.Float64() < 0.5 {
@@ -321,14 +321,14 @@ func (s *GameSession) grantSecretTreasure() {
Name: weaponName(floor), Type: entity.ItemWeapon, Bonus: bonus, Name: weaponName(floor), Type: entity.ItemWeapon, Bonus: bonus,
} }
p.Inventory = append(p.Inventory, item) p.Inventory = append(p.Inventory, item)
s.addLog(fmt.Sprintf("%s found %s (ATK+%d)", p.Name, item.Name, item.Bonus)) s.addLog(fmt.Sprintf("%s %s 발견 (ATK+%d)", p.Name, item.Name, item.Bonus))
} else { } else {
bonus := 2 + rand.Intn(4) + floor/4 bonus := 2 + rand.Intn(4) + floor/4
item := entity.Item{ item := entity.Item{
Name: armorName(floor), Type: entity.ItemArmor, Bonus: bonus, Name: armorName(floor), Type: entity.ItemArmor, Bonus: bonus,
} }
p.Inventory = append(p.Inventory, item) p.Inventory = append(p.Inventory, item)
s.addLog(fmt.Sprintf("%s found %s (DEF+%d)", p.Name, item.Name, item.Bonus)) s.addLog(fmt.Sprintf("%s %s 발견 (DEF+%d)", p.Name, item.Name, item.Bonus))
} }
} }
} }
@@ -376,7 +376,7 @@ func (s *GameSession) spawnMiniBoss() {
miniBoss.ATK = int(float64(miniBoss.ATK) * mult) miniBoss.ATK = int(float64(miniBoss.ATK) * mult)
} }
s.state.Monsters = []*entity.Monster{miniBoss} s.state.Monsters = []*entity.Monster{miniBoss}
s.addLog(fmt.Sprintf("A mini-boss appears: %s!", miniBoss.Name)) s.addLog(fmt.Sprintf("미니보스 등장: %s!", miniBoss.Name))
// Reset skill uses for all players at combat start // Reset skill uses for all players at combat start
for _, p := range s.state.Players { for _, p := range s.state.Players {

View File

@@ -117,10 +117,10 @@ func (l *Lobby) InvitePlayer(roomCode, fingerprint string) error {
defer l.mu.Unlock() defer l.mu.Unlock()
p, ok := l.online[fingerprint] p, ok := l.online[fingerprint]
if !ok { if !ok {
return fmt.Errorf("player not online") return fmt.Errorf("플레이어가 온라인이 아닙니다")
} }
if p.InRoom != "" { if p.InRoom != "" {
return fmt.Errorf("player already in a room") return fmt.Errorf("플레이어가 이미 방에 있습니다")
} }
// Store the invite as a pending field // Store the invite as a pending field
p.InRoom = "invited:" + roomCode p.InRoom = "invited:" + roomCode
@@ -148,13 +148,13 @@ func (l *Lobby) JoinRoom(code, playerName, fingerprint string) error {
defer l.mu.Unlock() defer l.mu.Unlock()
room, ok := l.rooms[code] room, ok := l.rooms[code]
if !ok { if !ok {
return fmt.Errorf("room %s not found", code) return fmt.Errorf("방 %s을(를) 찾을 수 없습니다", code)
} }
if len(room.Players) >= l.cfg.Game.MaxPlayers { if len(room.Players) >= l.cfg.Game.MaxPlayers {
return fmt.Errorf("room %s is full", code) return fmt.Errorf("방 %s이(가) 가득 찼습니다", code)
} }
if room.Status != RoomWaiting { if room.Status != RoomWaiting {
return fmt.Errorf("room %s already in progress", code) return fmt.Errorf("방 %s이(가) 이미 진행 중입니다", code)
} }
room.Players = append(room.Players, LobbyPlayer{Name: playerName, Fingerprint: fingerprint}) room.Players = append(room.Players, LobbyPlayer{Name: playerName, Fingerprint: fingerprint})
slog.Info("player joined", "room", code, "player", playerName) slog.Info("player joined", "room", code, "player", playerName)

View File

@@ -19,15 +19,15 @@ type Mutation struct {
// Mutations is the list of all available mutations. // Mutations is the list of all available mutations.
var Mutations = []Mutation{ var Mutations = []Mutation{
{ID: "no_skills", Name: "Skill Lockout", Description: "Class skills are disabled", {ID: "no_skills", Name: "스킬 봉인", Description: "직업 스킬 사용 불가",
Apply: func(cfg *config.GameConfig) { cfg.SkillUses = 0 }}, Apply: func(cfg *config.GameConfig) { cfg.SkillUses = 0 }},
{ID: "speed_run", Name: "Speed Run", Description: "Turn timeout halved", {ID: "speed_run", Name: "스피드 런", Description: "턴 제한 시간 절반",
Apply: func(cfg *config.GameConfig) { cfg.TurnTimeoutSec = max(cfg.TurnTimeoutSec/2, 2) }}, Apply: func(cfg *config.GameConfig) { cfg.TurnTimeoutSec = max(cfg.TurnTimeoutSec/2, 2) }},
{ID: "no_shop", Name: "Shop Closed", Description: "Shops are unavailable", {ID: "no_shop", Name: "상점 폐쇄", Description: "상점 이용 불가",
Apply: func(cfg *config.GameConfig) {}}, // handled at runtime in EnterRoom Apply: func(cfg *config.GameConfig) {}}, // handled at runtime in EnterRoom
{ID: "glass_cannon", Name: "Glass Cannon", Description: "Double damage, half HP", {ID: "glass_cannon", Name: "유리 대포", Description: "피해 2배, HP 절반",
Apply: func(cfg *config.GameConfig) {}}, // handled at runtime in AddPlayer/spawnMonsters Apply: func(cfg *config.GameConfig) {}}, // handled at runtime in AddPlayer/spawnMonsters
{ID: "elite_flood", Name: "Elite Flood", Description: "All monsters are elite", {ID: "elite_flood", Name: "엘리트 범람", Description: "모든 몬스터가 엘리트",
Apply: func(cfg *config.GameConfig) {}}, // handled at runtime in spawnMonsters Apply: func(cfg *config.GameConfig) {}}, // handled at runtime in spawnMonsters
} }

View File

@@ -28,211 +28,211 @@ func GetRandomEvents() []RandomEvent {
return []RandomEvent{ return []RandomEvent{
{ {
Name: "altar", Name: "altar",
Description: "You discover an ancient altar glowing with strange energy.", Description: "이상한 에너지로 빛나는 고대 제단을 발견했습니다.",
Choices: []EventChoice{ Choices: []EventChoice{
{ {
Label: "Pray at the altar", Label: "제단에서 기도하기",
Resolve: func(floor int) EventOutcome { Resolve: func(floor int) EventOutcome {
if rand.Float64() < 0.6 { if rand.Float64() < 0.6 {
heal := 15 + floor*2 heal := 15 + floor*2
return EventOutcome{HPChange: heal, Description: "The altar blesses you with healing light."} return EventOutcome{HPChange: heal, Description: "제단이 치유의 빛으로 축복합니다."}
} }
dmg := 10 + floor dmg := 10 + floor
return EventOutcome{HPChange: -dmg, Description: "The altar's energy lashes out at you!"} return EventOutcome{HPChange: -dmg, Description: "제단의 에너지가 당신을 공격합니다!"}
}, },
}, },
{ {
Label: "Offer gold", Label: "골드 바치기",
Resolve: func(floor int) EventOutcome { Resolve: func(floor int) EventOutcome {
cost := 10 + floor cost := 10 + floor
return EventOutcome{GoldChange: -cost, ItemDrop: true, Description: "You offer gold and receive a divine gift."} return EventOutcome{GoldChange: -cost, ItemDrop: true, Description: "골드를 바치고 신성한 선물을 받았습니다."}
}, },
}, },
{ {
Label: "Walk away", Label: "그냥 지나가기",
Resolve: func(floor int) EventOutcome { Resolve: func(floor int) EventOutcome {
return EventOutcome{Description: "You leave the altar undisturbed."} return EventOutcome{Description: "제단을 건드리지 않고 떠납니다."}
}, },
}, },
}, },
}, },
{ {
Name: "fountain", Name: "fountain",
Description: "A shimmering fountain bubbles in the center of the room.", Description: "방 중앙에서 빛나는 분수가 솟아오릅니다.",
Choices: []EventChoice{ Choices: []EventChoice{
{ {
Label: "Drink from the fountain", Label: "분수의 물 마시기",
Resolve: func(floor int) EventOutcome { Resolve: func(floor int) EventOutcome {
heal := 20 + floor*2 heal := 20 + floor*2
return EventOutcome{HPChange: heal, Description: "The water rejuvenates you!"} return EventOutcome{HPChange: heal, Description: "물이 당신을 활기차게 합니다!"}
}, },
}, },
{ {
Label: "Toss a coin", Label: "동전 던지기",
Resolve: func(floor int) EventOutcome { Resolve: func(floor int) EventOutcome {
if rand.Float64() < 0.5 { if rand.Float64() < 0.5 {
gold := 15 + floor*3 gold := 15 + floor*3
return EventOutcome{GoldChange: gold, Description: "The fountain rewards your generosity!"} return EventOutcome{GoldChange: gold, Description: "분수가 당신의 관대함에 보답합니다!"}
} }
return EventOutcome{GoldChange: -5, Description: "The coin sinks and nothing happens."} return EventOutcome{GoldChange: -5, Description: "동전이 가라앉고 아무 일도 일어나지 않습니다."}
}, },
}, },
}, },
}, },
{ {
Name: "merchant", Name: "merchant",
Description: "A hooded merchant appears from the shadows.", Description: "두건을 쓴 상인이 어둠 속에서 나타납니다.",
Choices: []EventChoice{ Choices: []EventChoice{
{ {
Label: "Trade gold for healing", Label: "골드로 치료 거래",
Resolve: func(floor int) EventOutcome { Resolve: func(floor int) EventOutcome {
cost := 15 + floor cost := 15 + floor
heal := 25 + floor*2 heal := 25 + floor*2
return EventOutcome{HPChange: heal, GoldChange: -cost, Description: "The merchant sells you a healing draught."} return EventOutcome{HPChange: heal, GoldChange: -cost, Description: "상인이 치유의 물약을 팝니다."}
}, },
}, },
{ {
Label: "Buy a mystery item", Label: "미스터리 아이템 구매",
Resolve: func(floor int) EventOutcome { Resolve: func(floor int) EventOutcome {
cost := 20 + floor*2 cost := 20 + floor*2
return EventOutcome{GoldChange: -cost, ItemDrop: true, Description: "The merchant hands you a wrapped package."} return EventOutcome{GoldChange: -cost, ItemDrop: true, Description: "상인이 포장된 꾸러미를 건넵니다."}
}, },
}, },
{ {
Label: "Decline", Label: "거절하기",
Resolve: func(floor int) EventOutcome { Resolve: func(floor int) EventOutcome {
return EventOutcome{Description: "The merchant vanishes into the shadows."} return EventOutcome{Description: "상인이 어둠 속으로 사라집니다."}
}, },
}, },
}, },
}, },
{ {
Name: "trap_room", Name: "trap_room",
Description: "The floor is covered with suspicious pressure plates.", Description: "바닥이 수상한 압력판으로 덮여 있습니다.",
Choices: []EventChoice{ Choices: []EventChoice{
{ {
Label: "Carefully navigate", Label: "조심히 지나가기",
Resolve: func(floor int) EventOutcome { Resolve: func(floor int) EventOutcome {
if rand.Float64() < 0.5 { if rand.Float64() < 0.5 {
return EventOutcome{Description: "You skillfully avoid all the traps!"} return EventOutcome{Description: "능숙하게 모든 함정을 피했습니다!"}
} }
dmg := 8 + floor dmg := 8 + floor
return EventOutcome{HPChange: -dmg, Description: "You trigger a trap and take damage!"} return EventOutcome{HPChange: -dmg, Description: "함정을 밟아 피해를 입었습니다!"}
}, },
}, },
{ {
Label: "Rush through", Label: "돌진하기",
Resolve: func(floor int) EventOutcome { Resolve: func(floor int) EventOutcome {
dmg := 5 + floor/2 dmg := 5 + floor/2
gold := 10 + floor*2 gold := 10 + floor*2
return EventOutcome{HPChange: -dmg, GoldChange: gold, Description: "You take minor damage but find hidden gold!"} return EventOutcome{HPChange: -dmg, GoldChange: gold, Description: "약간의 피해를 입었지만 숨겨진 골드를 발견했습니다!"}
}, },
}, },
}, },
}, },
{ {
Name: "shrine", Name: "shrine",
Description: "A glowing shrine hums with divine power.", Description: "신성한 힘으로 울리는 빛나는 성소가 있습니다.",
Choices: []EventChoice{ Choices: []EventChoice{
{ {
Label: "Kneel and pray", Label: "무릎 꿇고 기도하기",
Resolve: func(floor int) EventOutcome { Resolve: func(floor int) EventOutcome {
heal := 30 + floor*2 heal := 30 + floor*2
return EventOutcome{HPChange: heal, Description: "The shrine fills you with renewed vigor!"} return EventOutcome{HPChange: heal, Description: "성소가 새로운 활력으로 가득 채워줍니다!"}
}, },
}, },
{ {
Label: "Take the offering", Label: "제물 가져가기",
Resolve: func(floor int) EventOutcome { Resolve: func(floor int) EventOutcome {
gold := 20 + floor*3 gold := 20 + floor*3
dmg := 15 + floor dmg := 15 + floor
return EventOutcome{HPChange: -dmg, GoldChange: gold, Description: "You steal the offering but anger the spirits!"} return EventOutcome{HPChange: -dmg, GoldChange: gold, Description: "제물을 훔쳤지만 영혼들이 분노합니다!"}
}, },
}, },
}, },
}, },
{ {
Name: "chest", Name: "chest",
Description: "An ornate chest sits in the corner of the room.", Description: "방 구석에 화려한 상자가 놓여 있습니다.",
Choices: []EventChoice{ Choices: []EventChoice{
{ {
Label: "Open carefully", Label: "조심히 열기",
Resolve: func(floor int) EventOutcome { Resolve: func(floor int) EventOutcome {
if rand.Float64() < 0.7 { if rand.Float64() < 0.7 {
gold := 15 + floor*2 gold := 15 + floor*2
return EventOutcome{GoldChange: gold, Description: "The chest contains a pile of gold!"} return EventOutcome{GoldChange: gold, Description: "상자 안에 골드 더미가 있습니다!"}
} }
dmg := 12 + floor dmg := 12 + floor
return EventOutcome{HPChange: -dmg, Description: "The chest was a mimic! It bites you!"} return EventOutcome{HPChange: -dmg, Description: "상자가 미믹이었습니다! 물어뜯깁니다!"}
}, },
}, },
{ {
Label: "Smash it open", Label: "부수어 열기",
Resolve: func(floor int) EventOutcome { Resolve: func(floor int) EventOutcome {
return EventOutcome{ItemDrop: true, Description: "You smash the chest and find equipment inside!"} return EventOutcome{ItemDrop: true, Description: "상자를 부수고 안에서 장비를 발견했습니다!"}
}, },
}, },
{ {
Label: "Leave it", Label: "그냥 두기",
Resolve: func(floor int) EventOutcome { Resolve: func(floor int) EventOutcome {
return EventOutcome{Description: "Better safe than sorry."} return EventOutcome{Description: "안전한 게 최고입니다."}
}, },
}, },
}, },
}, },
{ {
Name: "ghost", Name: "ghost",
Description: "A spectral figure materializes before you.", Description: "유령 같은 형체가 눈앞에 나타납니다.",
Choices: []EventChoice{ Choices: []EventChoice{
{ {
Label: "Speak with the ghost", Label: "유령과 대화하기",
Resolve: func(floor int) EventOutcome { Resolve: func(floor int) EventOutcome {
gold := 10 + floor*2 gold := 10 + floor*2
return EventOutcome{GoldChange: gold, Description: "The ghost thanks you for listening and rewards you."} return EventOutcome{GoldChange: gold, Description: "유령이 들어줘서 감사하며 보상합니다."}
}, },
}, },
{ {
Label: "Attack the ghost", Label: "유령 공격하기",
Resolve: func(floor int) EventOutcome { Resolve: func(floor int) EventOutcome {
if rand.Float64() < 0.4 { if rand.Float64() < 0.4 {
return EventOutcome{ItemDrop: true, Description: "The ghost drops a spectral weapon as it fades!"} return EventOutcome{ItemDrop: true, Description: "유령이 사라지며 유령 무기를 떨어뜨립니다!"}
} }
dmg := 15 + floor dmg := 15 + floor
return EventOutcome{HPChange: -dmg, Description: "The ghost retaliates with ghostly fury!"} return EventOutcome{HPChange: -dmg, Description: "유령이 분노하여 반격합니다!"}
}, },
}, },
}, },
}, },
{ {
Name: "mushroom", Name: "mushroom",
Description: "Strange glowing mushrooms grow in clusters here.", Description: "이상하게 빛나는 버섯들이 무리 지어 자라고 있습니다.",
Choices: []EventChoice{ Choices: []EventChoice{
{ {
Label: "Eat a mushroom", Label: "버섯 먹기",
Resolve: func(floor int) EventOutcome { Resolve: func(floor int) EventOutcome {
r := rand.Float64() r := rand.Float64()
if r < 0.33 { if r < 0.33 {
heal := 20 + floor*2 heal := 20 + floor*2
return EventOutcome{HPChange: heal, Description: "The mushroom tastes great and heals you!"} return EventOutcome{HPChange: heal, Description: "버섯이 맛있고 치유 효과가 있습니다!"}
} else if r < 0.66 { } else if r < 0.66 {
dmg := 10 + floor dmg := 10 + floor
return EventOutcome{HPChange: -dmg, Description: "The mushroom was poisonous!"} return EventOutcome{HPChange: -dmg, Description: "독버섯이었습니다!"}
} }
gold := 10 + floor gold := 10 + floor
return EventOutcome{GoldChange: gold, Description: "The mushroom gives you strange visions... and gold falls from above!"} return EventOutcome{GoldChange: gold, Description: "버섯이 이상한 환각을 보여주고... 위에서 골드가 떨어집니다!"}
}, },
}, },
{ {
Label: "Collect and sell", Label: "채집하여 팔기",
Resolve: func(floor int) EventOutcome { Resolve: func(floor int) EventOutcome {
gold := 8 + floor gold := 8 + floor
return EventOutcome{GoldChange: gold, Description: "You carefully harvest the mushrooms for sale."} return EventOutcome{GoldChange: gold, Description: "조심히 버섯을 채집하여 판매합니다."}
}, },
}, },
{ {
Label: "Ignore them", Label: "무시하기",
Resolve: func(floor int) EventOutcome { Resolve: func(floor int) EventOutcome {
return EventOutcome{Description: "You wisely avoid the mysterious fungi."} return EventOutcome{Description: "의문의 균류를 현명하게 피합니다."}
}, },
}, },
}, },

View File

@@ -176,7 +176,7 @@ func (s *GameSession) combatLoop() {
if last, ok := s.lastActivity[p.Fingerprint]; ok { if last, ok := s.lastActivity[p.Fingerprint]; ok {
if now.Sub(last) > 60*time.Second { if now.Sub(last) > 60*time.Second {
slog.Warn("player inactive removed", "fingerprint", p.Fingerprint, "name", p.Name) slog.Warn("player inactive removed", "fingerprint", p.Fingerprint, "name", p.Name)
s.addLog(fmt.Sprintf("%s removed (disconnected)", p.Name)) s.addLog(fmt.Sprintf("%s 제거됨 (접속 끊김)", p.Name))
changed = true changed = true
continue continue
} }
@@ -335,15 +335,15 @@ func (s *GameSession) SubmitAction(playerID string, action PlayerAction) {
desc := "" desc := ""
switch action.Type { switch action.Type {
case ActionAttack: case ActionAttack:
desc = "Attacking" desc = "공격"
case ActionSkill: case ActionSkill:
desc = "Using Skill" desc = "스킬 사용"
case ActionItem: case ActionItem:
desc = "Using Item" desc = "아이템 사용"
case ActionFlee: case ActionFlee:
desc = "Fleeing" desc = "도주"
case ActionWait: case ActionWait:
desc = "Defending" desc = "방어"
} }
if s.state.SubmittedActions == nil { if s.state.SubmittedActions == nil {
s.state.SubmittedActions = make(map[string]string) s.state.SubmittedActions = make(map[string]string)
@@ -382,12 +382,12 @@ func (s *GameSession) AllocateSkillPoint(fingerprint string, branchIdx int) erro
for _, p := range s.state.Players { for _, p := range s.state.Players {
if p.Fingerprint == fingerprint { if p.Fingerprint == fingerprint {
if p.Skills == nil || p.Skills.Points <= p.Skills.Allocated { if p.Skills == nil || p.Skills.Points <= p.Skills.Allocated {
return fmt.Errorf("no skill points available") return fmt.Errorf("스킬 포인트가 없습니다")
} }
return p.Skills.Allocate(branchIdx, p.Class) return p.Skills.Allocate(branchIdx, p.Class)
} }
} }
return fmt.Errorf("player not found") return fmt.Errorf("플레이어를 찾을 수 없습니다")
} }
// BuyResult describes the outcome of a shop purchase attempt. // BuyResult describes the outcome of a shop purchase attempt.

View File

@@ -99,13 +99,13 @@ func (s *GameSession) resolvePlayerActions() {
bonus := int(float64(e.Value) * (theme.DamageMult - 1.0)) bonus := int(float64(e.Value) * (theme.DamageMult - 1.0))
if bonus > 0 { if bonus > 0 {
p.TakeDamage(bonus) p.TakeDamage(bonus)
s.addLog(fmt.Sprintf(" (%s theme: +%d damage)", theme.Name, bonus)) s.addLog(fmt.Sprintf(" (%s 테마: +%d 피해)", theme.Name, bonus))
} }
} }
} }
if p.IsDead() { if p.IsDead() {
s.addLog(fmt.Sprintf("☠ %s has fallen!", p.Name)) s.addLog(fmt.Sprintf("☠ %s 쓰러졌습니다!", p.Name))
} }
} }
} }
@@ -127,7 +127,7 @@ func (s *GameSession) resolvePlayerActions() {
} }
// Frozen players skip their action // Frozen players skip their action
if frozenPlayers[p.Fingerprint] { if frozenPlayers[p.Fingerprint] {
s.addLog(fmt.Sprintf("%s is frozen and cannot act!", p.Name)) s.addLog(fmt.Sprintf("%s 동결되어 행동할 수 없습니다!", p.Name))
continue continue
} }
action, ok := s.actions[p.Fingerprint] action, ok := s.actions[p.Fingerprint]
@@ -146,7 +146,7 @@ func (s *GameSession) resolvePlayerActions() {
intentOwners = append(intentOwners, p.Name) intentOwners = append(intentOwners, p.Name)
case ActionSkill: case ActionSkill:
if p.SkillUses <= 0 { if p.SkillUses <= 0 {
s.addLog(fmt.Sprintf("%s has no skill uses left!", p.Name)) s.addLog(fmt.Sprintf("%s 스킬 사용 횟수가 없습니다!", p.Name))
break break
} }
p.SkillUses-- p.SkillUses--
@@ -158,7 +158,7 @@ func (s *GameSession) resolvePlayerActions() {
m.TauntTurns = 2 m.TauntTurns = 2
} }
} }
s.addLog(fmt.Sprintf("%s used Taunt! Enemies focus on %s for 2 turns", p.Name, p.Name)) s.addLog(fmt.Sprintf("%s Taunt 사용! 적들이 2턴간 %s를 집중 공격합니다", p.Name, p.Name))
case entity.ClassMage: case entity.ClassMage:
skillPower := 0 skillPower := 0
if p.Skills != nil { if p.Skills != nil {
@@ -197,13 +197,13 @@ func (s *GameSession) resolvePlayerActions() {
} }
before := target.HP before := target.HP
target.Heal(healAmount) target.Heal(healAmount)
s.addLog(fmt.Sprintf("%s healed %s for %d HP", p.Name, target.Name, target.HP-before)) s.addLog(fmt.Sprintf("%s이(가) %s에게 HP %d 회복", p.Name, target.Name, target.HP-before))
case entity.ClassRogue: case entity.ClassRogue:
currentRoom := s.state.Floor.Rooms[s.state.Floor.CurrentRoom] currentRoom := s.state.Floor.Rooms[s.state.Floor.CurrentRoom]
for _, neighborIdx := range currentRoom.Neighbors { for _, neighborIdx := range currentRoom.Neighbors {
s.state.Floor.Rooms[neighborIdx].Visited = true s.state.Floor.Rooms[neighborIdx].Visited = true
} }
s.addLog(fmt.Sprintf("%s scouted nearby rooms!", p.Name)) s.addLog(fmt.Sprintf("%s 주변 방을 정찰했습니다!", p.Name))
} }
case ActionItem: case ActionItem:
found := false found := false
@@ -216,17 +216,17 @@ func (s *GameSession) resolvePlayerActions() {
} }
p.Heal(healAmt) p.Heal(healAmt)
p.Inventory = append(p.Inventory[:i], p.Inventory[i+1:]...) p.Inventory = append(p.Inventory[:i], p.Inventory[i+1:]...)
s.addLog(fmt.Sprintf("%s used %s, restored %d HP", p.Name, item.Name, p.HP-before)) s.addLog(fmt.Sprintf("%s %s 사용, HP %d 회복", p.Name, item.Name, p.HP-before))
found = true found = true
break break
} }
} }
if !found { if !found {
s.addLog(fmt.Sprintf("%s has no items to use!", p.Name)) s.addLog(fmt.Sprintf("%s 사용할 아이템이 없습니다!", p.Name))
} }
case ActionFlee: case ActionFlee:
if combat.AttemptFlee(s.cfg.Combat.FleeChance) { if combat.AttemptFlee(s.cfg.Combat.FleeChance) {
s.addLog(fmt.Sprintf("%s fled from battle!", p.Name)) s.addLog(fmt.Sprintf("%s 전투에서 도주했습니다!", p.Name))
s.state.FleeSucceeded = true s.state.FleeSucceeded = true
if s.state.SoloMode { if s.state.SoloMode {
s.state.Phase = PhaseExploring s.state.Phase = PhaseExploring
@@ -234,10 +234,10 @@ func (s *GameSession) resolvePlayerActions() {
} }
p.Fled = true p.Fled = true
} else { } else {
s.addLog(fmt.Sprintf("%s failed to flee!", p.Name)) s.addLog(fmt.Sprintf("%s 도주에 실패했습니다!", p.Name))
} }
case ActionWait: case ActionWait:
s.addLog(fmt.Sprintf("%s is defending", p.Name)) s.addLog(fmt.Sprintf("%s 방어 중", p.Name))
} }
} }
@@ -252,7 +252,7 @@ func (s *GameSession) resolvePlayerActions() {
if allFled && !s.state.SoloMode { if allFled && !s.state.SoloMode {
s.state.Phase = PhaseExploring s.state.Phase = PhaseExploring
s.state.Floor.Rooms[s.state.Floor.CurrentRoom].Cleared = true s.state.Floor.Rooms[s.state.Floor.CurrentRoom].Cleared = true
s.addLog("All players fled!") s.addLog("모든 플레이어가 도주했습니다!")
for _, p := range s.state.Players { for _, p := range s.state.Players {
p.Fled = false p.Fled = false
} }
@@ -307,16 +307,16 @@ func (s *GameSession) resolvePlayerActions() {
if r.IsAoE { if r.IsAoE {
coopStr := "" coopStr := ""
if r.CoopApplied { if r.CoopApplied {
coopStr = " (co-op!)" coopStr = " (협동!)"
} }
s.addLog(fmt.Sprintf("%s hit all enemies for %d total dmg%s", owner, r.Damage, coopStr)) s.addLog(fmt.Sprintf("%s 전체 적에게 총 %d 피해%s", owner, r.Damage, coopStr))
} else if r.TargetIdx >= 0 && r.TargetIdx < len(s.state.Monsters) { } else if r.TargetIdx >= 0 && r.TargetIdx < len(s.state.Monsters) {
target := s.state.Monsters[r.TargetIdx] target := s.state.Monsters[r.TargetIdx]
coopStr := "" coopStr := ""
if r.CoopApplied { if r.CoopApplied {
coopStr = " (co-op!)" coopStr = " (협동!)"
} }
s.addLog(fmt.Sprintf("%s hit %s for %d dmg%s", owner, target.Name, r.Damage, coopStr)) s.addLog(fmt.Sprintf("%s이(가) %s에게 %d 피해%s", owner, target.Name, r.Damage, coopStr))
} }
// Apply Life Siphon relic: heal percentage of damage dealt // Apply Life Siphon relic: heal percentage of damage dealt
if r.Damage > 0 { if r.Damage > 0 {
@@ -326,7 +326,7 @@ func (s *GameSession) resolvePlayerActions() {
heal := r.Damage * rel.Value / 100 heal := r.Damage * rel.Value / 100
if heal > 0 { if heal > 0 {
p.Heal(heal) p.Heal(heal)
s.addLog(fmt.Sprintf(" %s's Life Siphon heals %d HP", p.Name, heal)) s.addLog(fmt.Sprintf(" %s Life Siphon으로 HP %d 회복", p.Name, heal))
} }
} }
} }
@@ -359,13 +359,13 @@ func (s *GameSession) resolvePlayerActions() {
} }
if r.Effect == entity.RelicHealOnKill { if r.Effect == entity.RelicHealOnKill {
p.Heal(r.Value) p.Heal(r.Value)
s.addLog(fmt.Sprintf("%s's relic heals %d HP", p.Name, r.Value)) s.addLog(fmt.Sprintf("%s의 유물로 HP %d 회복", p.Name, r.Value))
} }
} }
p.Gold += goldReward + bonus p.Gold += goldReward + bonus
} }
} }
s.addLog(fmt.Sprintf("%s defeated! +%d gold", m.Name, goldReward)) s.addLog(fmt.Sprintf("%s 처치! +%d 골드", m.Name, goldReward))
if m.IsBoss { if m.IsBoss {
s.state.BossKilled = true s.state.BossKilled = true
s.grantBossRelic() s.grantBossRelic()
@@ -385,7 +385,7 @@ func (s *GameSession) resolvePlayerActions() {
// Check if combat is over // Check if combat is over
if len(s.state.Monsters) == 0 { if len(s.state.Monsters) == 0 {
s.state.Floor.Rooms[s.state.Floor.CurrentRoom].Cleared = true s.state.Floor.Rooms[s.state.Floor.CurrentRoom].Cleared = true
s.addLog("Room cleared!") s.addLog("방 클리어!")
for _, p := range s.state.Players { for _, p := range s.state.Players {
p.Fled = false p.Fled = false
} }
@@ -402,7 +402,7 @@ func (s *GameSession) advanceFloor() {
s.state.Phase = PhaseResult s.state.Phase = PhaseResult
s.state.Victory = true s.state.Victory = true
s.state.GameOver = true s.state.GameOver = true
s.addLog("You conquered the Catacombs!") s.addLog("카타콤을 정복했습니다!")
return return
} }
// Grant 1 skill point per floor clear // Grant 1 skill point per floor clear
@@ -421,11 +421,11 @@ func (s *GameSession) advanceFloor() {
} }
s.state.Phase = PhaseExploring s.state.Phase = PhaseExploring
s.state.CombatTurn = 0 s.state.CombatTurn = 0
s.addLog(fmt.Sprintf("Descending to B%d...", s.state.FloorNum)) s.addLog(fmt.Sprintf("B%d층으로 내려갑니다...", s.state.FloorNum))
for _, p := range s.state.Players { for _, p := range s.state.Players {
if p.IsDead() { if p.IsDead() {
p.Revive(0.30) p.Revive(0.30)
s.addLog(fmt.Sprintf("✦ %s revived at %d HP!", p.Name, p.HP)) s.addLog(fmt.Sprintf("✦ %s HP %d로 부활!", p.Name, p.HP))
} }
p.Fled = false p.Fled = false
} }
@@ -445,7 +445,7 @@ func (s *GameSession) grantBossRelic() {
if !p.IsOut() { if !p.IsOut() {
r := relics[rand.Intn(len(relics))] r := relics[rand.Intn(len(relics))]
p.Relics = append(p.Relics, r) p.Relics = append(p.Relics, r)
s.addLog(fmt.Sprintf("%s obtained relic: %s", p.Name, r.Name)) s.addLog(fmt.Sprintf("%s 유물 획득: %s", p.Name, r.Name))
} }
} }
} }
@@ -464,9 +464,9 @@ func (s *GameSession) resolveMonsterActions() {
if !p.IsOut() { if !p.IsOut() {
dmg := combat.CalcDamage(m.ATK, p.EffectiveDEF(), 0.5) dmg := combat.CalcDamage(m.ATK, p.EffectiveDEF(), 0.5)
p.TakeDamage(dmg) p.TakeDamage(dmg)
s.addLog(fmt.Sprintf("%s AoE hits %s for %d dmg", m.Name, p.Name, dmg)) s.addLog(fmt.Sprintf("%s 광역 공격으로 %s에게 %d 피해", m.Name, p.Name, dmg))
if p.IsDead() { if p.IsDead() {
s.addLog(fmt.Sprintf("☠ %s has fallen!", p.Name)) s.addLog(fmt.Sprintf("☠ %s 쓰러졌습니다!", p.Name))
} }
} }
} }
@@ -477,21 +477,21 @@ func (s *GameSession) resolveMonsterActions() {
for _, p := range s.state.Players { for _, p := range s.state.Players {
if !p.IsOut() { if !p.IsOut() {
p.AddEffect(entity.ActiveEffect{Type: entity.StatusPoison, Duration: 3, Value: 5}) p.AddEffect(entity.ActiveEffect{Type: entity.StatusPoison, Duration: 3, Value: 5})
s.addLog(fmt.Sprintf("%s poisons %s!", m.Name, p.Name)) s.addLog(fmt.Sprintf("%s이(가) %s에게 독을 걸었습니다!", m.Name, p.Name))
} }
} }
case entity.PatternBurn: case entity.PatternBurn:
for _, p := range s.state.Players { for _, p := range s.state.Players {
if !p.IsOut() { if !p.IsOut() {
p.AddEffect(entity.ActiveEffect{Type: entity.StatusBurn, Duration: 2, Value: 8}) p.AddEffect(entity.ActiveEffect{Type: entity.StatusBurn, Duration: 2, Value: 8})
s.addLog(fmt.Sprintf("%s burns %s!", m.Name, p.Name)) s.addLog(fmt.Sprintf("%s이(가) %s을(를) 불태웠습니다!", m.Name, p.Name))
} }
} }
case entity.PatternFreeze: case entity.PatternFreeze:
for _, p := range s.state.Players { for _, p := range s.state.Players {
if !p.IsOut() { if !p.IsOut() {
p.AddEffect(entity.ActiveEffect{Type: entity.StatusFreeze, Duration: 1, Value: 0}) p.AddEffect(entity.ActiveEffect{Type: entity.StatusFreeze, Duration: 1, Value: 0})
s.addLog(fmt.Sprintf("%s freezes %s!", m.Name, p.Name)) s.addLog(fmt.Sprintf("%s이(가) %s을(를) 동결시켰습니다!", m.Name, p.Name))
} }
} }
case entity.PatternHeal: case entity.PatternHeal:
@@ -500,7 +500,7 @@ func (s *GameSession) resolveMonsterActions() {
if m.HP > m.MaxHP { if m.HP > m.MaxHP {
m.HP = m.MaxHP m.HP = m.MaxHP
} }
s.addLog(fmt.Sprintf("%s regenerates %d HP!", m.Name, healAmt)) s.addLog(fmt.Sprintf("%s HP %d 재생!", m.Name, healAmt))
} }
} }
} else { } else {
@@ -509,20 +509,20 @@ func (s *GameSession) resolveMonsterActions() {
if !p.IsOut() { if !p.IsOut() {
dmg := combat.CalcDamage(m.ATK, p.EffectiveDEF(), 1.0) dmg := combat.CalcDamage(m.ATK, p.EffectiveDEF(), 1.0)
p.TakeDamage(dmg) p.TakeDamage(dmg)
s.addLog(fmt.Sprintf("%s attacks %s for %d dmg", m.Name, p.Name, dmg)) s.addLog(fmt.Sprintf("%s이(가) %s을(를) 공격하여 %d 피해", m.Name, p.Name, dmg))
if m.IsElite { if m.IsElite {
def := entity.ElitePrefixDefs[m.ElitePrefix] def := entity.ElitePrefixDefs[m.ElitePrefix]
if def.OnHit >= 0 { if def.OnHit >= 0 {
p.AddEffect(entity.ActiveEffect{Type: def.OnHit, Duration: 2, Value: 3}) p.AddEffect(entity.ActiveEffect{Type: def.OnHit, Duration: 2, Value: 3})
s.addLog(fmt.Sprintf("%s's %s effect afflicts %s!", m.Name, def.Name, p.Name)) s.addLog(fmt.Sprintf("%s %s 효과가 %s에게 적용!", m.Name, def.Name, p.Name))
} else if m.ElitePrefix == entity.PrefixVampiric { } else if m.ElitePrefix == entity.PrefixVampiric {
heal := dmg / 4 heal := dmg / 4
m.HP = min(m.HP+heal, m.MaxHP) m.HP = min(m.HP+heal, m.MaxHP)
s.addLog(fmt.Sprintf("%s drains life from %s! (+%d HP)", m.Name, p.Name, heal)) s.addLog(fmt.Sprintf("%s이(가) %s의 생명력을 흡수! (+%d HP)", m.Name, p.Name, heal))
} }
} }
if p.IsDead() { if p.IsDead() {
s.addLog(fmt.Sprintf("☠ %s has fallen!", p.Name)) s.addLog(fmt.Sprintf("☠ %s 쓰러졌습니다!", p.Name))
} }
} }
} }
@@ -540,6 +540,6 @@ func (s *GameSession) resolveMonsterActions() {
if allPlayersDead { if allPlayersDead {
s.state.Phase = PhaseResult s.state.Phase = PhaseResult
s.state.GameOver = true s.state.GameOver = true
s.addLog("Party wiped!") s.addLog("파티가 전멸했습니다!")
} }
} }

View File

@@ -14,16 +14,16 @@ type Achievement struct {
} }
var AchievementDefs = []Achievement{ var AchievementDefs = []Achievement{
{ID: "first_clear", Name: "Dungeon Delver", Description: "Clear floor 5 for the first time"}, {ID: "first_clear", Name: "던전 탐험가", Description: "처음으로 5층 클리어"},
{ID: "boss_slayer", Name: "Boss Slayer", Description: "Defeat any boss"}, {ID: "boss_slayer", Name: "보스 슬레이어", Description: "보스 처치"},
{ID: "floor10", Name: "Deep Explorer", Description: "Reach floor 10"}, {ID: "floor10", Name: "심층 탐험가", Description: "10층 도달"},
{ID: "floor20", Name: "Conqueror", Description: "Conquer the Catacombs (floor 20)"}, {ID: "floor20", Name: "정복자", Description: "카타콤 정복 (20)"},
{ID: "solo_clear", Name: "Lone Wolf", Description: "Clear floor 5 solo"}, {ID: "solo_clear", Name: "외로운 늑대", Description: "솔로로 5층 클리어"},
{ID: "gold_hoarder", Name: "Gold Hoarder", Description: "Accumulate 200+ gold in one run"}, {ID: "gold_hoarder", Name: "골드 수집가", Description: "한 번의 플레이에서 골드 200 이상 모으기"},
{ID: "no_death", Name: "Untouchable", Description: "Complete a floor without anyone dying"}, {ID: "no_death", Name: "무적", Description: "아무도 죽지 않고 층 클리어"},
{ID: "full_party", Name: "Fellowship", Description: "Start a game with 4 players"}, {ID: "full_party", Name: "동료들", Description: "4명으로 게임 시작"},
{ID: "relic_collector", Name: "Relic Collector", Description: "Collect 3+ relics in one run"}, {ID: "relic_collector", Name: "유물 수집가", Description: "한 번의 플레이에서 유물 3개 이상 수집"},
{ID: "flee_master", Name: "Tactical Retreat", Description: "Successfully flee from combat"}, {ID: "flee_master", Name: "전략적 후퇴", Description: "전투에서 도주 성공"},
} }
func (d *DB) initAchievements() error { func (d *DB) initAchievements() error {

View File

@@ -33,7 +33,7 @@ func (s *AchievementsScreen) View(ctx *Context) string {
} }
func renderAchievements(playerName string, achievements []store.Achievement, width, height int) string { func renderAchievements(playerName string, achievements []store.Achievement, width, height int) string {
title := styleHeader.Render("── Achievements ──") title := styleHeader.Render("── 업적 ──")
var content string var content string
unlocked := 0 unlocked := 0
@@ -49,9 +49,9 @@ func renderAchievements(playerName string, achievements []store.Achievement, wid
content += styleSystem.Render(" "+a.Description) + "\n" content += styleSystem.Render(" "+a.Description) + "\n"
} }
progress := fmt.Sprintf("\n %s", styleGold.Render(fmt.Sprintf("%d/%d Unlocked", unlocked, len(achievements)))) progress := fmt.Sprintf("\n %s", styleGold.Render(fmt.Sprintf("%d/%d 해금", unlocked, len(achievements))))
footer := styleSystem.Render("\n[A] Back") footer := styleSystem.Render("\n[A] 뒤로")
return lipgloss.Place(width, height, lipgloss.Center, lipgloss.Center, return lipgloss.Place(width, height, lipgloss.Center, lipgloss.Center,
lipgloss.JoinVertical(lipgloss.Center, title, "", content, progress, footer)) lipgloss.JoinVertical(lipgloss.Center, title, "", content, progress, footer))

View File

@@ -69,10 +69,10 @@ var classOptions = []struct {
name string name string
desc string desc string
}{ }{
{entity.ClassWarrior, "Warrior", "HP:120 ATK:12 DEF:8 Skill: Taunt (draw enemy fire)"}, {entity.ClassWarrior, "Warrior", "HP:120 ATK:12 DEF:8 스킬: Taunt (적의 공격을 끌어옴)"},
{entity.ClassMage, "Mage", "HP:70 ATK:20 DEF:3 Skill: Fireball (AoE damage)"}, {entity.ClassMage, "Mage", "HP:70 ATK:20 DEF:3 스킬: Fireball (광역 피해)"},
{entity.ClassHealer, "Healer", "HP:90 ATK:8 DEF:5 Skill: Heal (restore 30 HP)"}, {entity.ClassHealer, "Healer", "HP:90 ATK:8 DEF:5 스킬: Heal (HP 30 회복)"},
{entity.ClassRogue, "Rogue", "HP:85 ATK:15 DEF:4 Skill: Scout (reveal rooms)"}, {entity.ClassRogue, "Rogue", "HP:85 ATK:15 DEF:4 스킬: Scout (주변 방 탐색)"},
} }
func renderClassSelect(state classSelectState, width, height int) string { func renderClassSelect(state classSelectState, width, height int) string {
@@ -90,7 +90,7 @@ func renderClassSelect(state classSelectState, width, height int) string {
descStyle := lipgloss.NewStyle(). descStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color("240")) Foreground(lipgloss.Color("240"))
header := headerStyle.Render("── Choose Your Class ──") header := headerStyle.Render("── 직업을 선택하세요 ──")
list := "" list := ""
for i, opt := range classOptions { for i, opt := range classOptions {
marker := " " marker := " "
@@ -103,7 +103,7 @@ func renderClassSelect(state classSelectState, width, height int) string {
marker, style.Render(opt.name), descStyle.Render(opt.desc)) marker, style.Render(opt.name), descStyle.Render(opt.desc))
} }
menu := "[Up/Down] Select [Enter] Confirm" menu := "[Up/Down] 선택 [Enter] 확인"
return lipgloss.JoinVertical(lipgloss.Left, return lipgloss.JoinVertical(lipgloss.Left,
header, header,

View File

@@ -71,10 +71,10 @@ func (s *CodexScreen) Update(msg tea.Msg, ctx *Context) (Screen, tea.Cmd) {
} }
func (s *CodexScreen) View(ctx *Context) string { func (s *CodexScreen) View(ctx *Context) string {
title := styleHeader.Render("-- Codex --") title := styleHeader.Render("-- 도감 --")
// Tab headers // Tab headers
tabNames := []string{"Monsters", "Items", "Events"} tabNames := []string{"몬스터", "아이템", "이벤트"}
var tabs []string var tabs []string
for i, name := range tabNames { for i, name := range tabNames {
if i == s.tab { if i == s.tab {
@@ -117,7 +117,7 @@ func (s *CodexScreen) View(ctx *Context) string {
} }
completion := lipgloss.NewStyle().Foreground(colorCyan). completion := lipgloss.NewStyle().Foreground(colorCyan).
Render(fmt.Sprintf("Discovered: %d/%d (%.0f%%)", count, total, pct)) Render(fmt.Sprintf("발견: %d/%d (%.0f%%)", count, total, pct))
// Sort discovered keys for consistent display // Sort discovered keys for consistent display
discoveredKeys := make([]string, 0, len(discovered)) discoveredKeys := make([]string, 0, len(discovered))
@@ -151,7 +151,7 @@ func (s *CodexScreen) View(ctx *Context) string {
} }
} }
footer := styleSystem.Render("[Tab/Left/Right] Switch Tab [Esc] Back") footer := styleSystem.Render("[Tab/Left/Right] 탭 전환 [Esc] 뒤로")
content := lipgloss.JoinVertical(lipgloss.Center, content := lipgloss.JoinVertical(lipgloss.Center,
title, title,

View File

@@ -386,7 +386,7 @@ func renderMap(floor *dungeon.Floor) string {
} }
total := len(floor.Rooms) total := len(floor.Rooms)
header := headerStyle.Render(fmt.Sprintf("── Catacombs B%d: %s ── %d/%d Rooms ──", floor.Number, theme.Name, explored, total)) header := headerStyle.Render(fmt.Sprintf("── 카타콤 B%d: %s ── %d/%d ──", floor.Number, theme.Name, explored, total))
return header + "\n" + dungeon.RenderFloor(floor, floor.CurrentRoom, true) return header + "\n" + dungeon.RenderFloor(floor, floor.CurrentRoom, true)
} }
@@ -401,16 +401,16 @@ func renderHUD(state game.GameState, targetCursor int, moveCursor int, fingerpri
hpBar := renderHPBar(p.HP, p.MaxHP, 20) hpBar := renderHPBar(p.HP, p.MaxHP, 20)
status := "" status := ""
if p.IsDead() { if p.IsDead() {
status = " [DEAD]" status = " [사망]"
} }
sb.WriteString(fmt.Sprintf("%s (%s) %s %d/%d%s Gold: %d", sb.WriteString(fmt.Sprintf("%s (%s) %s %d/%d%s 골드: %d",
p.Name, p.Class, hpBar, p.HP, p.MaxHP, status, p.Gold)) p.Name, p.Class, hpBar, p.HP, p.MaxHP, status, p.Gold))
// Show inventory count // Show inventory count
itemCount := len(p.Inventory) itemCount := len(p.Inventory)
relicCount := len(p.Relics) relicCount := len(p.Relics)
if itemCount > 0 || relicCount > 0 { if itemCount > 0 || relicCount > 0 {
sb.WriteString(fmt.Sprintf(" Items:%d Relics:%d", itemCount, relicCount)) sb.WriteString(fmt.Sprintf(" 아이템:%d 유물:%d", itemCount, relicCount))
} }
sb.WriteString("\n") sb.WriteString("\n")
} }
@@ -439,7 +439,7 @@ func renderHUD(state game.GameState, targetCursor int, moveCursor int, fingerpri
sb.WriteString("\n") sb.WriteString("\n")
// Action bar // Action bar
sb.WriteString(styleAction.Render("[1]Attack [2]Skill [3]Item [4]Flee [5]Wait [Tab]Target [/]Chat")) sb.WriteString(styleAction.Render("[1]공격 [2]스킬 [3]아이템 [4]도주 [5]대기 [Tab]대상 [/]채팅"))
sb.WriteString("\n") sb.WriteString("\n")
// Timer // Timer
@@ -448,7 +448,7 @@ func renderHUD(state game.GameState, targetCursor int, moveCursor int, fingerpri
if remaining < 0 { if remaining < 0 {
remaining = 0 remaining = 0
} }
sb.WriteString(styleTimer.Render(fmt.Sprintf(" Timer: %.1fs", remaining.Seconds()))) sb.WriteString(styleTimer.Render(fmt.Sprintf(" 타이머: %.1f", remaining.Seconds())))
sb.WriteString("\n") sb.WriteString("\n")
} }
@@ -458,15 +458,15 @@ func renderHUD(state game.GameState, targetCursor int, moveCursor int, fingerpri
var skillDesc string var skillDesc string
switch p.Class { switch p.Class {
case entity.ClassWarrior: case entity.ClassWarrior:
skillDesc = "Skill: Taunt — enemies attack you for 2 turns" skillDesc = "스킬: Taunt — 2턴간 적의 공격을 끌어옴"
case entity.ClassMage: case entity.ClassMage:
skillDesc = "Skill: Fireball — AoE 0.8x dmg to all enemies" skillDesc = "스킬: Fireball — 전체 적에게 0.8배 피해"
case entity.ClassHealer: case entity.ClassHealer:
skillDesc = "Skill: Heal — restore 30 HP to an ally" skillDesc = "스킬: Heal — 아군 HP 30 회복"
case entity.ClassRogue: case entity.ClassRogue:
skillDesc = "Skill: Scout — reveal neighboring rooms" skillDesc = "스킬: Scout — 주변 방 공개"
} }
skillDesc += fmt.Sprintf(" (%d uses left)", p.SkillUses) skillDesc += fmt.Sprintf(" (남은 횟수: %d)", p.SkillUses)
sb.WriteString(styleSystem.Render(skillDesc)) sb.WriteString(styleSystem.Render(skillDesc))
sb.WriteString("\n") sb.WriteString("\n")
break break
@@ -484,7 +484,7 @@ func renderHUD(state game.GameState, targetCursor int, moveCursor int, fingerpri
r := state.Floor.Rooms[n] r := state.Floor.Rooms[n]
status := r.Type.String() status := r.Type.String()
if r.Cleared { if r.Cleared {
status = "Cleared" status = "클리어"
} }
marker := " " marker := " "
style := normalStyle style := normalStyle
@@ -492,7 +492,7 @@ func renderHUD(state game.GameState, targetCursor int, moveCursor int, fingerpri
marker = "> " marker = "> "
style = selectedStyle style = selectedStyle
} }
sb.WriteString(style.Render(fmt.Sprintf("%sRoom %d: %s", marker, n, status))) sb.WriteString(style.Render(fmt.Sprintf("%s %d: %s", marker, n, status)))
sb.WriteString("\n") sb.WriteString("\n")
} }
} }
@@ -504,7 +504,7 @@ func renderHUD(state game.GameState, targetCursor int, moveCursor int, fingerpri
branches := entity.GetBranches(p.Class) branches := entity.GetBranches(p.Class)
sb.WriteString("\n") sb.WriteString("\n")
skillStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("213")).Bold(true) skillStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("213")).Bold(true)
sb.WriteString(skillStyle.Render(fmt.Sprintf(" Skill Point Available! (%d unspent)", p.Skills.Points-p.Skills.Allocated))) sb.WriteString(skillStyle.Render(fmt.Sprintf(" 스킬 포인트 사용 가능! (미사용: %d)", p.Skills.Points-p.Skills.Allocated)))
sb.WriteString("\n") sb.WriteString("\n")
for i, branch := range branches { for i, branch := range branches {
key := "[" key := "["
@@ -513,7 +513,7 @@ func renderHUD(state game.GameState, targetCursor int, moveCursor int, fingerpri
} }
nextNode := p.Skills.Allocated nextNode := p.Skills.Allocated
if p.Skills.BranchIndex >= 0 && p.Skills.BranchIndex != i { if p.Skills.BranchIndex >= 0 && p.Skills.BranchIndex != i {
sb.WriteString(fmt.Sprintf(" [%s] %s (locked)\n", key, branch.Name)) sb.WriteString(fmt.Sprintf(" [%s] %s (잠김)\n", key, branch.Name))
} else if nextNode < 3 { } else if nextNode < 3 {
node := branch.Nodes[nextNode] node := branch.Nodes[nextNode]
sb.WriteString(fmt.Sprintf(" [%s] %s -> %s\n", key, branch.Name, node.Name)) sb.WriteString(fmt.Sprintf(" [%s] %s -> %s\n", key, branch.Name, node.Name))
@@ -522,7 +522,7 @@ func renderHUD(state game.GameState, targetCursor int, moveCursor int, fingerpri
break break
} }
} }
sb.WriteString("[Up/Down] Select [Enter] Move [Q] Quit") sb.WriteString("[Up/Down] 선택 [Enter] 이동 [Q] 종료")
} }
if state.Phase == game.PhaseCombat { if state.Phase == game.PhaseCombat {
@@ -550,19 +550,19 @@ func renderCombatLog(log []string) string {
func colorizeLog(msg string) string { func colorizeLog(msg string) string {
switch { switch {
case strings.Contains(msg, "fled"): case strings.Contains(msg, "도주"):
return styleFlee.Render(msg) return styleFlee.Render(msg)
case strings.Contains(msg, "co-op"): case strings.Contains(msg, "협동"):
return styleCoop.Render(msg) return styleCoop.Render(msg)
case strings.Contains(msg, "healed") || strings.Contains(msg, "Heal") || strings.Contains(msg, "Blessing"): case strings.Contains(msg, "회복") || strings.Contains(msg, "Heal") || strings.Contains(msg, "치유") || strings.Contains(msg, "부활"):
return styleHeal.Render(msg) return styleHeal.Render(msg)
case strings.Contains(msg, "dmg") || strings.Contains(msg, "hit") || strings.Contains(msg, "attacks") || strings.Contains(msg, "Trap"): case strings.Contains(msg, "피해") || strings.Contains(msg, "공격") || strings.Contains(msg, "Trap") || strings.Contains(msg, "함정"):
return styleDamage.Render(msg) return styleDamage.Render(msg)
case strings.Contains(msg, "Taunt") || strings.Contains(msg, "scouted"): case strings.Contains(msg, "Taunt") || strings.Contains(msg, "정찰"):
return styleStatus.Render(msg) return styleStatus.Render(msg)
case strings.Contains(msg, "gold") || strings.Contains(msg, "Gold") || strings.Contains(msg, "found"): case strings.Contains(msg, "골드") || strings.Contains(msg, "Gold") || strings.Contains(msg, "발견"):
return styleGold.Render(msg) return styleGold.Render(msg)
case strings.Contains(msg, "defeated") || strings.Contains(msg, "cleared") || strings.Contains(msg, "Descending"): case strings.Contains(msg, "처치") || strings.Contains(msg, "클리어") || strings.Contains(msg, "내려갑니다") || strings.Contains(msg, "정복"):
return styleSystem.Render(msg) return styleSystem.Render(msg)
default: default:
return msg return msg
@@ -600,14 +600,14 @@ func renderHPBar(current, max, width int) string {
func renderPartyPanel(players []*entity.Player, submittedActions map[string]string) string { func renderPartyPanel(players []*entity.Player, submittedActions map[string]string) string {
var sb strings.Builder var sb strings.Builder
sb.WriteString(styleHeader.Render(" PARTY") + "\n\n") sb.WriteString(styleHeader.Render(" 아군") + "\n\n")
for _, p := range players { for _, p := range players {
nameStr := stylePlayer.Render(fmt.Sprintf(" ♦ %s", p.Name)) nameStr := stylePlayer.Render(fmt.Sprintf(" ♦ %s", p.Name))
classStr := styleSystem.Render(fmt.Sprintf(" (%s)", p.Class)) classStr := styleSystem.Render(fmt.Sprintf(" (%s)", p.Class))
status := "" status := ""
if p.IsDead() { if p.IsDead() {
status = styleDamage.Render(" [DEAD]") status = styleDamage.Render(" [사망]")
} }
sb.WriteString(nameStr + classStr + status + "\n") sb.WriteString(nameStr + classStr + status + "\n")
@@ -637,7 +637,7 @@ func renderPartyPanel(players []*entity.Player, submittedActions map[string]stri
sb.WriteString(styleHeal.Render(fmt.Sprintf(" ✓ %s", action))) sb.WriteString(styleHeal.Render(fmt.Sprintf(" ✓ %s", action)))
sb.WriteString("\n") sb.WriteString("\n")
} else if !p.IsOut() { } else if !p.IsOut() {
sb.WriteString(styleSystem.Render(" ... Waiting")) sb.WriteString(styleSystem.Render(" ... 대기중"))
sb.WriteString("\n") sb.WriteString("\n")
} }
sb.WriteString("\n") sb.WriteString("\n")
@@ -647,7 +647,7 @@ func renderPartyPanel(players []*entity.Player, submittedActions map[string]stri
func renderEnemyPanel(monsters []*entity.Monster, targetCursor int) string { func renderEnemyPanel(monsters []*entity.Monster, targetCursor int) string {
var sb strings.Builder var sb strings.Builder
sb.WriteString(styleHeader.Render(" ENEMIES") + "\n\n") sb.WriteString(styleHeader.Render(" ") + "\n\n")
for i, m := range monsters { for i, m := range monsters {
if m.IsDead() { if m.IsDead() {
@@ -667,7 +667,7 @@ func renderEnemyPanel(monsters []*entity.Monster, targetCursor int) string {
hpBar := renderHPBar(m.HP, m.MaxHP, 12) hpBar := renderHPBar(m.HP, m.MaxHP, 12)
taunt := "" taunt := ""
if m.TauntTarget { if m.TauntTarget {
taunt = styleStatus.Render(fmt.Sprintf(" [TAUNTED %dt]", m.TauntTurns)) taunt = styleStatus.Render(fmt.Sprintf(" [도발됨 %d]", m.TauntTurns))
} }
sb.WriteString(fmt.Sprintf(" %s[%d] %s %s %d/%d%s\n\n", sb.WriteString(fmt.Sprintf(" %s[%d] %s %s %d/%d%s\n\n",
marker, i, styleEnemy.Render(m.Name), hpBar, m.HP, m.MaxHP, taunt)) marker, i, styleEnemy.Render(m.Name), hpBar, m.HP, m.MaxHP, taunt))

View File

@@ -26,38 +26,38 @@ func (s *HelpScreen) View(ctx *Context) string {
} }
func renderHelp(width, height int) string { func renderHelp(width, height int) string {
title := styleHeader.Render("── Controls ──") title := styleHeader.Render("── 조작법 ──")
sections := []struct{ header, body string }{ sections := []struct{ header, body string }{
{"Lobby", ` [C] Create room [J] Join by code {"로비", ` [C] 방 만들기 [J] 코드로 참가
[Enter] Join selected room [Enter] 선택한 방 참가
[D] Daily Challenge [H] Hard Mode toggle [D] 일일 도전 [H] 하드 모드 전환
[Q] Back to title`}, [Q] 타이틀로 돌아가기`},
{"Exploration", ` [Up/Down] Select room {"탐험", ` [Up/Down] 방 선택
[Enter] Move to room [Enter] 방으로 이동
[[] / []] Allocate skill point (branch 1/2) [[] / []] 스킬 포인트 배분 (분기 1/2)
[/] Chat [/] 채팅
[Q] Quit`}, [Q] 종료`},
{"Combat (10s per turn)", ` [1] Attack [2] Skill {"전투 (턴당 10초)", ` [1] 공격 [2] 스킬
[3] Use Item [4] Flee [3] 아이템 사용 [4] 도주
[5] Defend [Tab] Switch Target [5] 방어 [Tab] 대상 변경
[/] Chat`}, [/] 채팅`},
{"Shop", ` [1-3] Buy item {"상점", ` [1-3] 아이템 구매
[Q] Leave shop`}, [Q] 상점 나가기`},
{"Classes", ` Warrior 120HP 12ATK 8DEF Taunt (draw fire 2t) {"직업", ` Warrior 120HP 12ATK 8DEF Taunt (2턴간 적 공격 유도)
Mage 70HP 20ATK 3DEF Fireball (AoE 0.8x) Mage 70HP 20ATK 3DEF Fireball (광역 0.8)
Healer 90HP 8ATK 5DEF Heal (restore 30HP) Healer 90HP 8ATK 5DEF Heal (HP 30 회복)
Rogue 85HP 15ATK 4DEF Scout (reveal rooms)`}, Rogue 85HP 15ATK 4DEF Scout (주변 방 공개)`},
{"Multiplayer", `Up to 4 players per room {"멀티플레이", `방당 최대 4명
Co-op bonus: +10% dmg when 2+ target same enemy 협동 보너스: 2명 이상이 같은 적 공격 시 피해 +10%
Class combos trigger bonus effects 직업 콤보로 추가 효과 발동
All players ready → game starts`}, 모든 플레이어 준비 완료 시 게임 시작`},
{"Tips", `Skills have 3 uses per combat {"", `스킬은 전투당 3회 사용 가능
Items are limited to 10 per player 아이템은 플레이어당 10개 제한
Dead players revive next floor at 30% HP 사망한 플레이어는 다음 층에서 HP 30%로 부활
Bosses appear at floors 5, 10, 15, 20 보스는 5, 10, 15, 20층에 등장
Skill points: 1 per floor clear (max 3) 스킬 포인트: 층 클리어당 1포인트 (최대 3)
Weekly mutations rotate gameplay modifiers`}, 주간 변이가 게임플레이를 변경`},
} }
var content string var content string
@@ -69,7 +69,7 @@ func renderHelp(width, height int) string {
content += bodyStyle.Render(s.body) + "\n\n" content += bodyStyle.Render(s.body) + "\n\n"
} }
footer := styleSystem.Render("[H] Back") footer := styleSystem.Render("[H] 뒤로")
return lipgloss.Place(width, height, lipgloss.Center, lipgloss.Center, return lipgloss.Place(width, height, lipgloss.Center, lipgloss.Center,
lipgloss.JoinVertical(lipgloss.Center, title, "", content, footer)) lipgloss.JoinVertical(lipgloss.Center, title, "", content, footer))

View File

@@ -43,10 +43,10 @@ func (s *LeaderboardScreen) View(ctx *Context) string {
} }
func renderLeaderboard(byFloor, byGold []store.RunRecord, daily []store.DailyRecord, tab, width, height int) string { func renderLeaderboard(byFloor, byGold []store.RunRecord, daily []store.DailyRecord, tab, width, height int) string {
title := styleHeader.Render("── Leaderboard ──") title := styleHeader.Render("── 리더보드 ──")
// Tab header // Tab header
tabs := []string{"Floor", "Gold", "Daily"} tabs := []string{"층수", "골드", "일일"}
var tabLine string var tabLine string
for i, t := range tabs { for i, t := range tabs {
if i == tab { if i == tab {
@@ -60,7 +60,7 @@ func renderLeaderboard(byFloor, byGold []store.RunRecord, daily []store.DailyRec
switch tab { switch tab {
case 0: // By Floor case 0: // By Floor
content += styleCoop.Render(" Top by Floor") + "\n" content += styleCoop.Render(" 층수 순위") + "\n"
for i, r := range byFloor { for i, r := range byFloor {
if i >= 10 { if i >= 10 {
break break
@@ -75,7 +75,7 @@ func renderLeaderboard(byFloor, byGold []store.RunRecord, daily []store.DailyRec
r.Floor, styleGold.Render(fmt.Sprintf("%dg", r.Score))) r.Floor, styleGold.Render(fmt.Sprintf("%dg", r.Score)))
} }
case 1: // By Gold case 1: // By Gold
content += styleCoop.Render(" Top by Gold") + "\n" content += styleCoop.Render(" 골드 순위") + "\n"
for i, r := range byGold { for i, r := range byGold {
if i >= 10 { if i >= 10 {
break break
@@ -90,9 +90,9 @@ func renderLeaderboard(byFloor, byGold []store.RunRecord, daily []store.DailyRec
r.Floor, styleGold.Render(fmt.Sprintf("%dg", r.Score))) r.Floor, styleGold.Render(fmt.Sprintf("%dg", r.Score)))
} }
case 2: // Daily case 2: // Daily
content += styleCoop.Render(fmt.Sprintf(" Daily Challenge — %s", time.Now().Format("2006-01-02"))) + "\n" content += styleCoop.Render(fmt.Sprintf(" 일일 도전 — %s", time.Now().Format("2006-01-02"))) + "\n"
if len(daily) == 0 { if len(daily) == 0 {
content += " No daily runs yet today.\n" content += " 오늘 일일 도전 기록이 없습니다.\n"
} }
for i, r := range daily { for i, r := range daily {
if i >= 20 { if i >= 20 {
@@ -105,7 +105,7 @@ func renderLeaderboard(byFloor, byGold []store.RunRecord, daily []store.DailyRec
} }
} }
footer := styleSystem.Render("\n[Tab] Switch Tab [L] Back") footer := styleSystem.Render("\n[Tab] 탭 전환 [L] 뒤로")
return lipgloss.Place(width, height, lipgloss.Center, lipgloss.Center, return lipgloss.Place(width, height, lipgloss.Center, lipgloss.Center,
lipgloss.JoinVertical(lipgloss.Center, title, tabLine, "", content, footer)) lipgloss.JoinVertical(lipgloss.Center, title, tabLine, "", content, footer))

View File

@@ -54,9 +54,9 @@ func (s *LobbyScreen) refreshLobby(ctx *Context) {
rooms := ctx.Lobby.ListRooms() rooms := ctx.Lobby.ListRooms()
s.rooms = make([]roomInfo, len(rooms)) s.rooms = make([]roomInfo, len(rooms))
for i, r := range rooms { for i, r := range rooms {
status := "Waiting" status := "대기중"
if r.Status == game.RoomPlaying { if r.Status == game.RoomPlaying {
status = "Playing" status = "진행중"
} }
players := make([]playerInfo, len(r.Players)) players := make([]playerInfo, len(r.Players))
for j, p := range r.Players { for j, p := range r.Players {
@@ -108,7 +108,7 @@ func (s *LobbyScreen) Update(msg tea.Msg, ctx *Context) (Screen, tea.Cmd) {
// Normal lobby key handling // Normal lobby key handling
if isKey(key, "c") { if isKey(key, "c") {
if ctx.Lobby != nil { if ctx.Lobby != nil {
code := ctx.Lobby.CreateRoom(ctx.PlayerName + "'s Room") code := ctx.Lobby.CreateRoom(ctx.PlayerName + "의 방")
ctx.Lobby.JoinRoom(code, ctx.PlayerName, ctx.Fingerprint) ctx.Lobby.JoinRoom(code, ctx.PlayerName, ctx.Fingerprint)
ctx.RoomCode = code ctx.RoomCode = code
return NewClassSelectScreen(), nil return NewClassSelectScreen(), nil
@@ -135,7 +135,7 @@ func (s *LobbyScreen) Update(msg tea.Msg, ctx *Context) (Screen, tea.Cmd) {
} else if isKey(key, "d") { } else if isKey(key, "d") {
// Daily Challenge: create a private solo daily session // Daily Challenge: create a private solo daily session
if ctx.Lobby != nil { if ctx.Lobby != nil {
code := ctx.Lobby.CreateRoom(ctx.PlayerName + "'s Daily") code := ctx.Lobby.CreateRoom(ctx.PlayerName + "의 일일 도전")
if err := ctx.Lobby.JoinRoom(code, ctx.PlayerName, ctx.Fingerprint); err == nil { if err := ctx.Lobby.JoinRoom(code, ctx.PlayerName, ctx.Fingerprint); err == nil {
ctx.RoomCode = code ctx.RoomCode = code
room := ctx.Lobby.GetRoom(code) room := ctx.Lobby.GetRoom(code)
@@ -200,14 +200,14 @@ func renderLobby(state lobbyState, width, height int) string {
Border(lipgloss.RoundedBorder()). Border(lipgloss.RoundedBorder()).
Padding(0, 1) Padding(0, 1)
header := headerStyle.Render(fmt.Sprintf("── Lobby ── %d online ──", state.online)) header := headerStyle.Render(fmt.Sprintf("── 로비 ── %d명 접속중 ──", state.online))
menu := "[C] Create Room [J] Join by Code [D] Daily Challenge [Up/Down] Select [Enter] Join [Q] Back" menu := "[C] 방 만들기 [J] 코드로 참가 [D] 일일 도전 [Up/Down] 선택 [Enter] 참가 [Q] 뒤로"
if state.hardUnlocked { if state.hardUnlocked {
hardStatus := "OFF" hardStatus := "OFF"
if state.hardMode { if state.hardMode {
hardStatus = "ON" hardStatus = "ON"
} }
menu += fmt.Sprintf(" [H] Hard Mode: %s", hardStatus) menu += fmt.Sprintf(" [H] 하드 모드: %s", hardStatus)
} }
roomList := "" roomList := ""
@@ -234,11 +234,11 @@ func renderLobby(state lobbyState, width, height int) string {
} }
} }
if roomList == "" { if roomList == "" {
roomList = " No rooms available. Create one!" roomList = " 방이 없습니다. 새로 만드세요!"
} }
if state.joining { if state.joining {
inputStr := state.codeInput + strings.Repeat("_", 4-len(state.codeInput)) inputStr := state.codeInput + strings.Repeat("_", 4-len(state.codeInput))
roomList += fmt.Sprintf("\n Enter room code: [%s] (Esc to cancel)\n", inputStr) roomList += fmt.Sprintf("\n 방 코드 입력: [%s] (Esc로 취소)\n", inputStr)
} }
return lipgloss.JoinVertical(lipgloss.Left, return lipgloss.JoinVertical(lipgloss.Left,

View File

@@ -39,7 +39,7 @@ func (s *NicknameScreen) Update(msg tea.Msg, ctx *Context) (Screen, tea.Cmd) {
gs := NewGameScreen() gs := NewGameScreen()
gs.gameState = ctx.Session.GetState() gs.gameState = ctx.Session.GetState()
ctx.Session.TouchActivity(ctx.Fingerprint) ctx.Session.TouchActivity(ctx.Fingerprint)
ctx.Session.SendChat("System", ctx.PlayerName+" reconnected!") ctx.Session.SendChat("System", ctx.PlayerName+" 재접속!")
return gs, gs.pollState() return gs, gs.pollState()
} }
} }
@@ -66,7 +66,7 @@ func (s *NicknameScreen) View(ctx *Context) string {
} }
func renderNickname(input string, width, height int) string { func renderNickname(input string, width, height int) string {
title := styleHeader.Render("── Enter Your Name ──") title := styleHeader.Render("── 이름을 입력하세요 ──")
display := input display := input
if display == "" { if display == "" {
@@ -81,8 +81,8 @@ func renderNickname(input string, width, height int) string {
Padding(0, 2). Padding(0, 2).
Render(stylePlayer.Render(display)) Render(stylePlayer.Render(display))
hint := styleSystem.Render(fmt.Sprintf("(%d/12 characters)", len(input))) hint := styleSystem.Render(fmt.Sprintf("(%d/12 글자)", len(input)))
footer := styleAction.Render("[Enter] Confirm [Esc] Cancel") footer := styleAction.Render("[Enter] 확인 [Esc] 취소")
return lipgloss.Place(width, height, lipgloss.Center, lipgloss.Center, return lipgloss.Place(width, height, lipgloss.Center, lipgloss.Center,
lipgloss.JoinVertical(lipgloss.Center, title, "", inputBox, hint, "", footer)) lipgloss.JoinVertical(lipgloss.Center, title, "", inputBox, hint, "", footer))

View File

@@ -56,30 +56,30 @@ func renderResult(state game.GameState, rankings []store.RunRecord) string {
// Title // Title
if state.Victory { if state.Victory {
sb.WriteString(styleHeal.Render(" ✦ VICTORY ✦ ") + "\n\n") sb.WriteString(styleHeal.Render(" ✦ 승리 ✦ ") + "\n\n")
sb.WriteString(styleSystem.Render(" You conquered the Catacombs!") + "\n\n") sb.WriteString(styleSystem.Render(" 카타콤을 정복했습니다!") + "\n\n")
} else { } else {
sb.WriteString(styleDamage.Render(" ✦ DEFEAT ✦ ") + "\n\n") sb.WriteString(styleDamage.Render(" ✦ 패배 ✦ ") + "\n\n")
sb.WriteString(styleSystem.Render(fmt.Sprintf(" Fallen on floor B%d", state.FloorNum)) + "\n\n") sb.WriteString(styleSystem.Render(fmt.Sprintf(" B%d층에서 쓰러졌습니다", state.FloorNum)) + "\n\n")
} }
// Player summary // Player summary
sb.WriteString(styleHeader.Render("── Party Summary ──") + "\n\n") sb.WriteString(styleHeader.Render("── 파티 요약 ──") + "\n\n")
totalGold := 0 totalGold := 0
for _, p := range state.Players { for _, p := range state.Players {
status := styleHeal.Render("Alive") status := styleHeal.Render("생존")
if p.IsDead() { if p.IsDead() {
status = styleDamage.Render("Dead") status = styleDamage.Render("사망")
} }
sb.WriteString(fmt.Sprintf(" %s (%s) %s Gold: %d Items: %d Relics: %d\n", sb.WriteString(fmt.Sprintf(" %s (%s) %s 골드: %d 아이템: %d 유물: %d\n",
stylePlayer.Render(p.Name), p.Class, status, p.Gold, len(p.Inventory), len(p.Relics))) stylePlayer.Render(p.Name), p.Class, status, p.Gold, len(p.Inventory), len(p.Relics)))
totalGold += p.Gold totalGold += p.Gold
} }
sb.WriteString(fmt.Sprintf("\n Total Gold: %s\n", styleGold.Render(fmt.Sprintf("%d", totalGold)))) sb.WriteString(fmt.Sprintf("\n 총 골드: %s\n", styleGold.Render(fmt.Sprintf("%d", totalGold))))
// Rankings // Rankings
if len(rankings) > 0 { if len(rankings) > 0 {
sb.WriteString("\n" + styleHeader.Render("── Top Runs ──") + "\n\n") sb.WriteString("\n" + styleHeader.Render("── 최고 기록 ──") + "\n\n")
for i, r := range rankings { for i, r := range rankings {
medal := " " medal := " "
switch i { switch i {
@@ -90,11 +90,11 @@ func renderResult(state game.GameState, rankings []store.RunRecord) string {
case 2: case 2:
medal = styleGold.Render("🥉") medal = styleGold.Render("🥉")
} }
sb.WriteString(fmt.Sprintf(" %s %s Floor B%d Score: %d\n", medal, r.Player, r.Floor, r.Score)) sb.WriteString(fmt.Sprintf(" %s %s B%d 점수: %d\n", medal, r.Player, r.Floor, r.Score))
} }
} }
sb.WriteString("\n" + styleAction.Render(" [Enter] Return to Lobby") + "\n") sb.WriteString("\n" + styleAction.Render(" [Enter] 로비로 돌아가기") + "\n")
return sb.String() return sb.String()
} }

View File

@@ -27,13 +27,13 @@ func (s *ShopScreen) Update(msg tea.Msg, ctx *Context) (Screen, tea.Cmd) {
idx := int(key.String()[0] - '1') idx := int(key.String()[0] - '1')
switch ctx.Session.BuyItem(ctx.Fingerprint, idx) { switch ctx.Session.BuyItem(ctx.Fingerprint, idx) {
case game.BuyOK: case game.BuyOK:
s.shopMsg = "Purchased!" s.shopMsg = "구매 완료!"
case game.BuyNoGold: case game.BuyNoGold:
s.shopMsg = "Not enough gold!" s.shopMsg = "골드가 부족합니다!"
case game.BuyInventoryFull: case game.BuyInventoryFull:
s.shopMsg = "Inventory full!" s.shopMsg = "인벤토리가 가득 찼습니다!"
default: default:
s.shopMsg = "Cannot buy that!" s.shopMsg = "구매할 수 없습니다!"
} }
s.gameState = ctx.Session.GetState() s.gameState = ctx.Session.GetState()
} }
@@ -76,23 +76,23 @@ func renderShop(state game.GameState, width, height int, shopMsg string) string
Foreground(lipgloss.Color("196")). Foreground(lipgloss.Color("196")).
Bold(true) Bold(true)
header := headerStyle.Render("── Shop ──") header := headerStyle.Render("── 상점 ──")
// Show current player's gold // Show current player's gold
goldLine := "" goldLine := ""
for _, p := range state.Players { for _, p := range state.Players {
inventoryCount := len(p.Inventory) inventoryCount := len(p.Inventory)
goldLine += goldStyle.Render(fmt.Sprintf(" %s — Gold: %d Items: %d/10", p.Name, p.Gold, inventoryCount)) goldLine += goldStyle.Render(fmt.Sprintf(" %s — 골드: %d 아이템: %d/10", p.Name, p.Gold, inventoryCount))
goldLine += "\n" goldLine += "\n"
} }
items := "" items := ""
for i, item := range state.ShopItems { for i, item := range state.ShopItems {
label := itemTypeLabel(item) label := itemTypeLabel(item)
items += fmt.Sprintf(" [%d] %s %s — %d gold\n", i+1, item.Name, label, item.Price) items += fmt.Sprintf(" [%d] %s %s — %d 골드\n", i+1, item.Name, label, item.Price)
} }
menu := "[1-3] Buy [Q] Leave Shop" menu := "[1-3] 구매 [Q] 상점 나가기"
parts := []string{header, "", goldLine, items, "", menu} parts := []string{header, "", goldLine, items, "", menu}
if shopMsg != "" { if shopMsg != "" {

View File

@@ -33,22 +33,22 @@ func (s *StatsScreen) View(ctx *Context) string {
} }
func renderStats(playerName string, stats store.PlayerStats, width, height int) string { func renderStats(playerName string, stats store.PlayerStats, width, height int) string {
title := styleHeader.Render("── Player Statistics ──") title := styleHeader.Render("── 플레이어 통계 ──")
var content string var content string
content += stylePlayer.Render(fmt.Sprintf(" %s", playerName)) + "\n\n" content += stylePlayer.Render(fmt.Sprintf(" %s", playerName)) + "\n\n"
content += fmt.Sprintf(" Total Runs: %s\n", styleGold.Render(fmt.Sprintf("%d", stats.TotalRuns))) content += fmt.Sprintf(" 총 플레이: %s\n", styleGold.Render(fmt.Sprintf("%d", stats.TotalRuns)))
content += fmt.Sprintf(" Best Floor: %s\n", styleGold.Render(fmt.Sprintf("B%d", stats.BestFloor))) content += fmt.Sprintf(" 최고 층: %s\n", styleGold.Render(fmt.Sprintf("B%d", stats.BestFloor)))
content += fmt.Sprintf(" Total Gold: %s\n", styleGold.Render(fmt.Sprintf("%d", stats.TotalGold))) content += fmt.Sprintf(" 총 골드: %s\n", styleGold.Render(fmt.Sprintf("%d", stats.TotalGold)))
content += fmt.Sprintf(" Victories: %s\n", styleHeal.Render(fmt.Sprintf("%d", stats.Victories))) content += fmt.Sprintf(" 승리 횟수: %s\n", styleHeal.Render(fmt.Sprintf("%d", stats.Victories)))
winRate := 0.0 winRate := 0.0
if stats.TotalRuns > 0 { if stats.TotalRuns > 0 {
winRate = float64(stats.Victories) / float64(stats.TotalRuns) * 100 winRate = float64(stats.Victories) / float64(stats.TotalRuns) * 100
} }
content += fmt.Sprintf(" Win Rate: %s\n", styleSystem.Render(fmt.Sprintf("%.1f%%", winRate))) content += fmt.Sprintf(" 승률: %s\n", styleSystem.Render(fmt.Sprintf("%.1f%%", winRate)))
footer := styleSystem.Render("[S] Back") footer := styleSystem.Render("[S] 뒤로")
return lipgloss.Place(width, height, lipgloss.Center, lipgloss.Center, return lipgloss.Place(width, height, lipgloss.Center, lipgloss.Center,
lipgloss.JoinVertical(lipgloss.Center, title, "", content, "", footer)) lipgloss.JoinVertical(lipgloss.Center, title, "", content, "", footer))

View File

@@ -44,7 +44,7 @@ func (s *TitleScreen) Update(msg tea.Msg, ctx *Context) (Screen, tea.Cmd) {
gs := NewGameScreen() gs := NewGameScreen()
gs.gameState = ctx.Session.GetState() gs.gameState = ctx.Session.GetState()
ctx.Session.TouchActivity(ctx.Fingerprint) ctx.Session.TouchActivity(ctx.Fingerprint)
ctx.Session.SendChat("System", ctx.PlayerName+" reconnected!") ctx.Session.SendChat("System", ctx.PlayerName+" 재접속!")
return gs, gs.pollState() return gs, gs.pollState()
} }
} }
@@ -101,7 +101,7 @@ func renderTitle(width, height int) string {
subtitle := lipgloss.NewStyle(). subtitle := lipgloss.NewStyle().
Foreground(colorGray). Foreground(colorGray).
Render("⚔ A Cooperative Dungeon Crawler ⚔") Render("⚔ 협동 던전 크롤러 ⚔")
server := lipgloss.NewStyle(). server := lipgloss.NewStyle().
Foreground(colorCyan). Foreground(colorCyan).
@@ -110,7 +110,7 @@ func renderTitle(width, height int) string {
menu := lipgloss.NewStyle(). menu := lipgloss.NewStyle().
Foreground(colorWhite). Foreground(colorWhite).
Bold(true). Bold(true).
Render("[Enter] Start [H] Help [S] Stats [A] Achievements [L] Leaderboard [C] Codex [Q] Quit") Render("[Enter] 시작 [H] 도움말 [S] 통계 [A] 업적 [L] 리더보드 [C] 도감 [Q] 종료")
content := lipgloss.JoinVertical(lipgloss.Center, content := lipgloss.JoinVertical(lipgloss.Center,
logo, logo,

View File

@@ -83,7 +83,7 @@ func (s *WaitingScreen) View(ctx *Context) string {
notReadyStyle := lipgloss.NewStyle(). notReadyStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color("240")) Foreground(lipgloss.Color("240"))
header := headerStyle.Render(fmt.Sprintf("── Waiting Room [%s] ──", ctx.RoomCode)) header := headerStyle.Render(fmt.Sprintf("── 대기실 [%s] ──", ctx.RoomCode))
playerList := "" playerList := ""
if ctx.Lobby != nil { if ctx.Lobby != nil {
@@ -92,7 +92,7 @@ func (s *WaitingScreen) View(ctx *Context) string {
for _, p := range room.Players { for _, p := range room.Players {
status := notReadyStyle.Render("...") status := notReadyStyle.Render("...")
if p.Ready { if p.Ready {
status = readyStyle.Render("READY") status = readyStyle.Render("준비 완료")
} }
cls := p.Class cls := p.Class
if cls == "" { if cls == "" {
@@ -103,11 +103,11 @@ func (s *WaitingScreen) View(ctx *Context) string {
} }
} }
menu := "[Enter] Ready" menu := "[Enter] 준비"
if s.ready { if s.ready {
menu = "Waiting for other players..." menu = "다른 플레이어를 기다리는 중..."
} }
menu += " [Esc] Leave" menu += " [Esc] 나가기"
return lipgloss.JoinVertical(lipgloss.Left, return lipgloss.JoinVertical(lipgloss.Left,
header, header,