Files
aetherbound-guild/runtime/p9_8/runtime_controller.gd
T

486 lines
22 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
class_name P98RuntimeController
extends "res://p9_7/runtime_controller.gd"
const P98Content = preload("res://content/p9_8_content.gd")
const P98BattleService = preload("res://p9_8/battle_service.gd")
const P98BossBattleService = preload("res://p9_8/boss_battle_service.gd")
const P98BossService = preload("res://p9_8/boss_service.gd")
const P98ContractState = preload("res://p9_8/contract_state.gd")
const P98SaveService = preload("res://p9_8/save_service.gd")
const PANE_COLORS := {
"projectile": Color("#73e3e6"),
"spell": Color("#b29aff"),
"forced_movement": Color("#ffb15e"),
}
var boss_battle = P98BossBattleService.new()
var boss_mode := false
var boss_actor: TextureRect
var boss_hp_fill: ColorRect
var boss_pane_label: Label
func _bootstrap_market() -> void:
if entry_snapshot.is_empty():
last_code = "entry_snapshot_required"
_queue_render()
return
_install_p98_content()
market = P98BossService.new()
var restored := false
if bool(entry_snapshot.get("p98_activated", false)):
restored = market.state.restore_snapshot(entry_snapshot)
else:
restored = market.state.initialize_from_p97(entry_snapshot)
if not restored:
market = null
last_code = "p98_snapshot_invalid"
_queue_render()
return
battle = P98BattleService.new()
saves = P98SaveService.new()
pending_line_ids.assign(market.state.deployed_recruit_ids)
selected_inventory_instance = ""
focused_company_id = ""
last_code = "p98_contract_ready"
battle_active = false
battle_result_visible = false
boss_mode = false
_save_stable()
_queue_render()
func _install_p98_content() -> void:
content = P98Content.snapshot()
var view_professions: Dictionary = content.base_professions.duplicate(true)
for promotion_id in content.promotions:
var promotion: Dictionary = Dictionary(content.promotions[promotion_id]).duplicate(true)
var base: Dictionary = Dictionary(content.base_professions[promotion.parent_id]).duplicate(true)
base.merge(promotion, true)
view_professions[promotion_id] = base
content["professions"] = view_professions
func choose_artifact(artifact_id: String) -> void:
if market == null:
return
_accept_market_result(market.choose_artifact(_operation("artifact", artifact_id), artifact_id), true)
func choose_promotion(promotion_id: String) -> void:
if market == null or market.state.promotion_draft.is_empty():
return
var recruit_id := String(market.state.promotion_draft.recruit_id)
_accept_market_result(market.confirm_promotion(_operation("promotion", promotion_id), recruit_id, promotion_id), true)
func choose_boss_plan(plan: String) -> void:
if market == null:
return
var committed: Dictionary = market.commit_boss_plan(_operation("boss-plan", plan), plan)
last_code = String(committed.code)
if not committed.ok:
_queue_render()
return
_save_stable()
boss_battle = P98BossBattleService.new()
var started: Dictionary = boss_battle.start_from_contract(market.state)
last_code = String(started.code)
if not started.ok:
_queue_render()
return
boss_mode = true
battle_active = true
battle_result_visible = false
battle_paused = false
battle_speed = 1
battle_tick_accumulator = 0.0
battle_actions.clear()
presented_event_counts.clear()
_queue_render()
func prepare_full_boss_line() -> void:
if market == null or int(market.state.contract_phase) != P98ContractState.ContractPhase.BOSS_MUSTER or market.state.recovery_required:
return
var candidates: Array[String] = []
for recruit_id in market.state.company.slice(0, mini(market.state.line_capacity, market.state.company.size())):
if int(market.state.member_condition.get(recruit_id, 0)) > 0:
candidates.append(String(recruit_id))
var ordered: Array[String] = market.auto_order_candidate(candidates)
if ordered.is_empty():
last_code = "boss_line_unavailable"
_queue_render()
return
pending_line_ids.assign(ordered)
_accept_market_result(market.commit_deployment(_operation("boss-deploy", "full-line"), ordered), true)
func advance_battle_ticks(ticks: int) -> Dictionary:
if not boss_mode:
return super.advance_battle_ticks(ticks)
if not battle_active or battle_result_visible:
return {"ok": false, "code": "boss_battle_inactive", "events": []}
var result: Dictionary = boss_battle.advance_ticks(ticks)
last_code = String(result.code)
for event in result.events:
_handle_boss_event(event)
_update_boss_visuals()
if boss_battle.state.terminal:
battle_result_visible = true
_queue_render()
return result
func settle_and_return() -> void:
if not boss_mode:
super.settle_and_return()
return
if not battle_active or not boss_battle.state.terminal:
return
var result: Dictionary = market.settle_boss(_operation("boss-settle", boss_battle.state.plan), boss_battle.state.snapshot())
last_code = String(result.code)
if not result.ok:
_queue_render()
return
battle_active = false
battle_result_visible = false
boss_mode = false
pending_line_ids.assign(market.state.deployed_recruit_ids)
_save_stable()
_queue_render()
func close_contract() -> void:
if market == null:
return
var result: Dictionary = market.close_contract(_operation("contract-close", "windglass"))
last_code = String(result.code)
if result.ok:
pending_line_ids.clear()
focused_company_id = ""
_save_stable()
_queue_render()
func _operation(kind: String, target: String) -> String:
operation_index += 1
return "p98-ui-%s-%d-%d-%s" % [kind, market.state.revision, operation_index, target]
func _render_market() -> void:
match int(market.state.contract_phase):
P98ContractState.ContractPhase.BOSS_MUSTER:
_render_boss_muster()
P98ContractState.ContractPhase.CONTRACT_RESULT:
_render_contract_result()
P98ContractState.ContractPhase.RUN_CLOSED:
_render_persistent_result()
_:
if market.state.selected_artifact_id.is_empty():
_render_artifact_shrine()
elif not market.state.promotion_draft.is_empty():
_render_promotion_anvil()
else:
super._render_market()
func _render_artifact_shrine() -> void:
_add_background(HALL_BACKGROUND)
_add_contract_header(_tr("风晶契约 · 选择共享规则", "WINDGLASS CONTRACT · CHOOSE A SHARED RULE"), _tr("从下一座集市起永久生效", "Permanent from the next Market"))
var draft: Dictionary = market.state.artifact_draft
var offers: Array = draft.get("offer_ids", [])
for index in range(offers.size()):
var artifact_id := String(offers[index])
var row: Dictionary = content.artifacts[artifact_id]
var center_x := size.x * (0.34 if index == 0 else 0.66)
_add_ground_ring(Vector2(center_x, size.y * 0.62), 150, CYAN if index == 0 else GOLD, "P98ArtifactRing_%s" % artifact_id)
var glyph := _outlined_label("◇" if index == 0 else "◆", 58, CYAN if index == 0 else GOLD_LIGHT)
glyph.position = Vector2(center_x - 48, size.y * 0.25)
glyph.size = Vector2(96, 112)
glyph.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
display_root.add_child(glyph)
var name_label := _outlined_label(_tr(String(row.name_zh), String(row.name_en)), 19, WHITE)
name_label.position = Vector2(center_x - 160, size.y * 0.52)
name_label.size = Vector2(320, 42)
name_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
display_root.add_child(name_label)
var consequence := _outlined_label(_tr("获得:%s\n代价:%s" % [row.benefit_zh, row.cost_zh], "GAIN: %s\nCOST: %s" % [row.benefit_en, row.cost_en]), 11, WHITE)
consequence.position = Vector2(center_x - 180, size.y * 0.62)
consequence.size = Vector2(360, 66)
consequence.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
consequence.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
display_root.add_child(consequence)
var choose := _action_button("P98ArtifactButton_%s" % artifact_id, _tr("立下规则", "BIND RULE"), choose_artifact.bind(artifact_id))
choose.position = Vector2(center_x - 86, size.y - 66)
choose.size = Vector2(172, 50)
display_root.add_child(choose)
func _render_promotion_anvil() -> void:
_add_background(HALL_BACKGROUND)
var draft: Dictionary = market.state.promotion_draft
var recruit_id := String(draft.recruit_id)
_add_contract_header(_tr("晋阶铁砧 · %s" % _recruit_name(recruit_id), "PROMOTION ANVIL · %s" % _recruit_name(recruit_id)), _tr("保留等级、生命、装备与队列位置", "Keeps level, health, equipment and line position"))
var actor := _add_actor(_recruit_rig(recruit_id), Vector2(size.x * 0.5 - 58, size.y * 0.20), 116, "P98PromotionActor", true)
actor.self_modulate = _recruit_color(recruit_id)
for index in range(Array(draft.offer_ids).size()):
var promotion_id := String(draft.offer_ids[index])
var row: Dictionary = content.promotions[promotion_id]
var x := size.x * (0.27 if index == 0 else 0.73)
var title := _outlined_label(_tr(String(row.name_zh), String(row.name_en)), 19, GOLD_LIGHT)
title.position = Vector2(x - 175, size.y * 0.51)
title.size = Vector2(350, 40)
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
display_root.add_child(title)
var choice := _outlined_label(_tr(String(row.choice_zh), String(row.choice_en)), 11, WHITE)
choice.position = Vector2(x - 180, size.y * 0.61)
choice.size = Vector2(360, 54)
choice.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
choice.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
display_root.add_child(choice)
var button := _action_button("P98PromotionButton_%s" % promotion_id, _tr("选择晋阶", "PROMOTE"), choose_promotion.bind(promotion_id))
button.position = Vector2(x - 88, size.y - 66)
button.size = Vector2(176, 50)
display_root.add_child(button)
func _render_boss_muster() -> void:
_add_background(WINDGLASS_BACKGROUND)
_add_contract_header(_tr("Boss 集结 · 多棱镜鹿", "BOSS MUSTER · THE MANY-PANED STAG"), _tr("投射屏障起始 · 半血后每次转两棱 · 鹿角横扫最前列", "Projectile pane first · two-step turns below half · sweep hits the foremost"))
_add_ground_ring(Vector2(size.x * 0.68, size.y * 0.71), 190, VIOLET, "P98BossFootprintRear")
_add_ground_ring(Vector2(size.x * 0.77, size.y * 0.71), 190, CYAN, "P98BossFootprintFront")
var stag := _add_actor("Riftblade", Vector2(size.x * 0.66, size.y * 0.30), minf(190, size.y * 0.48), "P98BossMusterActor", true, "idle")
stag.self_modulate = Color("#d9c6ff")
var company_text := _outlined_label(_tr("战线 %d/%d · ◆%d · 当前 Artifact:%s" % [market.state.deployed_recruit_ids.size(), market.state.line_capacity, market.state.coin, _artifact_name()], "LINE %d/%d · ◆%d · ARTIFACT: %s" % [market.state.deployed_recruit_ids.size(), market.state.line_capacity, market.state.coin, _artifact_name()]), 12, WHITE)
company_text.position = Vector2(18, size.y * 0.27)
company_text.size = Vector2(size.x * 0.48, 44)
display_root.add_child(company_text)
if market.state.recovery_required:
var recovery := _outlined_label(_tr("队伍溃败:支付 ◆%d,回到同一只鹿、同一棱面与同一阵容。" % market.state.recovery_cost, "DEFEAT: pay ◆%d and return to the same Stag, pane and Company." % market.state.recovery_cost), 13, CORAL)
recovery.position = Vector2(18, size.y * 0.46)
recovery.size = Vector2(size.x * 0.48, 62)
recovery.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
display_root.add_child(recovery)
var recover := _action_button("P98BossRecoveryButton", _tr("整队恢复 ◆%d" % market.state.recovery_cost, "RECOVER ◆%d" % market.state.recovery_cost), recover_company)
recover.position = Vector2(18, size.y - 68)
recover.size = Vector2(220, 52)
display_root.add_child(recover)
return
var rally := _secondary_button("P98BossRallyButton", _tr("最终整编 %d/%d" % [market.state.deployed_recruit_ids.size(), market.state.line_capacity], "FINAL RALLY %d/%d" % [market.state.deployed_recruit_ids.size(), market.state.line_capacity]), prepare_full_boss_line)
rally.position = Vector2(18, size.y * 0.38)
rally.size = Vector2(224, 52)
display_root.add_child(rally)
var plans: Array = market.state.boss_muster.get("plans", [])
for index in range(plans.size()):
var row: Dictionary = plans[index]
var x := 18.0 + float(index) * 240.0
var summary := _outlined_label(_tr(String(row.summary_zh), String(row.summary_en)), 10, WHITE)
summary.position = Vector2(x, size.y * 0.52)
summary.size = Vector2(224, 80)
summary.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
display_root.add_child(summary)
var button := _action_button("P98BossPlanButton_%s" % String(row.id), _tr(String(row.name_zh), String(row.name_en)), choose_boss_plan.bind(String(row.id)))
button.position = Vector2(x, size.y - 68)
button.size = Vector2(224, 52)
display_root.add_child(button)
func _render_battle() -> void:
if not boss_mode:
super._render_battle()
for node_name in ["P97PauseButton", "P97SpeedButton"]:
var control := find_child(node_name, true, false) as Control
if control != null:
control.position.y = 4
control.size.y = 48
return
_render_boss_battle()
func _render_boss_battle() -> void:
_add_background(WINDGLASS_BACKGROUND)
var title := _outlined_label(_tr("多棱镜鹿 · %s" % _boss_plan_name(), "THE MANY-PANED STAG · %s" % _boss_plan_name()), 15, WHITE)
title.position = Vector2(16, 6)
title.size = Vector2(size.x * 0.52, 42)
display_root.add_child(title)
boss_pane_label = _outlined_label("", 12, PANE_COLORS.get(boss_battle.state.pane, CYAN))
boss_pane_label.position = Vector2(size.x * 0.48, 6)
boss_pane_label.size = Vector2(size.x * 0.25, 42)
boss_pane_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
display_root.add_child(boss_pane_label)
var pause := _secondary_button("P98BossPauseButton", "▶" if battle_paused else "Ⅱ", toggle_battle_pause)
pause.position = Vector2(size.x - 112, 4)
pause.size = Vector2(48, 48)
display_root.add_child(pause)
var speed := _secondary_button("P98BossSpeedButton", "%dx" % battle_speed, toggle_battle_speed)
speed.position = Vector2(size.x - 58, 4)
speed.size = Vector2(50, 48)
display_root.add_child(speed)
for recruit_id in boss_battle.state.member_ids:
var actor := _add_actor(_recruit_rig(recruit_id), Vector2.ZERO, clampf(size.y * 0.25, 92, 132), "P98BossBattleActor_%s" % recruit_id, false)
actor.self_modulate = _recruit_color(recruit_id)
battle_actors[recruit_id] = actor
boss_actor = _add_actor("Riftblade", Vector2.ZERO, clampf(size.y * 0.46, 160, 220), "P98BossBattleActor_BO-R1-01", false)
boss_actor.self_modulate = Color("#d9c6ff")
battle_actors["BO-R1-01"] = boss_actor
var hp_back := ColorRect.new()
hp_back.name = "P98BossVitality"
hp_back.position = Vector2(size.x * 0.58, 56)
hp_back.size = Vector2(size.x * 0.34, 14)
hp_back.color = Color(0.02, 0.04, 0.07, 0.94)
display_root.add_child(hp_back)
boss_hp_fill = ColorRect.new()
boss_hp_fill.position = Vector2(2, 2)
boss_hp_fill.size = Vector2(hp_back.size.x - 4, 10)
boss_hp_fill.color = VIOLET
hp_back.add_child(boss_hp_fill)
_update_boss_visuals()
if battle_result_visible:
_render_boss_battle_result()
func _update_boss_visuals() -> void:
if not boss_mode or boss_actor == null:
return
for recruit_id in boss_battle.state.member_ids:
var actor: TextureRect = battle_actors.get(recruit_id)
if actor == null:
continue
var hp := int(boss_battle.state.member_hp[recruit_id])
var x := _battle_x(int(boss_battle.state.member_positions[recruit_id]))
actor.position = Vector2(x - actor.size.x * 0.5, size.y * 0.51)
actor.texture = animation.texture(_recruit_rig(recruit_id), "idle" if boss_battle.state.terminal else "advance", world_clock)
actor.modulate.a = 0.25 if hp <= 0 else 1.0
actor.visible = hp > 0 or not boss_battle.state.terminal
boss_actor.position = Vector2(_battle_x(100) - boss_actor.size.x * 0.62, size.y * 0.34)
boss_actor.texture = animation.texture("Riftblade", "death" if boss_battle.state.boss_hp <= 0 else "attack" if int(boss_battle.state.tick) % 12 <= 2 else "idle", world_clock)
if boss_hp_fill != null:
boss_hp_fill.size.x = maxf(0, (size.x * 0.34 - 4) * float(boss_battle.state.boss_hp) / maxf(1, boss_battle.state.boss_max_hp))
if boss_pane_label != null:
boss_pane_label.text = _tr("当前屏障:%s%s" % [_pane_name(boss_battle.state.pane), " · 双步旋转" if boss_battle.state.phase_two else ""], "PANE: %s%s" % [_pane_name(boss_battle.state.pane), " · TWO-STEP" if boss_battle.state.phase_two else ""])
boss_pane_label.add_theme_color_override("font_color", PANE_COLORS.get(boss_battle.state.pane, CYAN))
func _update_battle_control_labels() -> void:
if not boss_mode:
super._update_battle_control_labels()
return
var pause := find_child("P98BossPauseButton", true, false) as Button
var speed := find_child("P98BossSpeedButton", true, false) as Button
if pause != null:
pause.text = "▶" if battle_paused else "Ⅱ"
if speed != null:
speed.text = "%dx" % battle_speed
func _handle_boss_event(event: Dictionary) -> void:
var kind := String(event.get("kind", ""))
presented_event_counts[kind] = int(presented_event_counts.get(kind, 0)) + 1
if kind == "member_action":
var source := String(event.get("source_member_id", ""))
var target := String(event.get("target_member_id", "BO-R1-01"))
var delta := int(event.get("delta", 0))
_spawn_number(delta, target, JADE if delta > 0 else CORAL)
if target == "BO-R1-01":
_spawn_projectile(source, target)
elif kind == "boss_action":
var target := String(event.get("target_member_id", ""))
_spawn_number(int(event.get("delta", 0)), target, CORAL)
_spawn_vfx(target, HIT_VFX)
elif kind == "boss_rule":
_spawn_word("BO-R1-01", _tr("转棱", "PANE TURN") if String(event.get("rule", "")) == "pane_rotated" else _tr("屏障", "SCREENED"), PANE_COLORS.get(String(event.get("to_family", event.get("pane", "projectile"))), CYAN))
elif kind == "boss_plan":
_spawn_word("BO-R1-01", _tr("固阵", "BRACE") if String(event.get("plan", "")) == "brace" else _tr("破面", "ROTATE"), GOLD_LIGHT)
func _render_boss_battle_result() -> void:
var band := ColorRect.new()
band.name = "P98BossResultBand"
band.position = Vector2(0, size.y - 76)
band.size = Vector2(size.x, 76)
band.color = Color(0.015, 0.035, 0.075, 0.95)
display_root.add_child(band)
var won: bool = String(boss_battle.state.outcome) == "victory"
var result := _outlined_label(_tr("契约首领击破 · 结算永久记录", "CONTRACT BOSS DEFEATED · RECORD THE CLEAR") if won else _tr("战线溃败 · 可返回同一集结点恢复", "COMPANY DEFEATED · RECOVER AT THE SAME MUSTER"), 18, GOLD_LIGHT if won else CORAL)
result.position = Vector2(18, size.y - 70)
result.size = Vector2(size.x * 0.67, 58)
display_root.add_child(result)
var button := _action_button("P98BossSettleButton", _tr("确认战果", "CONFIRM RESULT"), settle_and_return)
button.position = Vector2(size.x - 212, size.y - 66)
button.size = Vector2(194, 52)
display_root.add_child(button)
func _render_contract_result() -> void:
_add_background(WINDGLASS_BACKGROUND)
_add_contract_header(_tr("风晶契约已完成", "WINDGLASS CONTRACT COMPLETE"), _tr("永久结算只执行一次", "The persistent settlement applies exactly once"))
var result: Dictionary = market.state.pending_boss_result
var summary := _outlined_label(_tr("多棱镜鹿已击破\n计划:%s\n永久获得:3 声望 · Company Record · Boss/Artifact/晋阶图鉴" % _boss_plan_name(), "THE MANY-PANED STAG DEFEATED\nPLAN: %s\nPERSIST: 3 RENOWN · COMPANY RECORD · BOSS/ARTIFACT/PROMOTION DISCOVERIES" % _boss_plan_name()), 17, WHITE)
summary.position = Vector2(size.x * 0.16, size.y * 0.27)
summary.size = Vector2(size.x * 0.68, size.y * 0.42)
summary.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
summary.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
summary.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
display_root.add_child(summary)
var close := _action_button("P98CloseContractButton", _tr("写入公会编年史", "WRITE THE GUILD RECORD"), close_contract)
close.position = Vector2(size.x * 0.5 - 132, size.y - 72)
close.size = Vector2(264, 54)
display_root.add_child(close)
func _render_persistent_result() -> void:
_add_background(HALL_BACKGROUND)
_add_contract_header(_tr("风晶契约 · 永久完成", "WINDGLASS CONTRACT · PERMANENT CLEAR"), _tr("下一份契约方向已开启", "The next Contract direction is open"))
var record: Dictionary = market.state.company_records[-1] if not market.state.company_records.is_empty() else {}
var company_size := Array(record.get("company", [])).size()
var summary := _outlined_label(_tr("声望 %d\n完成印记 CLEAR-RG01-C01\nCompany Record %d 名成员 / 12 回合\n已发现 %d 项\n\n本轮金币、队伍、装备与市场已结清;永久记录保留。" % [market.state.persistent_renown, company_size, market.state.discovery_ids.size()], "RENOWN %d\nCLEAR MARK CLEAR-RG01-C01\nCOMPANY RECORD %d MEMBERS / 12 ROUNDS\nDISCOVERIES %d\n\nRun Coin, Company, gear and Market are closed; the permanent record remains." % [market.state.persistent_renown, company_size, market.state.discovery_ids.size()]), 16, WHITE)
summary.position = Vector2(size.x * 0.17, size.y * 0.24)
summary.size = Vector2(size.x * 0.66, size.y * 0.55)
summary.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
summary.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
summary.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
display_root.add_child(summary)
func _add_contract_header(title_text: String, subtitle_text: String) -> void:
var shade := ColorRect.new()
shade.position = Vector2.ZERO
shade.size = Vector2(size.x, 58)
shade.color = Color(0.015, 0.035, 0.075, 0.88)
shade.mouse_filter = Control.MOUSE_FILTER_IGNORE
display_root.add_child(shade)
var title := _outlined_label(title_text, 17, GOLD_LIGHT)
title.position = Vector2(16, 2)
title.size = Vector2(size.x * 0.58, 52)
display_root.add_child(title)
var subtitle := _outlined_label(subtitle_text, 11, WHITE)
subtitle.position = Vector2(size.x * 0.48, 2)
subtitle.size = Vector2(size.x * 0.50 - 16, 52)
subtitle.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
subtitle.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
display_root.add_child(subtitle)
func _artifact_name() -> String:
var artifact_id := String(market.state.selected_artifact_id)
if artifact_id.is_empty() or not content.artifacts.has(artifact_id):
return _tr("未选择", "NONE")
var row: Dictionary = content.artifacts[artifact_id]
return _tr(String(row.name_zh), String(row.name_en))
func _boss_plan_name() -> String:
var plan := String(market.state.selected_boss_plan)
return _tr("固阵反击", "BRACE AND PUNISH") if plan == "brace" else _tr("转棱破面", "ROTATE THE PANE")
func _pane_name(pane: String) -> String:
var names := {
"projectile": [_tr("投射", "PROJECTILE")],
"spell": [_tr("法术", "SPELL")],
"forced_movement": [_tr("位移", "FORCED MOVE")],
}
return String(names.get(pane, [pane])[0])