Antique global battle language

This commit is contained in:
2026-06-19 08:44:18 +09:00
parent c98064c528
commit b73fa6adbf
17 changed files with 317 additions and 267 deletions

View File

@@ -46,35 +46,35 @@ const GROWTH_GRADE_GAINS := {
const DEFAULT_TERRAIN_DEFS := {
"G": {
"name": "Plain",
"name": "평원",
"move_cost": 1,
"defense": 0,
"avoid": 0,
"color": Color(0.43, 0.62, 0.31)
},
"F": {
"name": "Forest",
"name": "",
"move_cost": 2,
"defense": 2,
"avoid": 10,
"color": Color(0.20, 0.45, 0.23)
},
"H": {
"name": "Hill",
"name": "산지",
"move_cost": 2,
"defense": 1,
"avoid": 5,
"color": Color(0.55, 0.50, 0.34)
},
"R": {
"name": "Road",
"name": "관도",
"move_cost": 1,
"defense": 0,
"avoid": 0,
"color": Color(0.64, 0.55, 0.42)
},
"W": {
"name": "Water",
"name": "하천",
"move_cost": 99,
"defense": 0,
"avoid": 0,
@@ -114,7 +114,7 @@ var data_catalog := DataCatalogScript.new()
var rng := RandomNumberGenerator.new()
func load_battle(path: String, roster_overrides := {}, inventory_overrides := {}, flag_overrides := {}, joined_officer_overrides := []) -> bool:
func load_battle(path: String, roster_overrides := {}, inventory_overrides := {}, flag_overrides := {}, joined_officer_overrides := [], display_name_override := "") -> bool:
var text := FileAccess.get_file_as_string(path)
if text.is_empty():
push_error("Battle file is empty or missing: %s" % path)
@@ -142,7 +142,10 @@ func load_battle(path: String, roster_overrides := {}, inventory_overrides := {}
campaign_joined_officers = _joined_officer_lookup(joined_officer_overrides)
terrain_defs = data_catalog.get_runtime_terrain_defs(DEFAULT_TERRAIN_DEFS)
_apply_battle_data(parsed, roster_overrides_snapshot)
_emit_log("Battle loaded: %s" % battle_name)
var display_name := str(display_name_override).strip_edges()
if not display_name.is_empty():
battle_name = display_name
_emit_log("전장도 펼침: %s" % battle_name)
_run_events("battle_start")
_run_events("turn_start", current_team, turn_number)
_notify_changed()
@@ -627,11 +630,11 @@ func select_unit(unit_id: String) -> bool:
if unit.get("team", "") != current_team:
return false
if unit.get("acted", false):
_emit_log("%s has already acted." % unit["name"])
_emit_log("%s는 이미 군령을 마쳤다." % unit["name"])
return false
selected_unit_id = unit_id
_emit_log("Selected %s." % unit["name"])
_emit_log("%s을 지목하였다." % unit["name"])
_notify_changed()
return true
@@ -650,7 +653,7 @@ func try_move_selected(cell: Vector2i, defer_reach_events := false) -> bool:
if unit.get("moved", false) or unit.get("acted", false):
return false
if _unit_has_action_lock(unit, "move"):
_emit_log("%s cannot move while snared." % unit["name"])
_emit_log("%s는 발이 묶여 행군하지 못한다." % unit["name"])
return false
if not get_movement_range(unit["id"]).has(cell):
return false
@@ -661,7 +664,7 @@ func try_move_selected(cell: Vector2i, defer_reach_events := false) -> bool:
unit["pos"] = cell
unit["moved"] = true
unit_motion_requested.emit(str(unit.get("id", "")), from_cell, cell)
_emit_log("%s moved to %s." % [unit["name"], _format_cell(cell)])
_emit_log("%s, %s으로 진군." % [unit["name"], _format_cell(cell)])
if not defer_reach_events:
_run_events("unit_reaches_tile", str(unit.get("team", "")), turn_number, unit)
_check_battle_status()
@@ -694,7 +697,7 @@ func cancel_pending_move(unit_id: String, from_cell: Vector2i, to_cell: Vector2i
unit["pos"] = from_cell
unit["moved"] = false
unit_motion_requested.emit(unit_id, to_cell, from_cell)
_emit_log("%s cancels movement." % unit["name"])
_emit_log("%s의 행군을 거두었다." % unit["name"])
_notify_changed()
return true
@@ -709,7 +712,7 @@ func try_attack_selected(target_id: String) -> bool:
if attacker.get("acted", false):
return false
if _unit_has_action_lock(attacker, "attack"):
_emit_log("%s cannot attack while disarmed." % attacker["name"])
_emit_log("%s는 병장을 빼앗겨 타격하지 못한다." % attacker["name"])
return false
if attacker.get("team", "") == target.get("team", ""):
return false
@@ -814,7 +817,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 사용: %s -> %s." % [user["name"], _item_display_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,7 +1129,7 @@ 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 시전 %s -> %s. %s" % [
_emit_log("%s, %s을 펼쳐 %s에게 %s." % [
caster["name"],
skill.get("name", "책략"),
target["name"],
@@ -1751,25 +1754,25 @@ func try_set_unit_deployed(unit_id: String, deployed: bool) -> bool:
if unit.is_empty() or unit.get("team", "") != TEAM_PLAYER:
return false
if bool(unit.get("required_deployment", false)) and not deployed:
_emit_log("%s must deploy." % unit["name"])
_emit_log("%s는 반드시 출진해야 한다." % unit["name"])
return false
if bool(unit.get("deployed", true)) == deployed:
return true
if deployed:
if get_deployed_player_count() >= get_deployment_max_units():
_emit_log("Deployment limit reached.")
_emit_log("출진 명부가 이미 찼다.")
return false
var target_cell := _formation_cell_for_deployment(unit)
if not is_inside(target_cell):
_emit_log("No open formation cell for %s." % unit["name"])
_emit_log("%s이 설 진형 자리가 없다." % unit["name"])
return false
unit["pos"] = target_cell
unit["alive"] = true
unit["deployed"] = true
_emit_log("%s joins the sortie." % unit["name"])
_emit_log("%s, 출진 명부에 들었다." % unit["name"])
else:
unit["deployed"] = false
_emit_log("%s moves to reserve." % unit["name"])
_emit_log("%s, 예비대로 물러났다." % unit["name"])
_notify_changed()
return true
@@ -1803,7 +1806,7 @@ func try_set_prebattle_formation(unit_id: String, cell: Vector2i) -> bool:
return true
occupant["pos"] = unit["pos"]
unit["pos"] = cell
_emit_log("%s takes formation at %s." % [unit["name"], _format_cell(cell)])
_emit_log("%s, %s에 진형을 잡았다." % [unit["name"], _format_cell(cell)])
_notify_changed()
return true
@@ -1831,23 +1834,23 @@ func try_equip_item(unit_id: String, item_id: String) -> bool:
if not bool(unit.get("controllable", true)):
return false
if unit.get("moved", false) or unit.get("acted", false):
_emit_log("%s can change equipment before moving or acting." % unit["name"])
_emit_log("%s는 행군이나 군령 전에만 병장을 고칠 수 있다." % unit["name"])
return false
var slot := _equipment_slot_for_item(item)
if slot.is_empty():
return false
if int(battle_inventory.get(item_id, 0)) <= 0:
_emit_log("No %s is in inventory." % item.get("name", item_id))
_emit_log("병장고에 %s이 없다." % item.get("name", item_id))
return false
if not _unit_can_equip_item(unit, item):
_emit_log("%s cannot equip %s." % [unit["name"], item.get("name", item_id)])
_emit_log("%s%s을 들 수 없다." % [unit["name"], item.get("name", item_id)])
return false
var equipment: Dictionary = unit.get("equipment", {}).duplicate(true)
var old_item_id := _equipment_item_id_for_slot(equipment, slot)
if old_item_id == item_id:
_emit_log("%s already has %s equipped." % [unit["name"], item.get("name", item_id)])
_emit_log("%s는 이미 %s을 지녔다." % [unit["name"], item.get("name", item_id)])
return false
battle_inventory[item_id] = max(0, int(battle_inventory.get(item_id, 0)) - 1)
@@ -1859,7 +1862,7 @@ func try_equip_item(unit_id: String, item_id: String) -> bool:
unit["equipment"] = equipment
_apply_equipment_stat_delta(unit, old_item, item)
_refresh_attack_range_from_equipment(unit)
_emit_log("%s equips %s." % [unit["name"], item.get("name", item_id)])
_emit_log("%s, %s을 장착." % [unit["name"], item.get("name", item_id)])
_notify_changed()
return true
@@ -1873,7 +1876,7 @@ func try_unequip_item(unit_id: String, slot: String) -> bool:
if not bool(unit.get("controllable", true)):
return false
if unit.get("moved", false) or unit.get("acted", false):
_emit_log("%s can change equipment before moving or acting." % unit["name"])
_emit_log("%s는 행군이나 군령 전에만 병장을 고칠 수 있다." % unit["name"])
return false
var normalized_slot := _normalized_equipment_slot(slot)
@@ -1883,7 +1886,7 @@ func try_unequip_item(unit_id: String, slot: String) -> bool:
var equipment: Dictionary = unit.get("equipment", {}).duplicate(true)
var old_item_id := _equipment_item_id_for_slot(equipment, normalized_slot)
if old_item_id.is_empty():
_emit_log("%s has no %s equipped." % [unit["name"], normalized_slot])
_emit_log("%s%s에 찬 병장이 없다." % [unit["name"], _equipment_slot_display_name(normalized_slot)])
return false
var old_item := get_item_def(old_item_id)
@@ -1896,7 +1899,7 @@ func try_unequip_item(unit_id: String, slot: String) -> bool:
_apply_equipment_stat_delta(unit, old_item, {})
_refresh_attack_range_from_equipment(unit)
battle_inventory[old_item_id] = int(battle_inventory.get(old_item_id, 0)) + 1
_emit_log("%s unequips %s." % [unit["name"], old_item.get("name", old_item_id)])
_emit_log("%s, %s을 해제." % [unit["name"], old_item.get("name", old_item_id)])
_notify_changed()
return true
@@ -1996,7 +1999,7 @@ func get_map_background_path() -> String:
func get_terrain_name(cell: Vector2i) -> String:
return String(terrain_defs.get(get_terrain_key(cell), DEFAULT_TERRAIN_DEFS["G"]).get("name", "Plain"))
return String(terrain_defs.get(get_terrain_key(cell), DEFAULT_TERRAIN_DEFS["G"]).get("name", "평원"))
func get_terrain_color(cell: Vector2i) -> Color:
@@ -2105,8 +2108,8 @@ func get_status_text() -> String:
return "패전"
var turn_limit := get_turn_limit()
if turn_limit > 0:
return "%d/%d · %s" % [turn_number, turn_limit, _team_status_text(current_team)]
return "%d · %s" % [turn_number, _team_status_text(current_team)]
return "%d군령/%d · %s" % [turn_number, turn_limit, _team_status_text(current_team)]
return "%d군령 · %s" % [turn_number, _team_status_text(current_team)]
func _team_status_text(team: String) -> String:
@@ -2718,6 +2721,17 @@ func _normalized_equipment_slot(slot: String) -> String:
return ""
func _equipment_slot_display_name(slot: String) -> String:
var normalized := _normalized_equipment_slot(slot)
if normalized == "weapon":
return "무기"
if normalized == "armor":
return "갑옷"
if normalized == "accessory":
return "보물"
return "병장"
func _equipment_item_id_for_slot(equipment: Dictionary, slot: String) -> String:
var item_id = equipment.get(slot, "")
if item_id == null:
@@ -2872,7 +2886,7 @@ func _enemy_take_action(enemy: Dictionary) -> void:
enemy["pos"] = best_cell
enemy["moved"] = true
unit_motion_requested.emit(str(enemy.get("id", "")), from_cell, best_cell)
_emit_log("%s advances to %s." % [enemy["name"], _format_cell(best_cell)])
_emit_log("%s, %s으로 압박." % [enemy["name"], _format_cell(best_cell)])
_run_events("unit_reaches_tile", str(enemy.get("team", "")), turn_number, enemy)
if battle_status != STATUS_ACTIVE:
return
@@ -3292,7 +3306,7 @@ 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 시전 %s -> %s, %d 피해." % [
_emit_log("%s, %s을 펼쳐 %s에게 %d 피해." % [
caster["name"],
skill.get("name", "책략"),
target["name"],
@@ -3329,7 +3343,7 @@ 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 시전 %s -> %s, 병력 %d 회복." % [
_emit_log("%s, %s으로 %s 병력 %d 회복." % [
caster["name"],
skill.get("name", "책략"),
target["name"],
@@ -3360,8 +3374,8 @@ 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 Lv.%d에 이르렀다." % [unit["name"], unit["level"]])
_emit_combat_feedback(unit, "Lv.%d" % int(unit["level"]), "progress")
_emit_log("%s 품계 %d에 이르렀다." % [unit["name"], unit["level"]])
_emit_combat_feedback(unit, "품계 %d" % int(unit["level"]), "progress")
if str(unit.get("team", "")) == TEAM_PLAYER:
_record_progression_event({
"type": "level_up",
@@ -3396,8 +3410,8 @@ func _try_promote_unit(unit: Dictionary) -> void:
_apply_stat_bonus_delta(unit, class_def.get("base_bonus", {}), next_class_def.get("base_bonus", {}))
unit["class_id"] = next_class_id
_apply_class_runtime_fields(unit, next_class_id)
_emit_log("%s promotes to %s." % [unit["name"], unit["class"]])
_emit_combat_feedback(unit, "PROMOTE", "progress")
_emit_log("%s, %s으로 승급하였다." % [unit["name"], unit["class"]])
_emit_combat_feedback(unit, "승급", "progress")
if str(unit.get("team", "")) == TEAM_PLAYER:
_record_progression_event({
"type": "promotion",
@@ -4082,7 +4096,7 @@ func _grant_event_item(action: Dictionary, trigger_unit: Dictionary = {}) -> boo
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 = _item_display_name(item_id)
var count_text: String = "" if count == 1 else " x%d" % count
var count_text: String = "" if count == 1 else " %d" % count
_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")
@@ -4097,10 +4111,10 @@ 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 = "군자금: %dG." % amount
text = "군자금 %d냥을 거두었다." % 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")
_emit_combat_feedback(trigger_unit, "+%d" % amount, "gold")
return true
@@ -4226,28 +4240,28 @@ func _spawn_event_deployment(deployment) -> bool:
var hydrated_unit := data_catalog.hydrate_deployment(deployment)
_apply_roster_overlay(hydrated_unit, roster_overrides_snapshot)
if not _is_deployment_available(hydrated_unit):
_emit_log("Reinforcement %s is not available." % hydrated_unit.get("name", hydrated_unit.get("id", "unit")))
_emit_log("원군 %s은 아직 부를 수 없다." % hydrated_unit.get("name", hydrated_unit.get("id", "unit")))
return false
var unit := _prepare_unit(hydrated_unit)
if unit.is_empty():
return false
if not is_inside(unit["pos"]):
_emit_log("Reinforcement %s could not deploy outside the battlefield." % unit["name"])
_emit_log("원군 %s은 전장 밖에 설 수 없다." % unit["name"])
return false
if get_move_cost(unit["pos"], str(unit.get("move_type", "foot"))) >= 99:
_emit_log("Reinforcement %s could not deploy on impassable terrain." % unit["name"])
_emit_log("원군 %s은 험지에 설 수 없다." % unit["name"])
return false
if not get_unit(unit["id"]).is_empty():
_emit_log("Reinforcement %s already exists." % unit["name"])
_emit_log("원군 %s은 이미 전장에 있다." % unit["name"])
return false
if not get_unit_at(unit["pos"]).is_empty():
_emit_log("Reinforcement %s could not deploy at %s." % [unit["name"], _format_cell(unit["pos"])])
_emit_log("원군 %s%s에 설 수 없다." % [unit["name"], _format_cell(unit["pos"])])
return false
unit["acted"] = current_team == unit.get("team", "")
unit["moved"] = current_team == unit.get("team", "")
units.append(unit)
_emit_log("%s arrives at %s." % [unit["name"], _format_cell(unit["pos"])])
_emit_log("%s, %s에 당도." % [unit["name"], _format_cell(unit["pos"])])
return true