Localize Sishui gate and battle log tone

This commit is contained in:
2026-06-19 06:06:14 +09:00
parent 30ac2479c4
commit 0b599b39be
12 changed files with 314 additions and 190 deletions

View File

@@ -1,12 +1,12 @@
{
"id": "cao_cao_campaign",
"title": "Cao Cao Campaign",
"title": "조조전",
"start_scenario": "001_yellow_turbans",
"initial_joined_officers": ["cao_cao", "xiahou_dun"],
"chapters": [
{
"id": "rise_through_turmoil",
"title": "Rise Through Turmoil",
"title": "난세의 기치",
"start_scenario": "001_yellow_turbans",
"end_scenario": "009_xiapi_siege"
},
@@ -56,12 +56,12 @@
"scenarios": [
{
"id": "001_yellow_turbans",
"title": "Yingchuan Skirmish",
"title": "영천 소전",
"path": "res://data/scenarios/001_yellow_turbans.json"
},
{
"id": "002_sishui_gate",
"title": "Sishui Gate Vanguard",
"title": "사수관 선봉전",
"path": "res://data/scenarios/002_sishui_gate.json"
},
{

View File

@@ -1,9 +1,9 @@
{
"id": "002_sishui_gate",
"name": "Sishui Gate Vanguard",
"name": "사수관 선봉전",
"objectives": {
"victory": "Defeat Hua Xiong's vanguard.",
"defeat": "Cao Cao is defeated or the battle reaches Turn 9."
"victory": "화웅의 선봉대를 격파하라.",
"defeat": "조조가 퇴각하거나 제9턴에 이르면 패한다."
},
"conditions": {
"victory": { "type": "all_units_defeated", "team": "enemy", "after_event": "turn_2_warning" },
@@ -13,58 +13,60 @@
]
},
"briefing": {
"title": "Sishui Gate Vanguard",
"location": "Sishui Gate, 190 CE",
"title": "사수관 선봉전",
"location": "사수관, 초평 원년",
"lines": [
"The coalition gathers to challenge Dong Zhuo.",
"Cao Cao leads a small vanguard to test Hua Xiong's defenses."
"동탁을 토벌하기 위해 제후의 군기가 사수관 아래 모였다.",
"조조는 작은 선봉을 이끌고 화웅의 진을 먼저 시험하려 한다."
],
"camp_conversations": [
{
"id": "sishui_coalition_reading",
"group": "officer",
"officer_id": "cao_cao",
"label": "Cao Cao - Coalition Nerve",
"label": "조조 - 연합의 담력",
"speaker": "Cao Cao",
"summary": "Measure the coalition before challenging Hua Xiong.",
"display_speaker": "조조",
"summary": "화웅을 치기 전에 제후 연합의 속내를 헤아린다.",
"lines": [
{ "speaker": "Cao Cao", "side": "left", "text": "The coalition has banners enough to shade the pass, yet every lord waits for another to move first." },
{ "speaker": "Xiahou Dun", "side": "right", "text": "Then let them watch us move. Hua Xiong will learn which banner has teeth." }
{ "speaker": "Cao Cao", "display_speaker": "조조", "side": "left", "text": "제후의 깃발은 관문을 덮을 만큼 많으나, 누구도 먼저 나서려 하지 않는다." },
{ "speaker": "Xiahou Dun", "display_speaker": "하후돈", "side": "right", "text": "그렇다면 우리가 움직이는 모습을 보이겠습니다. 화웅도 어느 깃발에 이빨이 있는지 알게 될 것입니다." }
]
},
{
"id": "sishui_xiahou_yuan_ridge",
"group": "officer",
"officer_id": "xiahou_yuan",
"label": "Xiahou Yuan - Ridge Line",
"label": "하후연 - 산등성이",
"speaker": "Xiahou Yuan",
"summary": "Discuss the archers watching the road.",
"display_speaker": "하후연",
"summary": "길 위를 내려다보는 궁병 진을 논한다.",
"lines": [
{ "speaker": "Xiahou Yuan", "side": "right", "text": "Their archers favor the ridge above the road. If we let them settle, the vanguard will bleed before touching Hua Xiong." },
{ "speaker": "Cao Cao", "side": "left", "text": "Keep their eyes busy. A single opened flank will make the pass feel wider than it is." }
{ "speaker": "Xiahou Yuan", "display_speaker": "하후연", "side": "right", "text": "적 궁병은 길 위의 능선을 좋아합니다. 자리를 굳히게 두면 화웅에게 닿기도 전에 선봉이 피를 흘릴 것입니다." },
{ "speaker": "Cao Cao", "display_speaker": "조조", "side": "left", "text": "그 눈을 흔들어 두어라. 한쪽 옆구리만 열어도 좁은 관문이 넓어 보일 것이다." }
]
},
{
"id": "sishui_quartermaster_antidote",
"group": "topic",
"label": "Quartermaster's Bundle",
"summary": "Check the last useful supplies before the gate.",
"label": "군수관의 약낭",
"summary": "관문 앞에서 마지막 쓸 만한 보급을 확인한다.",
"lines": [
{ "speaker": "Coalition Quartermaster", "side": "left", "text": "My lord, the western stores are crowded with borrowed seals and empty promises. I found one antidote bundle that is actually useful." },
{ "speaker": "Cao Ren", "side": "right", "text": "Take it. A single clean cure can keep a line from folding." }
{ "speaker": "Coalition Quartermaster", "display_speaker": "연합 군수관", "side": "left", "text": "장군, 서쪽 보급고에는 빌린 인장과 빈 약속만 가득합니다. 그래도 쓸 만한 해독약 한 꾸러미를 찾았습니다." },
{ "speaker": "Cao Ren", "display_speaker": "조인", "side": "right", "text": "가져오게. 제대로 된 치료 한 번이 전열 하나를 무너지지 않게 한다." }
],
"effects": [
{ "type": "grant_item", "item_id": "antidote", "count": 1, "text": "The quartermaster adds an Antidote to the field supplies." }
{ "type": "grant_item", "item_id": "antidote", "count": 1, "text": "군수관이 해독약 한 꾸러미를 군수에 보탰다." }
]
}
]
},
"shop": {
"merchant": {
"name": "Coalition Merchant",
"name": "연합 상인",
"lines": [
"Every lord at Sishui wants credit, but only the first vanguard buys enough bandages.",
"I have robes, armor, bows, and spears. If Hua Xiong breaks through, nobody will ask where they came from."
"사수관의 제후들은 공을 탐하지만, 붕대를 넉넉히 사는 이는 늘 선봉뿐입니다.",
"도포와 갑옷, 활과 창을 갖췄습니다. 화웅이 뚫고 나오면 물건의 출처를 묻는 이는 없을 겁니다."
]
},
"items": [
@@ -134,7 +136,7 @@
},
{
"unit_id": "hua_xiong_vanguard",
"name": "Hua Xiong Vanguard",
"name": "화웅 선봉대",
"class_id": "cavalry",
"team": "enemy",
"level": 2,
@@ -143,7 +145,7 @@
},
{
"unit_id": "liang_spearman_1",
"name": "Liang Spearman",
"name": "양주 창병",
"class_id": "infantry",
"team": "enemy",
"level": 2,
@@ -152,7 +154,7 @@
},
{
"unit_id": "liang_spearman_2",
"name": "Liang Spearman",
"name": "양주 창병",
"class_id": "infantry",
"team": "enemy",
"level": 2,
@@ -161,7 +163,7 @@
},
{
"unit_id": "liang_archer",
"name": "Liang Archer",
"name": "양주 궁병",
"class_id": "archer",
"team": "enemy",
"level": 2,
@@ -175,7 +177,7 @@
"once": true,
"when": { "type": "battle_start" },
"actions": [
{ "type": "log", "text": "The coalition vanguard moves toward Sishui Gate." }
{ "type": "log", "text": "연합 선봉이 사수관을 향해 움직였다." }
]
},
{
@@ -186,8 +188,8 @@
{
"type": "dialogue",
"lines": [
{ "speaker": "Cao Cao", "text": "Hua Xiong's vanguard is testing the coalition's nerve. We cannot give them ground." },
{ "speaker": "Xiahou Yuan", "text": "Their archers hold the ridge. I will keep pressure on their flank." }
{ "speaker": "Cao Cao", "display_speaker": "조조", "text": "화웅의 선봉은 제후 연합의 담력을 시험하고 있다. 한 치도 땅을 내줄 수 없다." },
{ "speaker": "Xiahou Yuan", "display_speaker": "하후연", "text": "적 궁병이 능선을 잡았습니다. 제가 옆구리를 눌러 두겠습니다." }
]
}
]
@@ -197,18 +199,18 @@
"once": true,
"when": { "type": "turn_start", "team": "enemy", "turn": 2 },
"actions": [
{ "type": "log", "text": "Hua Xiong's men tighten their formation." },
{ "type": "log", "text": "화웅의 병사들이 진형을 조여 왔다." },
{
"type": "set_objective",
"victory": "Defeat Hua Xiong's vanguard and reinforcements.",
"defeat": "Cao Cao is defeated or the battle reaches Turn 9."
"victory": "화웅의 선봉대와 원군을 모두 격파하라.",
"defeat": "조조가 퇴각하거나 제9턴에 이르면 패한다."
},
{
"type": "spawn_deployments",
"deployments": [
{
"unit_id": "liang_reinforcement_1",
"name": "Liang Rider",
"name": "양주 기병",
"class_id": "cavalry",
"team": "enemy",
"level": 2,
@@ -217,7 +219,7 @@
},
{
"unit_id": "liang_reinforcement_2",
"name": "Liang Guard",
"name": "양주 호위병",
"class_id": "infantry",
"team": "enemy",
"level": 2,
@@ -232,15 +234,15 @@
"post_battle_choices": [
{
"id": "pursue_dong_zhuo",
"label": "Pursue Dong Zhuo",
"description": "Cao Cao presses the retreating army while Cao Ren secures Sishui.",
"label": "동탁 추격",
"description": "조조가 물러나는 적을 몰아붙이고 조인이 사수관을 지킨다.",
"leave_officers": ["cao_ren"],
"set_flags": { "pursued_dong_zhuo": true, "regrouped_after_sishui": false }
},
{
"id": "regroup_coalition",
"label": "Regroup",
"description": "The vanguard pauses to gather supplies.",
"label": "연합 재정비",
"description": "선봉이 잠시 멈추어 보급을 모은다.",
"set_flags": { "pursued_dong_zhuo": false, "regrouped_after_sishui": true }
}
],
@@ -249,7 +251,7 @@
"items": ["bean"]
},
"post_battle_dialogue": [
{ "speaker": "Xiahou Yuan", "text": "Sishui Gate is broken, but Dong Zhuo's main force is already moving west." },
{ "speaker": "Cao Cao", "text": "The coalition will argue while the enemy escapes. We must decide whether to pursue or regroup." }
{ "speaker": "Xiahou Yuan", "display_speaker": "하후연", "text": "사수관은 무너졌으나 동탁의 본대는 이미 서쪽으로 움직이고 있습니다." },
{ "speaker": "Cao Cao", "display_speaker": "조조", "text": "제후들은 말다툼을 벌이는 동안 적은 달아날 것이다. 추격할지, 재정비할지 우리가 정해야 한다." }
]
}

View File

@@ -735,12 +735,12 @@ func try_cast_selected_skill(skill_id: String, target_cell: Vector2i) -> bool:
if caster.get("acted", false):
return false
if _unit_has_action_lock(caster, "skill"):
_emit_log("%s cannot use tactics while sealed." % caster["name"])
_emit_log("%s는 봉인되어 책략을 펼치지 못한다." % caster["name"])
return false
if not _unit_has_skill(caster, skill_id):
return false
if int(caster.get("mp", 0)) < int(skill.get("mp_cost", 0)):
_emit_log("%s does not have enough MP." % caster["name"])
_emit_log("%s의 기력이 모자라다." % caster["name"])
return false
if not _skill_cells_from(caster, skill).has(target_cell):
return false
@@ -749,9 +749,9 @@ func try_cast_selected_skill(skill_id: String, target_cell: Vector2i) -> bool:
var targets := _skill_targets_for_cast(caster, skill_id, skill, target_cell)
if targets.is_empty():
if skill_kind == "heal":
_emit_log("%s has no injured targets in range." % skill.get("name", "Skill"))
_emit_log("%s의 범위 안에 다친 아군이 없다." % skill.get("name", "책략"))
else:
_emit_log("%s has no valid targets." % skill.get("name", "Skill"))
_emit_log("%s의 표적이 없다." % skill.get("name", "책략"))
return false
if skill_kind == "support" and not _skill_has_support_effects(skill):
return false
@@ -795,7 +795,7 @@ func try_use_selected_item_on_cell(item_id: String, target_cell: Vector2i) -> bo
if user.get("acted", false):
return false
if int(battle_inventory.get(item_id, 0)) <= 0:
_emit_log("No %s remains." % item.get("name", item_id))
_emit_log("%s은 이미 다하였다." % _item_display_name(item_id))
return false
if str(item.get("kind", "")) != "consumable":
return false
@@ -814,7 +814,7 @@ func try_use_selected_item_on_cell(item_id: String, target_cell: Vector2i) -> bo
user["acted"] = true
user["moved"] = true
selected_unit_id = ""
_emit_log("%s uses %s on %s." % [user["name"], item.get("name", item_id), target["name"]])
_emit_log("%s 사용: %s -> %s." % [user["name"], _item_display_name(item_id), target["name"]])
_check_battle_status()
_notify_changed()
return true
@@ -1126,9 +1126,9 @@ func _resolve_skill_support(caster: Dictionary, target: Dictionary, skill_id: St
"remaining_phases": max(1, int(effect.get("duration_turns", 1)))
})
target["status_effects"] = status_effects
_emit_log("%s casts %s on %s. %s" % [
_emit_log("%s 시전 %s -> %s. %s" % [
caster["name"],
skill.get("name", "Skill"),
skill.get("name", "책략"),
target["name"],
_format_support_effects(skill)
])
@@ -1159,25 +1159,25 @@ func _format_support_effects(skill: Dictionary) -> String:
continue
var effect_type := str(effect.get("type", ""))
var duration: int = maxi(1, int(effect.get("duration_turns", 1)))
var duration_text := "next phase" if duration == 1 else "%d phases" % duration
var duration_text := _support_duration_text(duration)
if effect_type == "stat_bonus":
var stat := str(effect.get("stat", ""))
var amount := int(effect.get("amount", 0))
if not _is_supported_status_stat(stat) or amount == 0:
continue
var sign := "+" if amount > 0 else ""
parts.append("%s %s%d until %s" % [stat.to_upper(), sign, amount, duration_text])
parts.append("%s %s%d, %s 지속" % [_status_stat_display_name(stat), sign, amount, duration_text])
elif effect_type == "damage_over_time":
var damage := int(effect.get("amount", 0))
if damage <= 0:
continue
var status_name := _status_display_name(str(effect.get("status", "poison")))
parts.append("%s -%d HP for %s" % [status_name, damage, duration_text])
parts.append("%s -%d 병력, %s 지속" % [status_name, damage, duration_text])
elif effect_type == "action_lock":
var status_name := _status_display_name(str(effect.get("status", "status")))
parts.append("%s %s for %s" % [status_name, _action_lock_display_name(str(effect.get("action", ""))), duration_text])
parts.append("%s %s, %s 지속" % [status_name, _action_lock_display_name(str(effect.get("action", ""))), duration_text])
if parts.is_empty():
return "No support effect."
return "효험 없음"
return _join_strings(parts, ", ")
@@ -1193,15 +1193,15 @@ func _format_support_popup(skill: Dictionary) -> String:
if not _is_supported_status_stat(stat) or amount == 0:
continue
var sign := "+" if amount > 0 else ""
parts.append("%s %s%d" % [stat.to_upper(), sign, amount])
parts.append("%s %s%d" % [_status_stat_display_name(stat), sign, amount])
elif effect_type == "damage_over_time":
var damage := int(effect.get("amount", 0))
if damage > 0:
parts.append(_status_display_name(str(effect.get("status", "poison"))).to_upper())
parts.append(_status_display_name(str(effect.get("status", "poison"))))
elif effect_type == "action_lock":
parts.append(_status_display_name(str(effect.get("status", "status"))).to_upper())
parts.append(_status_display_name(str(effect.get("status", "status"))))
if parts.is_empty():
return "Support"
return "지원"
return _join_strings(parts, ", ")
@@ -1232,7 +1232,7 @@ func _format_active_status_effects(unit: Dictionary) -> String:
var duration_text := _format_status_remaining_phases(effect)
if effect_type == "damage_over_time":
if amount > 0:
parts.append("%s -%d HP %s" % [
parts.append("%s -%d 병력 %s" % [
_status_display_name(str(effect.get("status", "poison"))),
amount,
duration_text
@@ -1249,23 +1249,53 @@ func _format_active_status_effects(unit: Dictionary) -> String:
if not _is_supported_status_stat(stat) or amount == 0:
continue
var sign := "+" if amount > 0 else ""
parts.append("%s %s%d %s" % [stat.to_upper(), sign, amount, duration_text])
parts.append("%s %s%d %s" % [_status_stat_display_name(stat), sign, amount, duration_text])
return _join_strings(parts, ", ")
func _format_status_remaining_phases(effect: Dictionary) -> String:
var remaining: int = maxi(1, int(effect.get("remaining_phases", 1)))
var label := "phase" if remaining == 1 else "phases"
return "(%d %s)" % [remaining, label]
return "(%d회)" % remaining
func _status_display_name(status: String) -> String:
var normalized := status.strip_edges()
var normalized := status.strip_edges().to_lower()
if normalized.is_empty():
return "Status"
return "상태"
if normalized == "poison":
return ""
if normalized == "seal":
return "봉인"
if normalized == "snare":
return "발묶임"
if normalized == "disarm":
return "무장해제"
if normalized == "debuff":
return "약화"
return normalized.replace("_", " ").capitalize()
func _status_stat_display_name(stat: String) -> String:
var normalized := stat.strip_edges().to_lower()
if normalized == "atk":
return "무력"
if normalized == "def":
return "방비"
if normalized == "int":
return "지략"
if normalized == "agi":
return "순발"
if normalized == "move":
return "행군"
return normalized.to_upper()
func _support_duration_text(duration: int) -> String:
if duration <= 1:
return "다음 차례"
return "%d차례" % duration
func _is_supported_status_stat(stat: String) -> bool:
return stat == "atk" or stat == "def" or stat == "int" or stat == "agi" or stat == "move"
@@ -1282,12 +1312,12 @@ func _unit_has_action_lock(unit: Dictionary, action: String) -> bool:
func _action_lock_display_name(action: String) -> String:
if action == "skill":
return "no tactics"
return "책략 봉인"
if action == "move":
return "no movement"
return "행군 봉인"
if action == "attack":
return "no attacks"
return "no action"
return "타격 봉인"
return "행동 봉인"
func try_wait_selected() -> bool:
@@ -1300,7 +1330,7 @@ func try_wait_selected() -> bool:
unit["acted"] = true
unit["moved"] = true
selected_unit_id = ""
_emit_log("%s waits." % unit["name"])
_emit_log("%s 대기." % unit["name"])
_notify_changed()
return true
@@ -2290,12 +2320,12 @@ func _format_skill_threat_effect_summary(skill: Dictionary) -> String:
if not _is_supported_status_stat(stat) or amount == 0:
continue
var sign := "+" if amount > 0 else ""
parts.append("%s %s%d" % [stat.to_upper(), sign, amount])
parts.append("%s %s%d" % [_status_stat_display_name(stat), sign, amount])
elif effect_type == "damage_over_time":
var damage := int(effect.get("amount", 0))
if damage <= 0:
continue
parts.append("%s -%d HP" % [_status_display_name(str(effect.get("status", "poison"))), damage])
parts.append("%s -%d 병력" % [_status_display_name(str(effect.get("status", "poison"))), damage])
elif effect_type == "action_lock":
var status_name := _status_display_name(str(effect.get("status", "status")))
parts.append("%s %s" % [status_name, _action_lock_display_name(str(effect.get("action", "")))])
@@ -2518,28 +2548,28 @@ func _apply_item_effects(user: Dictionary, target: Dictionary, item: Dictionary)
var healed := _apply_item_heal(target, int(effect.get("amount", 0)))
if healed > 0:
applied = true
_emit_log("%s recovers %d HP." % [target["name"], healed])
_emit_combat_feedback(target, "+%d HP" % healed, "heal")
_emit_log("%s의 병력 %d 회복." % [target["name"], healed])
_emit_combat_feedback(target, "+%d 병력" % healed, "heal")
elif effect_type == "heal_mp":
var restored := _apply_item_mp_heal(target, int(effect.get("amount", 0)))
if restored > 0:
applied = true
_emit_log("%s recovers %d MP." % [target["name"], restored])
_emit_combat_feedback(target, "+%d MP" % restored, "mp")
_emit_log("%s의 기력 %d 회복." % [target["name"], restored])
_emit_combat_feedback(target, "+%d 기력" % restored, "mp")
elif effect_type == "cure_status":
var cured_statuses := _apply_item_status_cure(target, str(effect.get("status", "")))
if not cured_statuses.is_empty():
applied = true
_emit_log("%s is cured of %s." % [target["name"], _join_strings(cured_statuses, ", ")])
_emit_combat_feedback(target, "CURE", "support")
_emit_log("%s %s 해소." % [target["name"], _join_strings(cured_statuses, ", ")])
_emit_combat_feedback(target, "해소", "support")
elif effect_type == "cleanse_debuffs":
var cleansed_debuffs := _apply_item_debuff_cleanse(target)
if not cleansed_debuffs.is_empty():
applied = true
_emit_log("%s is cleansed of %s." % [target["name"], _join_strings(cleansed_debuffs, ", ")])
_emit_combat_feedback(target, "CLEANSE", "support")
_emit_log("%s의 약화 해소: %s." % [target["name"], _join_strings(cleansed_debuffs, ", ")])
_emit_combat_feedback(target, "정화", "support")
if not applied:
_emit_log("%s has no effect on %s." % [item.get("name", "Item"), target["name"]])
_emit_log("%s%s에게 효험이 없었다." % [str(item.get("name", "도구")), target["name"]])
return applied
@@ -2670,8 +2700,8 @@ func _is_negative_stat_bonus_effect(effect: Dictionary) -> bool:
func _stat_debuff_display_name(effect: Dictionary) -> String:
var stat := str(effect.get("stat", "stat"))
if not _is_supported_status_stat(stat):
return "Debuff"
return "%s down" % stat.to_upper()
return "약화"
return "%s 저하" % _status_stat_display_name(stat)
func _equipment_slot_for_item(item: Dictionary) -> String:
@@ -3184,7 +3214,7 @@ func _execute_ai_skill_action(caster: Dictionary, action: Dictionary) -> bool:
if _resolve_skill_damage(caster, area_target, skill):
defeated_count += 1
_grant_experience(caster, EXP_SKILL + defeated_count * EXP_DEFEAT_BONUS)
_emit_log("%s spends %d MP." % [caster["name"], int(skill.get("mp_cost", 0))])
_emit_log("%s 기력 %d 소모." % [caster["name"], int(skill.get("mp_cost", 0))])
return true
@@ -3230,7 +3260,7 @@ func _resolve_combat(attacker: Dictionary, target: Dictionary) -> void:
func _resolve_attack(attacker: Dictionary, target: Dictionary, is_counter := false) -> Dictionary:
var hit_chance := calculate_hit_chance(attacker, target)
var verb := "counterattacks" if is_counter else "attacks"
var verb := "반격" if is_counter else "공격"
unit_action_motion_requested.emit(
str(attacker.get("id", "")),
attacker.get("pos", Vector2i(-1, -1)),
@@ -3238,22 +3268,22 @@ func _resolve_attack(attacker: Dictionary, target: Dictionary, is_counter := fal
"counter" if is_counter else "attack"
)
if rng.randi_range(1, 100) > hit_chance:
_emit_log("%s %s %s but misses. Hit %d%%." % [attacker["name"], verb, target["name"], hit_chance])
_emit_combat_feedback(target, "MISS", "miss")
_emit_log("%s %s %s, 헛쳤다. 명중 %d%%." % [attacker["name"], verb, target["name"], hit_chance])
_emit_combat_feedback(target, "헛침", "miss")
return {"hit": false, "defeated": false}
var damage := calculate_damage(attacker, target)
target["hp"] = max(0, int(target["hp"]) - damage)
var effective_bonus := _weapon_effectiveness_bonus(attacker, target)
var effective_text := ""
if effective_bonus > 0:
effective_text = " Effective +%d vs %s." % [effective_bonus, _format_move_type(_weapon_effectiveness_target_type(target))]
_emit_log("%s %s %s for %d damage. Hit %d%%.%s" % [attacker["name"], verb, target["name"], damage, hit_chance, effective_text])
effective_text = " %s 특효 +%d." % [_format_move_type(_weapon_effectiveness_target_type(target)), effective_bonus]
_emit_log("%s %s %s, %d 피해. 명중 %d%%.%s" % [attacker["name"], verb, target["name"], damage, hit_chance, effective_text])
_emit_combat_feedback(target, "-%d" % damage, "damage")
if int(target["hp"]) <= 0:
target["alive"] = false
_emit_log("%s is defeated." % target["name"])
_emit_combat_feedback(target, "DOWN", "defeat")
_emit_log("%s 퇴각." % target["name"])
_emit_combat_feedback(target, "퇴각", "defeat")
_handle_unit_defeated(target)
return {"hit": true, "defeated": true}
return {"hit": true, "defeated": false}
@@ -3262,9 +3292,9 @@ func _resolve_attack(attacker: Dictionary, target: Dictionary, is_counter := fal
func _resolve_skill_damage(caster: Dictionary, target: Dictionary, skill: Dictionary) -> bool:
var damage := calculate_skill_damage(caster, target, skill)
target["hp"] = max(0, int(target["hp"]) - damage)
_emit_log("%s casts %s on %s for %d damage." % [
_emit_log("%s 시전 %s -> %s, %d 피해." % [
caster["name"],
skill.get("name", "Skill"),
skill.get("name", "책략"),
target["name"],
damage
])
@@ -3272,8 +3302,8 @@ func _resolve_skill_damage(caster: Dictionary, target: Dictionary, skill: Dictio
if int(target["hp"]) <= 0:
target["alive"] = false
_emit_log("%s is defeated." % target["name"])
_emit_combat_feedback(target, "DOWN", "defeat")
_emit_log("%s 퇴각." % target["name"])
_emit_combat_feedback(target, "퇴각", "defeat")
_handle_unit_defeated(target)
return true
return false
@@ -3299,13 +3329,13 @@ func _handle_unit_defeated(unit: Dictionary) -> void:
func _resolve_skill_heal(caster: Dictionary, target: Dictionary, skill: Dictionary) -> int:
var healed := calculate_skill_heal(caster, target, skill)
target["hp"] = min(int(target["max_hp"]), int(target["hp"]) + healed)
_emit_log("%s casts %s on %s, restoring %d HP." % [
_emit_log("%s 시전 %s -> %s, 병력 %d 회복." % [
caster["name"],
skill.get("name", "Skill"),
skill.get("name", "책략"),
target["name"],
healed
])
_emit_combat_feedback(target, "+%d HP" % healed, "heal")
_emit_combat_feedback(target, "+%d 병력" % healed, "heal")
return healed
@@ -3313,7 +3343,7 @@ func _grant_experience(unit: Dictionary, amount: int) -> void:
if unit.is_empty() or not unit.get("alive", false) or amount <= 0:
return
unit["exp"] = int(unit.get("exp", 0)) + amount
_emit_log("%s gains %d EXP." % [unit["name"], amount])
_emit_log("%s 공적 +%d." % [unit["name"], amount])
while int(unit["exp"]) >= EXP_LEVEL:
unit["exp"] = int(unit["exp"]) - EXP_LEVEL
_level_up(unit)
@@ -3330,7 +3360,7 @@ func _level_up(unit: Dictionary) -> void:
unit["mp"] = min(int(unit["max_mp"]), int(unit.get("mp", 0)) + mp_gain)
for stat in ["atk", "def", "int", "agi"]:
unit[stat] = int(unit.get(stat, 0)) + _growth_gain(unit, stat, 1)
_emit_log("%s reaches Lv.%d." % [unit["name"], unit["level"]])
_emit_log("%s Lv.%d에 이르렀다." % [unit["name"], unit["level"]])
_emit_combat_feedback(unit, "Lv.%d" % int(unit["level"]), "progress")
if str(unit.get("team", "")) == TEAM_PLAYER:
_record_progression_event({
@@ -3414,12 +3444,12 @@ func _check_battle_status() -> void:
battle_status = STATUS_DEFEAT
current_team = TEAM_PLAYER
selected_unit_id = ""
_emit_log("Defeat condition met.")
_emit_log("패전의 조짐이 굳어졌다.")
elif _is_condition_group_met(battle_conditions.get("victory", {})):
battle_status = STATUS_VICTORY
current_team = TEAM_PLAYER
selected_unit_id = ""
_emit_log("Victory condition met.")
_emit_log("승전의 군령이 이루어졌다.")
func _is_condition_group_met(condition_group) -> bool:
@@ -4051,9 +4081,9 @@ func _grant_event_item(action: Dictionary, trigger_unit: Dictionary = {}) -> boo
return false
var count: int = maxi(1, int(action.get("count", 1)))
battle_inventory[item_id] = int(battle_inventory.get(item_id, 0)) + count
var item_name: String = str(item.get("name", item_id))
var item_name: String = _item_display_name(item_id)
var count_text: String = "" if count == 1 else " x%d" % count
_emit_log("Received %s%s." % [item_name, count_text])
_emit_log("전리품: %s%s." % [item_name, count_text])
if not trigger_unit.is_empty() and bool(trigger_unit.get("alive", false)):
_emit_combat_feedback(trigger_unit, "+%s" % item_name, "item")
return true
@@ -4067,7 +4097,7 @@ func _grant_event_gold(action: Dictionary, trigger_unit: Dictionary = {}) -> boo
battle_gold_reward += amount
var text := str(action.get("text", "")).strip_edges()
if text.is_empty():
text = "Received %d gold." % amount
text = "군자금: %dG." % amount
_emit_log(text)
if not trigger_unit.is_empty() and bool(trigger_unit.get("alive", false)):
_emit_combat_feedback(trigger_unit, "+%dG" % amount, "gold")
@@ -4230,11 +4260,11 @@ func _withdraw_event_unit(unit_id: String) -> bool:
return false
if selected_unit_id == normalized_id:
selected_unit_id = ""
_emit_combat_feedback(unit, "WITHDRAW", "fade")
_emit_combat_feedback(unit, "퇴각", "fade")
unit["deployed"] = false
unit["acted"] = true
unit["moved"] = true
_emit_log("%s withdraws from the battlefield." % unit.get("name", normalized_id))
_emit_log("%s 전장을 물러났다." % unit.get("name", normalized_id))
_notify_changed()
return true
@@ -4263,12 +4293,13 @@ func _apply_phase_status_effects_for_team(team: String) -> void:
continue
unit["hp"] = max(0, int(unit.get("hp", 0)) - damage)
var status := str(effect.get("status", "poison"))
_emit_log("%s suffers %d %s damage." % [unit["name"], damage, status.replace("_", " ")])
var status_name := _status_display_name(status)
_emit_log("%s, %s으로 병력 %d 손실." % [unit["name"], status_name, damage])
_emit_combat_feedback(unit, "-%d" % damage, status)
if int(unit.get("hp", 0)) <= 0:
unit["alive"] = false
_emit_log("%s is defeated by %s." % [unit["name"], status.replace("_", " ")])
_emit_combat_feedback(unit, "DOWN", "defeat")
_emit_log("%s, %s으로 퇴각." % [unit["name"], status_name])
_emit_combat_feedback(unit, "퇴각", "defeat")
_handle_unit_defeated(unit)
break
@@ -4288,13 +4319,13 @@ func _expire_status_effects_for_team(team: String) -> void:
kept_effect["remaining_phases"] = next_remaining
kept_effects.append(kept_effect)
else:
var effect_name := str(effect.get("name", "Support"))
var effect_name := str(effect.get("name", "지원"))
if not expired_names.has(effect_name):
expired_names.append(effect_name)
unit["status_effects"] = kept_effects
for expired_effect_name in expired_names:
_emit_log("%s's %s fades." % [unit["name"], expired_effect_name])
_emit_combat_feedback(unit, "FADES", "fade")
_emit_log("%s %s이 사라졌다." % [unit["name"], expired_effect_name])
_emit_combat_feedback(unit, "해제", "fade")
func _manhattan(a: Vector2i, b: Vector2i) -> int:
@@ -4316,6 +4347,47 @@ func _format_cells(cells: Array[Vector2i]) -> String:
return _join_strings(labels, ", ")
func _item_display_name(item_id: String) -> String:
match item_id:
"bronze_sword":
return "청동검"
"iron_sword":
return "철검"
"training_spear":
return "연습창"
"short_bow":
return "단궁"
"hand_axe":
return "손도끼"
"war_axe":
return "전부"
"war_drum":
return "전고"
"imperial_seal":
return "옥새"
"yitian_sword":
return "의천검"
"dragon_spear":
return "용담창"
"wind_chaser_bow":
return "추풍궁"
"black_tortoise_robe":
return "현무도포"
"bean":
return ""
"antidote":
return "해독약"
"panacea":
return "만능약"
"wine":
return ""
_:
var item := get_item_def(item_id)
if item.is_empty():
return item_id
return str(item.get("name", item_id))
func _join_strings(values: Array, delimiter: String) -> String:
var text := ""
for value in values:

View File

@@ -1645,25 +1645,25 @@ func _play_ui_cancel() -> void:
func _play_log_sfx(message: String) -> void:
if message.contains(" but misses."):
if message.contains(" but misses.") or message.contains("헛쳤다"):
_play_sfx("guard_block")
elif message.begins_with("Objective updated:") or message.begins_with("Defeat condition updated:"):
elif message.begins_with("Objective updated:") or message.begins_with("Defeat condition updated:") or message.begins_with("군령 갱신:") or message.begins_with("패전 군령 갱신:"):
_play_ui_confirm()
elif message.ends_with(" withdraws from the battlefield."):
elif message.ends_with(" withdraws from the battlefield.") or message.ends_with("전장을 물러났다."):
_play_ui_confirm()
elif message.contains(" attacks ") or message.contains(" counterattacks "):
if message.contains(" for "):
elif message.contains(" attacks ") or message.contains(" counterattacks ") or message.contains(" 공격 ") or message.contains(" 반격 "):
if message.contains(" for ") or message.contains("피해"):
_play_sfx("hit_heavy")
elif message.contains(" casts "):
if message.contains("restoring") or message.contains(" until "):
elif message.contains(" casts ") or message.contains(" 시전 "):
if message.contains("restoring") or message.contains(" until ") or message.contains("회복") or message.contains("지원"):
_play_sfx("skill_cast")
else:
_play_sfx("fire_skill")
elif message.contains(" moved to ") or message.contains(" advances to ") or message.contains(" takes formation at ") or message.contains(" arrives at "):
_play_sfx("footstep")
elif message.contains(" uses ") or message.contains(" equips ") or message.contains(" 매입:") or message.contains(" 매각:") or message.begins_with("Received "):
elif message.contains(" uses ") or message.contains(" equips ") or message.contains(" 사용:") or message.contains(" 장착:") or message.contains(" 매입:") or message.contains(" 매각:") or message.begins_with("Received ") or message.begins_with("전리품:") or message.begins_with("군자금:"):
_play_sfx("item_pickup")
elif message.contains(" reaches Lv.") or message.contains(" promotes to "):
elif message.contains(" reaches Lv.") or message.contains(" promotes to ") or message.contains("공적") or message.contains("승급"):
_play_sfx("menu_confirm")
@@ -3056,9 +3056,9 @@ func _skill_target_preview_badge_for_cell(selected: Dictionary, target: Dictiona
return _make_target_preview_badge_at("가득", "invalid", cell)
if bool(preview.get("has_area", false)):
return _make_target_preview_badge_at("+%d x%d" % [total_heal, target_count], "heal", cell)
return _make_target_preview_badge_at("+%d HP" % heal, "heal", cell)
return _make_target_preview_badge_at("+%d 병력" % heal, "heal", cell)
if kind == "support":
var effect_text := str(preview.get("effect_text", "Support"))
var effect_text := str(preview.get("effect_text", "지원"))
var badge_kind := _support_preview_badge_kind(effect_text)
if bool(preview.get("has_area", false)):
return _make_target_preview_badge_at("%s x%d" % [_compact_support_preview_text(effect_text), target_count], badge_kind, cell)
@@ -3099,8 +3099,8 @@ func _item_target_preview_badge_for_cell(selected: Dictionary, target: Dictionar
if hp_heal > 0 and mp_heal > 0:
return _make_target_preview_badge_at("+%d/%d" % [hp_heal, mp_heal], "heal", cell)
if hp_heal > 0:
return _make_target_preview_badge_at("+%d HP" % hp_heal, "heal", cell)
return _make_target_preview_badge_at("+%d MP" % mp_heal, "mp", cell)
return _make_target_preview_badge_at("+%d 병력" % hp_heal, "heal", cell)
return _make_target_preview_badge_at("+%d 기력" % mp_heal, "mp", cell)
func _hover_intent_preview_badge(selected: Dictionary, target: Dictionary) -> Dictionary:
@@ -3145,7 +3145,10 @@ func _compact_support_preview_text(effect_text: String) -> String:
var for_index := part.find(" for ")
if for_index >= 0:
part = part.substr(0, for_index)
if not part.is_empty() and part != "No support effect.":
var duration_index := part.find(", ")
if duration_index >= 0:
part = part.substr(0, duration_index)
if not part.is_empty() and part != "No support effect." and part != "효험 없음":
parts.append(_compact_support_badge_text(part))
if parts.is_empty():
return "지원"
@@ -3155,27 +3158,27 @@ func _compact_support_preview_text(effect_text: String) -> String:
func _compact_support_badge_text(text: String) -> String:
if text.contains("Disarm"):
if text.contains("Disarm") or text.contains("무장해제"):
return "무장해제"
if text.contains("Snare"):
if text.contains("Snare") or text.contains("발묶임"):
return "속박"
if text.contains("Seal"):
if text.contains("Seal") or text.contains("봉인"):
return "봉인"
if text.contains("Poison"):
if text.contains("Poison") or text.contains(""):
return ""
if text.contains("Cleanse"):
if text.contains("Cleanse") or text.contains("해소"):
return "해소"
return text
func _support_preview_badge_kind(effect_text: String) -> String:
if effect_text.contains("Disarm"):
if effect_text.contains("Disarm") or effect_text.contains("무장해제"):
return "disarm"
if effect_text.contains("Snare"):
if effect_text.contains("Snare") or effect_text.contains("발묶임"):
return "snare"
if effect_text.contains("Seal"):
if effect_text.contains("Seal") or effect_text.contains("봉인"):
return "seal"
if effect_text.contains("Poison"):
if effect_text.contains("Poison") or effect_text.contains(""):
return "poison"
if effect_text.contains("-"):
return "debuff"
@@ -3805,7 +3808,7 @@ func _update_skill_forecast(selected: Dictionary) -> void:
preview["mp_cost"],
mp_text,
area_text,
preview.get("effect_text", "Support effect"),
preview.get("effect_text", "지원"),
refresh_text
]
else:
@@ -4617,7 +4620,7 @@ func _load_pending_post_battle_choice() -> void:
if briefing_panel != null:
briefing_panel.visible = false
if log_box != null:
log_box.append_text("Campaign choice pending.\n")
log_box.append_text("군의 논의가 이어지고 있다.\n")
func _load_scenario(scenario_id: String) -> void:
@@ -6426,10 +6429,10 @@ func _on_post_battle_choice_pressed(choice: Dictionary) -> void:
_append_result_items(choice.get("items", []))
_append_unique_result_values("joined_officers", choice.get("join_officers", []))
_append_unique_result_values("left_officers", choice.get("leave_officers", []))
_on_log_added("Campaign choice saved: %s." % battle_result_summary["choice_label"])
_on_log_added("군령 채택: %s." % battle_result_summary["choice_label"])
else:
_play_ui_cancel()
_on_log_added("Could not save campaign choice.")
_on_log_added("군령을 봉하지 못했다.")
_rebuild_result_choices()
_update_result_next_button_visibility()
if result_label != null and state.battle_status == BattleState.STATUS_VICTORY:
@@ -6825,7 +6828,7 @@ func _show_campaign_complete() -> void:
if briefing_panel != null:
briefing_panel.visible = false
if log_box != null:
log_box.append_text("Campaign complete.\n")
log_box.append_text("전기가 여기서 매듭지어졌다.\n")
func _on_new_campaign_pressed() -> void:
@@ -7109,7 +7112,7 @@ func _format_post_move_tactic_button_text(skill_id: String, skill: Dictionary, s
func _format_post_move_item_button_text(item_id: String, item: Dictionary, count: int) -> String:
var marker := "> " if selected_item_id == item_id else ""
var item_name := str(item.get("name", item_id))
var item_name := _item_display_name(item_id)
return "%s%s x%d %s" % [marker, item_name, count, _format_post_move_item_effect_text(item)]
@@ -7545,13 +7548,13 @@ func _format_tactic_effect_text(skill: Dictionary) -> String:
if amount == 0:
continue
var sign := "+" if amount > 0 else ""
parts.append("%s %s%d" % [str(effect.get("stat", "")).to_upper(), sign, amount])
parts.append("%s %s%d" % [_battle_stat_display_name(str(effect.get("stat", ""))), sign, amount])
elif effect_type == "damage_over_time" and amount > 0:
var status_name := str(effect.get("status", "status")).replace("_", " ").capitalize()
var status_name := _battle_status_display_name(str(effect.get("status", "status")))
parts.append("%s -%d" % [status_name, amount])
elif effect_type == "action_lock":
parts.append("%s %s" % [
str(effect.get("status", "status")).replace("_", " ").capitalize(),
_battle_status_display_name(str(effect.get("status", "status"))),
_action_lock_effect_label(str(effect.get("action", "")))
])
if not parts.is_empty():
@@ -7592,6 +7595,42 @@ func _action_lock_effect_label(action: String) -> String:
return "군령 봉인"
func _battle_stat_display_name(stat: String) -> String:
var normalized := stat.strip_edges().to_lower()
if normalized == "hp":
return "병력"
if normalized == "mp":
return "기력"
if normalized == "atk":
return "무력"
if normalized == "def":
return "방비"
if normalized == "int":
return "지략"
if normalized == "agi":
return "순발"
if normalized == "move":
return "행군"
return _format_identifier_label(stat)
func _battle_status_display_name(status: String) -> String:
var normalized := status.strip_edges().to_lower()
if normalized == "poison":
return ""
if normalized == "seal":
return "봉인"
if normalized == "snare":
return "발묶임"
if normalized == "disarm":
return "무장해제"
if normalized == "debuff":
return "약화"
if normalized.is_empty():
return "상태"
return _format_identifier_label(status)
func _skill_kind_text(kind: String) -> String:
var normalized := kind.strip_edges().to_lower()
if normalized == "damage":
@@ -7714,7 +7753,7 @@ func _clear_item_list() -> void:
func _format_item_button_text(item_id: String, item: Dictionary, count: int) -> String:
var marker := "> " if selected_item_id == item_id else ""
var item_name := str(item.get("name", item_id))
var item_name := _item_display_name(item_id)
return "%s%s x%d %s" % [marker, item_name, count, _format_item_effect_text(item)]
@@ -7728,7 +7767,7 @@ func _format_item_effect_text(item: Dictionary) -> String:
elif str(effect.get("type", "")) == "heal_mp":
parts.append("기력 +%d" % int(effect.get("amount", 0)))
elif str(effect.get("type", "")) == "cure_status":
parts.append("치료 %s" % str(effect.get("status", "status")).replace("_", " ").capitalize())
parts.append("치료 %s" % _battle_status_display_name(str(effect.get("status", "status"))))
elif str(effect.get("type", "")) == "cleanse_debuffs":
parts.append("약화 해소")
if parts.is_empty():
@@ -7872,9 +7911,9 @@ func _format_unit_equipment(unit: Dictionary) -> String:
for slot in ["weapon", "armor", "accessory"]:
var item_id := _equipment_item_id(equipment, slot)
if item_id.is_empty():
parts.append("%s -" % slot.capitalize())
parts.append("%s -" % _equipment_slot_display_name(slot))
else:
parts.append("%s %s" % [slot.capitalize(), _item_display_name(item_id)])
parts.append("%s %s" % [_equipment_slot_display_name(slot), _item_display_name(item_id)])
return _join_strings(parts, ", ")
@@ -7911,7 +7950,7 @@ func _format_equipment_option_text(unit: Dictionary, item_id: String, item: Dict
func _format_equipment_unequip_text(unit: Dictionary, slot: String, item_id: String, item: Dictionary) -> String:
var parts := [
"%s 해제:" % slot.capitalize(),
"%s 해제:" % _equipment_slot_display_name(slot),
_item_display_name(item_id)
]
var change_text := _format_equipment_unequip_change_text(unit, slot, item)
@@ -7929,7 +7968,7 @@ func _format_equipment_unequip_change_text(unit: Dictionary, slot: String, item:
var delta := -int(current_bonuses.get(stat, 0))
if delta == 0:
continue
parts.append("%s %d" % [stat.to_upper(), delta])
parts.append("%s %d" % [_battle_stat_display_name(stat), delta])
if slot == "weapon":
var current_range := _format_equipment_item_range_text(item)
if not current_range.is_empty():
@@ -7960,7 +7999,7 @@ func _format_equipment_change_text(unit: Dictionary, item: Dictionary) -> String
if delta == 0:
continue
var sign := "+" if delta > 0 else ""
parts.append("%s %s%d" % [stat.to_upper(), sign, delta])
parts.append("%s %s%d" % [_battle_stat_display_name(stat), sign, delta])
var range_text := _format_equipment_range_change_text(current_item, item)
if not range_text.is_empty():
@@ -7980,6 +8019,17 @@ func _equipment_slot_for_display(item: Dictionary) -> String:
return ""
func _equipment_slot_display_name(slot: String) -> String:
var normalized := slot.strip_edges().to_lower()
if normalized == "weapon":
return "병장"
if normalized == "armor":
return "갑주"
if normalized == "accessory":
return "보물"
return _format_identifier_label(slot)
func _equipment_bonus_map(item: Dictionary) -> Dictionary:
if item.is_empty() or typeof(item.get("bonuses", {})) != TYPE_DICTIONARY:
return {}
@@ -7995,7 +8045,7 @@ func _format_equipment_range_change_text(current_item: Dictionary, candidate_ite
return ""
if current_range.is_empty():
current_range = "-"
return "RNG %s->%s" % [current_range, candidate_range]
return "사거리 %s->%s" % [current_range, candidate_range]
func _format_equipment_item_range_text(item: Dictionary) -> String:
@@ -8021,10 +8071,10 @@ func _format_equipment_effectiveness_change_text(current_item: Dictionary, candi
if candidate_text == current_text:
return ""
if current_text.is_empty():
return "Gains %s" % candidate_text
return "특효 획득 %s" % candidate_text
if candidate_text.is_empty():
return "Loses %s" % current_text
return "Effect changes"
return "특효 상실 %s" % current_text
return "특효 변화"
func _format_equipment_bonus_text(item: Dictionary) -> String:
@@ -8038,12 +8088,12 @@ func _format_equipment_bonus_text(item: Dictionary) -> String:
if amount == 0:
continue
var sign := "+" if amount > 0 else ""
parts.append("%s %s%d" % [stat.to_upper(), sign, amount])
parts.append("%s %s%d" % [_battle_stat_display_name(stat), sign, amount])
var effective_text := _format_equipment_effectiveness_text(item)
if not effective_text.is_empty():
parts.append(effective_text)
if parts.is_empty():
return str(item.get("kind", "equipment")).capitalize()
return _item_kind_text(str(item.get("kind", "item")))
return _join_strings(parts, ", ")
@@ -8054,7 +8104,7 @@ func _format_equipment_rarity_text(item: Dictionary) -> String:
func _format_item_rarity_text(item: Dictionary) -> String:
var rarity := str(item.get("rarity", ""))
if rarity == "named":
return "Named"
return "명품"
return ""
@@ -8074,7 +8124,7 @@ func _format_equipment_effectiveness_text(item: Dictionary) -> String:
labels.append(label)
if labels.is_empty():
return ""
return "Effective +%d vs %s" % [bonus, _join_strings(labels, "/")]
return "%s 특효 +%d" % [_join_strings(labels, "/"), bonus]
func _format_move_type(move_type: String) -> String:

View File

@@ -186,12 +186,12 @@ func _check_unequip_ui_entries(failures: Array[String]) -> void:
var selected: Dictionary = scene.state.get_unit("cao_cao")
scene._rebuild_equip_menu(selected)
if not _container_has_button_text(scene.equip_list, "Unequip Weapon:"):
if not _container_has_button_text(scene.equip_list, "병장 해제:"):
failures.append("battle equipment menu should include an Unequip Weapon button")
scene.armory_unit_id = "cao_cao"
scene._rebuild_armory_menu()
if not _container_has_button_text(scene.armory_list, "Unequip Weapon:"):
if not _container_has_button_text(scene.armory_list, "병장 해제:"):
failures.append("pre-battle Armory should include an Unequip Weapon button")
scene.free()

View File

@@ -48,13 +48,13 @@ func _check_wuchao_depot_pickup(failures: Array[String]) -> void:
failures.append("Wuchao depot should grant Wine x1: before %s after %s" % [str(before_inventory), str(after_inventory)])
if not logs.has("Usable Wuchao stores are seized before the granaries burn."):
failures.append("Wuchao depot pickup log missing")
if not logs.has("Received Bean x2."):
if not logs.has("전리품: 콩 x2."):
failures.append("Bean pickup receipt log missing")
if not logs.has("Received Wine."):
if not logs.has("전리품: 술."):
failures.append("Wine pickup receipt log missing")
if not feedback.has("cao_cao_ch13|+Bean|item"):
if not feedback.has("cao_cao_ch13|+|item"):
failures.append("Bean pickup feedback missing")
if not feedback.has("cao_cao_ch13|+Wine|item"):
if not feedback.has("cao_cao_ch13|+|item"):
failures.append("Wine pickup feedback missing")
if not state.fired_event_ids.has("wuchao_depot_burned"):
failures.append("Wuchao depot event should be marked fired")

View File

@@ -51,8 +51,8 @@ func _check_single_withdraw(state, failures: Array[String]) -> void:
failures.append("single withdraw unit should no longer occupy its map cell")
if state.get_living_units("enemy").size() != before_living - 1:
failures.append("single withdraw should remove one living enemy from battle counts")
_check_log_contains(failures, logs, "Smoke Raider withdraws from the battlefield.")
_check_feedback_contains(failures, feedback, "withdraw_smoke_enemy", "WITHDRAW", "fade")
_check_log_contains(failures, logs, "Smoke Raider 전장을 물러났다.")
_check_feedback_contains(failures, feedback, "withdraw_smoke_enemy", "퇴각", "fade")
func _check_multi_withdraw(state, failures: Array[String]) -> void:

View File

@@ -35,7 +35,7 @@ func _check_panacea_cleanses_debuffs(failures: Array[String]) -> void:
var preview: Dictionary = state.get_item_preview("cao_cao", "panacea", cao_cao["pos"])
var cure_names: Array = preview.get("cure_statuses", [])
for expected in ["Poison", "Seal", "ATK down", "MOVE down"]:
for expected in ["", "봉인", "무력 저하", "행군 저하"]:
if not cure_names.has(expected):
failures.append("Panacea preview should include %s in %s" % [expected, str(cure_names)])
@@ -58,15 +58,15 @@ func _check_panacea_cleanses_debuffs(failures: Array[String]) -> void:
if int(state.get_inventory_snapshot().get("panacea", 0)) != 0:
failures.append("Panacea should be consumed after cleansing")
var summary := state.get_status_effect_summary("cao_cao")
if not summary.contains("DEF +4"):
if not summary.contains("방비 +4"):
failures.append("Panacea should keep positive buffs: %s" % summary)
for removed_text in ["ATK -3", "MOVE -1", "Poison", "Seal"]:
for removed_text in ["무력 -3", "행군 -1", "", "봉인"]:
if summary.contains(removed_text):
failures.append("Panacea should remove %s from summary: %s" % [removed_text, summary])
_check_log_contains(failures, logs, "Cao Cao is cured of Poison.")
_check_log_contains(failures, logs, "Cao Cao is cured of Seal.")
_check_log_contains(failures, logs, "Cao Cao is cleansed of ATK down, MOVE down.")
if not feedback.has("cao_cao|CLEANSE|support"):
_check_log_contains(failures, logs, "Cao Cao의 독 해소.")
_check_log_contains(failures, logs, "Cao Cao의 봉인 해소.")
_check_log_contains(failures, logs, "Cao Cao의 약화 해소: 무력 저하, 행군 저하.")
if not feedback.has("cao_cao|정화|support"):
failures.append("Panacea cleanse feedback missing: %s" % str(feedback))
@@ -102,7 +102,7 @@ func _check_panacea_menu_text(failures: Array[String]) -> void:
scene.free()
return
var text := scene._format_item_effect_text(scene.state.get_item_def("panacea"))
if not text.contains("Cleanse debuffs"):
if not text.contains("약화 해소"):
failures.append("Panacea menu text should mention debuff cleanse: %s" % text)
scene.free()

View File

@@ -422,7 +422,7 @@ func _check_scene_post_move_item_picker_flow(failures: Array[String]) -> void:
if item_markers.size() != 1 or item_markers[0].get("cell", Vector2i.ZERO) != Vector2i(2, 4):
failures.append("item target markers should include only the Bean target: %s" % str(item_markers))
else:
if str(item_markers[0].get("text", "")) != "+20 HP":
if str(item_markers[0].get("text", "")) != "+20 병력":
failures.append("item target marker should preview healing: %s" % str(item_markers[0]))
if str(item_markers[0].get("kind", "")) != "heal":
failures.append("item target marker should use heal color: %s" % str(item_markers[0]))

View File

@@ -30,7 +30,7 @@ func _init() -> void:
{"type": "level_up", "name": "Cao Cao", "to_level": 2}
],
"next_scenario_id": "002_sishui_gate",
"next_scenario_title": "Sishui Gate",
"next_scenario_title": "사수관 선봉전",
"choice_applied": true,
"choice_label": ""
}
@@ -40,9 +40,9 @@ func _init() -> void:
_check_contains(failures, "item reward", result_text, "콩 x2")
_check_contains(failures, "roster section", result_text, "합류: 전위")
_check_contains(failures, "growth section", result_text, "Cao Cao Lv.2")
_check_contains(failures, "campaign section", result_text, "다음 행군: Sishui Gate")
_check_contains(failures, "campaign section", result_text, "다음 행군: 사수관 선봉전")
_check_contains(failures, "next camp section", result_text, "다음 군막")
_check_contains(failures, "next camp location", result_text, "Sishui Gate, 190 CE")
_check_contains(failures, "next camp location", result_text, "사수관, 초평 원년")
_check_contains(failures, "next camp talk", result_text, "군막 회의 · 3건")
_check_contains(failures, "next camp shop", result_text, "군상 물품 · 6종")
_check_contains(failures, "next camp deploy", result_text, "출진 명부 · 2/3")
@@ -58,7 +58,7 @@ func _init() -> void:
"joined_officers": [],
"left_officers": [],
"progression_events": [],
"next_scenario_title": "Sishui Gate",
"next_scenario_title": "사수관 선봉전",
"choice_applied": true,
"choice_label": ""
}

View File

@@ -45,9 +45,9 @@ func _check_status_summary_duration_text(failures: Array[String]) -> void:
}
]
var summary: String = state.get_status_effect_summary("cao_cao")
_check_contains(failures, "stat duration", summary, "ATK +3 (2 phases)")
_check_contains(failures, "poison duration", summary, "Poison -4 HP (1 phase)")
_check_contains(failures, "seal duration", summary, "Seal no tactics (3 phases)")
_check_contains(failures, "stat duration", summary, "무력 +3 (2)")
_check_contains(failures, "poison duration", summary, " -4 병력 (1)")
_check_contains(failures, "seal duration", summary, "봉인 책략 봉인 (3회)")
func _check_hud_reuses_status_summary(failures: Array[String]) -> void:
@@ -66,8 +66,8 @@ func _check_hud_reuses_status_summary(failures: Array[String]) -> void:
scene.state.selected_unit_id = "cao_cao"
scene.hover_cell = Vector2i(1, 3)
scene._update_hud()
_check_contains(failures, "selected HUD status", scene.selected_label.text, "상태: DEF -2 (2 phases)")
_check_contains(failures, "hover tile status", scene.cell_info_label.text, "상태: DEF -2 (2 phases)")
_check_contains(failures, "selected HUD status", scene.selected_label.text, "상태: 방비 -2 (2)")
_check_contains(failures, "hover tile status", scene.cell_info_label.text, "상태: 방비 -2 (2)")
scene.free()

View File

@@ -133,7 +133,7 @@ func _check_battle_visual_data(failures: Array[String]) -> void:
_check_camp_conversation(failures, gate_conversations[2], "sishui_quartermaster_antidote", "topic", "")
_check_camp_conversation_effect(failures, gate_conversations[2], "antidote", 1)
var gate_merchant := gate_state.get_shop_merchant()
if str(gate_merchant.get("name", "")) != "Coalition Merchant":
if str(gate_merchant.get("name", "")) != "연합 상인":
failures.append("002 shop should expose coalition merchant name")
if (gate_merchant.get("lines", []) as Array).size() < 2:
failures.append("002 shop should expose merchant flavor lines")