class_name P97RuntimeController extends Control const Content = preload("res://content/p9_7_content.gd") const MarketService = preload("res://p9_7/market_service.gd") const MarketState = preload("res://p9_7/market_state.gd") const BattleService = preload("res://p9_7/battle_service.gd") const SaveService = preload("res://p9_7/save_service.gd") const AnimationAdapter = preload("res://presentation/animation_adapter.gd") const HALL_BACKGROUND = preload("res://assets/backgrounds/p9_3_guild_hall.png") const WINDGLASS_BACKGROUND = preload("res://assets/backgrounds/windglass_verge.webp") const UI_FONT = preload("res://assets/fonts/NotoSansSC.ttf") const HEAL_VFX = preload("res://assets/vfx/protect_gain.png") const HIT_VFX = preload("res://assets/vfx/damage_heavy.png") const FRONTPIN_TEXTURE = preload("res://assets/equipment/eq_002_frontpin_greaves.png") const OPEN_LINE_TEXTURE = preload("res://assets/equipment/eq_012_open_line_cord.png") const ANIMATION_SPECS := { "Hearthguard": { "idle": {"root": "hearthguard/idle", "frames": 6, "fps": 6.0, "loop": true}, "advance": {"root": "hearthguard/advance", "frames": 8, "fps": 10.0, "loop": true}, "guard": {"root": "hearthguard/guard", "frames": 4, "fps": 10.0, "loop": false}, "basic_attack": {"root": "hearthguard/basic_attack", "frames": 6, "fps": 12.0, "loop": false}, "victory": {"root": "hearthguard/victory", "frames": 6, "fps": 6.0, "loop": false}, }, "Wayrunner": { "idle": {"root": "wayrunner/idle", "frames": 2, "fps": 3.0, "loop": true}, "advance": {"root": "wayrunner/advance", "frames": 2, "fps": 6.0, "loop": true}, "evade_counter": {"root": "wayrunner/evade_counter", "frames": 4, "fps": 7.0, "loop": false}, }, "Farstring": { "idle": {"root": "farstring/idle", "frames": 2, "fps": 3.0, "loop": true}, "advance": {"root": "farstring/advance", "frames": 2, "fps": 6.0, "loop": true}, "draw_release": {"root": "farstring/draw_release", "frames": 4, "fps": 7.0, "loop": false}, }, "Kindhand": { "idle": {"root": "kindhand/idle", "frames": 2, "fps": 3.0, "loop": true}, "advance": {"root": "kindhand/advance", "frames": 2, "fps": 6.0, "loop": true}, "lantern_heal": {"root": "kindhand/lantern_heal", "frames": 4, "fps": 7.0, "loop": false}, }, "Riftblade": { "idle": {"root": "riftblade/idle", "frames": 6, "fps": 6.0, "loop": true}, "advance": {"root": "riftblade/advance", "frames": 8, "fps": 10.0, "loop": true}, "telegraph": {"root": "riftblade/telegraph", "frames": 6, "fps": 10.0, "loop": false}, "attack": {"root": "riftblade/attack", "frames": 6, "fps": 12.0, "loop": false}, "hit": {"root": "riftblade/hit", "frames": 4, "fps": 10.0, "loop": false}, "death": {"root": "riftblade/death", "frames": 8, "fps": 10.0, "loop": false}, }, } const GOLD := Color("#f4c45c") const GOLD_LIGHT := Color("#ffe8a3") const INK := Color("#102033") const WHITE := Color("#f8fbff") const CYAN := Color("#73e3e6") const CORAL := Color("#ff7478") const JADE := Color("#75e0a4") const VIOLET := Color("#b29aff") const MARKET_HEADER_HEIGHT := 50.0 const MARKET_CONTEXT_HEIGHT := 74.0 const MARKET_SIDE_MARGIN := 16.0 var entry_snapshot: Dictionary = {} var market var battle = BattleService.new() var saves = SaveService.new() var content: Dictionary = Content.snapshot() var animation = AnimationAdapter.new() var display_root: Control var locale := "zh_CN" var text_scale := 1.0 var reduced_motion := false var muted := true var low_power := false var persistence_enabled := true var selected_inventory_instance := "" var focused_company_id := "" var pending_line_ids: Array[String] = [] var operation_index := 0 var last_code := "" var battle_active := false var battle_result_visible := false var battle_auto_advance := true var battle_paused := false var battle_speed := 1 var battle_tick_accumulator := 0.0 var world_clock := 0.0 var render_queued := false var ambient_actors: Dictionary = {} var battle_actors: Dictionary = {} var battle_hp_bars: Dictionary = {} var battle_actions: Dictionary = {} var presented_event_counts: Dictionary = {} func configure_entry(snapshot: Dictionary) -> void: entry_snapshot = snapshot.duplicate(true) if is_inside_tree(): _bootstrap_market() func _ready() -> void: set_meta("visible_world_ratio", 0.86) var ui_theme := Theme.new() ui_theme.default_font = UI_FONT theme = ui_theme animation.load_specs(ANIMATION_SPECS) display_root = Control.new() display_root.name = "P97DisplayRoot" display_root.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) add_child(display_root) resized.connect(_queue_render) _bootstrap_market() func _bootstrap_market() -> void: if entry_snapshot.is_empty(): last_code = "entry_snapshot_required" _queue_render() return if entry_snapshot.has("content_revision") and entry_snapshot.has("current_market"): market = MarketService.new() if not market.state.restore_snapshot(entry_snapshot): market = null last_code = "market_snapshot_invalid" _queue_render() return else: market = MarketService.new(entry_snapshot) pending_line_ids.assign(market.state.deployed_recruit_ids) selected_inventory_instance = "" focused_company_id = "" last_code = "market_ready" battle_active = false battle_result_visible = false _save_stable() _queue_render() func _process(delta: float) -> void: if not low_power: world_clock += maxf(0.0, delta) _update_ambient_actors() if not battle_active or battle_result_visible or battle_paused or not battle_auto_advance: return battle_tick_accumulator += maxf(0.0, delta) * float(battle_speed) var ticks := int(floor(battle_tick_accumulator / 0.08)) if ticks <= 0: return battle_tick_accumulator -= float(ticks) * 0.08 advance_battle_ticks(ticks) func tap_recruit_offer(recruit_id: String) -> void: if not _market_ready(): return if market.state.focused_recruit_offer != recruit_id: _accept_market_result(market.focus_recruit_offer(recruit_id)) return var result: Dictionary = market.hire(_operation("hire", recruit_id), recruit_id) if result.ok: pending_line_ids.assign(market.state.deployed_recruit_ids) _accept_market_result(result, true) func tap_equipment_offer(offer_id: String) -> void: if not _market_ready(): return if market.state.focused_equipment_offer != offer_id: _accept_market_result(market.focus_equipment_offer(offer_id)) return var result: Dictionary = market.buy_equipment(_operation("buy", offer_id), offer_id) if result.ok: selected_inventory_instance = String(result.receipt.instance_id) _accept_market_result(result, true) func tap_company_recruit(recruit_id: String) -> void: if not _market_ready() or recruit_id not in market.state.company: return if not selected_inventory_instance.is_empty(): var equip_result: Dictionary = market.equip(_operation("equip", recruit_id), selected_inventory_instance, recruit_id) if equip_result.ok: selected_inventory_instance = "" _accept_market_result(equip_result, true) return if focused_company_id != recruit_id: focused_company_id = recruit_id last_code = "company_focused" _queue_render() return if recruit_id in pending_line_ids: if pending_line_ids.size() > 1: pending_line_ids.erase(recruit_id) last_code = "line_removed" else: if pending_line_ids.size() < market.state.line_capacity and int(market.state.member_condition.get(recruit_id, 0)) > 0: pending_line_ids.append(recruit_id) last_code = "line_added" _queue_render() func commit_pending_line() -> void: if not _market_ready(): return var result: Dictionary = market.commit_deployment(_operation("deploy", "line"), pending_line_ids) _accept_market_result(result, true) func preview_focused_dismissal() -> void: if not _market_ready() or focused_company_id.is_empty(): return _accept_market_result(market.preview_dismissal(focused_company_id)) func confirm_focused_dismissal() -> void: if not _market_ready() or market.state.pending_dismissal.is_empty(): return var recruit_id := String(market.state.pending_dismissal.recruit_id) var result: Dictionary = market.confirm_dismissal(_operation("dismiss", recruit_id), recruit_id) if result.ok: focused_company_id = "" pending_line_ids.assign(market.state.deployed_recruit_ids) _accept_market_result(result, true) func cancel_dismissal() -> void: if not _market_ready(): return _accept_market_result(market.cancel_dismissal()) func recover_company() -> void: if not _market_ready(): return var result: Dictionary = market.recover_injured_company(_operation("recover", "company")) _accept_market_result(result, true) func tap_route(route_id: String) -> void: if not _market_ready(): return if market.state.focused_route_offer != route_id: _accept_market_result(market.focus_route(route_id)) return var result: Dictionary = market.commit_route(_operation("route", route_id), route_id) if not result.ok: _accept_market_result(result) return _save_stable() start_committed_battle() func start_committed_battle() -> void: if market == null: return var result: Dictionary = battle.start_from_market(market.state) last_code = String(result.code) if not result.ok: _queue_render() return 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 advance_battle_ticks(ticks: int) -> Dictionary: if not battle_active or battle_result_visible: return {"ok": false, "code": "battle_inactive", "events": []} var result: Dictionary = battle.advance_ticks(ticks) last_code = String(result.code) for event in result.events: _handle_battle_event(event) _update_battle_visuals() if battle.state.terminal: battle_result_visible = true _queue_render() return result func toggle_battle_pause() -> void: if not battle_active or battle_result_visible: return battle_paused = not battle_paused _update_battle_control_labels() func toggle_battle_speed() -> void: if not battle_active or battle_result_visible: return battle_speed = 2 if battle_speed == 1 else 1 _update_battle_control_labels() func settle_and_return() -> void: if not battle_active or not battle.state.terminal: return var result: Dictionary = market.settle_battle(_operation("settle", battle.state.route_id), battle.state.snapshot()) last_code = String(result.code) if not result.ok: _queue_render() return battle_active = false battle_result_visible = false pending_line_ids.assign(market.state.deployed_recruit_ids) _save_stable() _queue_render() func _market_ready() -> bool: return market != null and not battle_active and market.state.phase == MarketState.Phase.MARKET func _accept_market_result(result: Dictionary, persist := false) -> void: last_code = String(result.get("code", "")) if result.ok and persist: _save_stable() _queue_render() func _save_stable() -> bool: if not persistence_enabled or market == null: return false return bool(saves.save_snapshot(market.state.snapshot()).ok) func _operation(kind: String, target: String) -> String: operation_index += 1 return "p97-ui-%s-%d-%d-%s" % [kind, market.state.revision, operation_index, target] func _queue_render() -> void: if display_root == null or render_queued: return render_queued = true call_deferred("_deferred_render") func _deferred_render() -> void: render_queued = false _render_current() func _render_current() -> void: if display_root == null: return _clear_display() if size.y > size.x: _render_rotate_notice() elif market == null: _render_missing_entry() elif battle_active: _render_battle() else: _render_market() func _clear_display() -> void: ambient_actors.clear() battle_actors.clear() battle_hp_bars.clear() for child in display_root.get_children(): child.free() func _render_market() -> void: _add_background(HALL_BACKGROUND) _add_market_hud() if market.state.continuation_locked: _render_relay_threshold() return _render_company_line() _render_recruit_offers() _render_equipment_pedestals() _render_route_gates() _render_market_context() func _add_market_hud() -> void: var header := ColorRect.new() header.name = "P97MarketHud" header.position = Vector2.ZERO header.size = Vector2(size.x, MARKET_HEADER_HEIGHT) header.color = Color(0.015, 0.035, 0.075, 0.84) header.mouse_filter = Control.MOUSE_FILTER_IGNORE display_root.add_child(header) var title_width := minf(280.0, size.x * 0.34) var title := _outlined_label(_tr("风晶集市 %d" % market.state.current_market, "WINDGLASS MARKET %d" % market.state.current_market), 16, GOLD_LIGHT) title.position = Vector2(18, 5) title.size = Vector2(title_width - 18.0, 40) title.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS display_root.add_child(title) var values := _outlined_label(_tr("◆ %d 战线 %d/%d 队伍 %d 预备 %d" % [market.state.coin, market.state.deployed_recruit_ids.size(), market.state.line_capacity, market.state.company.size(), market.state.reserve_recruit_ids.size()], "◆ %d LINE %d/%d COMPANY %d RESERVE %d" % [market.state.coin, market.state.deployed_recruit_ids.size(), market.state.line_capacity, market.state.company.size(), market.state.reserve_recruit_ids.size()]), 13, WHITE) values.position = Vector2(title_width, 5) values.size = Vector2(size.x - title_width - 18.0, 40) values.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT values.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS display_root.add_child(values) func _render_company_line() -> void: var world_bottom := size.y - MARKET_CONTEXT_HEIGHT var actor_size := clampf(size.y * 0.25, 82.0, 132.0) var actor_y := world_bottom - actor_size - 24.0 var caption := _outlined_label(_tr("出战线 / 预备队", "LINE / RESERVE"), 11, GOLD_LIGHT) caption.position = Vector2(MARKET_SIDE_MARGIN, actor_y - 24.0) caption.size = Vector2(210, 28) display_root.add_child(caption) var count: int = market.state.company.size() var spacing := minf(actor_size + 6.0, maxf(48.0, (size.x - MARKET_SIDE_MARGIN * 2.0 - actor_size) / maxf(1.0, float(count - 1)))) for index in range(count): var recruit_id: String = market.state.company[index] var in_line := recruit_id in pending_line_ids var member_size := actor_size if in_line else actor_size - 8.0 var x := MARKET_SIDE_MARGIN + float(index) * spacing var y := actor_y if in_line else actor_y + 10.0 _add_ground_ring(Vector2(x + member_size * 0.5, y + member_size - 5), member_size * 0.72, GOLD if in_line else CYAN, "P97LineRing_%s" % recruit_id) var actor := _add_actor(_recruit_rig(recruit_id), Vector2(x, y), member_size, "P97CompanyActor_%s" % recruit_id, true) actor.self_modulate = _recruit_color(recruit_id) var target := _actor_target("P97CompanyTarget_%s" % recruit_id, Vector2(x - 4, y - 6), Vector2(member_size + 8.0, member_size + 12.0), tap_company_recruit.bind(recruit_id)) display_root.add_child(target) var state_text := _tr("战线", "LINE") if in_line else _tr("预备", "RESERVE") var label := _outlined_label("%s %dHP" % [state_text, int(market.state.member_condition[recruit_id])], 8, GOLD_LIGHT if in_line else CYAN) label.position = Vector2(x - 8, world_bottom - 26.0) label.size = Vector2(member_size + 20.0, 24) label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS display_root.add_child(label) func _render_recruit_offers() -> void: var lane := _market_recruit_lane() var caption := _outlined_label(_tr("候选队员", "RECRUITS"), 11, GOLD_LIGHT) caption.position = Vector2(lane.position.x, 52) caption.size = Vector2(lane.size.x, 24) display_root.add_child(caption) var offer_ids: Array = market.state.available_recruit_offer_ids var slot_width := lane.size.x / maxf(1.0, float(offer_ids.size())) for index in range(offer_ids.size()): var recruit_id := String(offer_ids[index]) var offer_size := clampf(slot_width * 0.88, 72.0, 92.0) var x := lane.position.x + float(index) * slot_width + (slot_width - offer_size) * 0.5 var actor := _add_actor(_recruit_rig(recruit_id), Vector2(x, 66), offer_size, "P97RecruitActor_%s" % recruit_id, true) actor.self_modulate = _recruit_color(recruit_id) var target := _actor_target("P97RecruitTarget_%s" % recruit_id, Vector2(x - 4, 62), Vector2(offer_size + 8.0, 106), tap_recruit_offer.bind(recruit_id)) display_root.add_child(target) var role := _outlined_label(_profession_name(recruit_id), 8, GOLD_LIGHT if market.state.focused_recruit_offer == recruit_id else WHITE) role.position = Vector2(lane.position.x + float(index) * slot_width, 148) role.size = Vector2(slot_width, 26) role.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER role.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS display_root.add_child(role) func _render_equipment_pedestals() -> void: var lane := _market_equipment_lane() var caption := _outlined_label(_tr("装备台", "GEAR"), 11, GOLD_LIGHT) caption.position = Vector2(lane.position.x, 52) caption.size = Vector2(lane.size.x, 24) display_root.add_child(caption) var offer_ids: Array = market.state.available_equipment_offer_ids var gap := 3.0 var slot_width := (lane.size.x - gap * float(maxi(0, offer_ids.size() - 1))) / maxf(1.0, float(offer_ids.size())) for index in range(offer_ids.size()): var offer_id := String(offer_ids[index]) var catalog_id := String(content.equipment_offers[offer_id].catalog_id) var x := lane.position.x + float(index) * (slot_width + gap) var pedestal := ColorRect.new() pedestal.name = "P97Pedestal_%s" % offer_id pedestal.position = Vector2(x + 2.0, 126) pedestal.size = Vector2(slot_width - 4.0, 10) pedestal.color = GOLD if market.state.focused_equipment_offer == offer_id else Color(0.30, 0.44, 0.52, 0.90) pedestal.mouse_filter = Control.MOUSE_FILTER_IGNORE display_root.add_child(pedestal) var icon := TextureRect.new() icon.texture = _equipment_texture(catalog_id) icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED icon.custom_minimum_size = Vector2.ZERO icon.position = Vector2(x + (slot_width - 38.0) * 0.5, 82) icon.size = Vector2(38, 46) icon.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST icon.self_modulate = _equipment_color(catalog_id) icon.mouse_filter = Control.MOUSE_FILTER_IGNORE display_root.add_child(icon) var target := _actor_target("P97EquipmentTarget_%s" % offer_id, Vector2(x, 76), Vector2(slot_width, 64), tap_equipment_offer.bind(offer_id)) display_root.add_child(target) var lock := _outlined_label(_tr("锁 刷新:中继站离线", "LOCK REFRESH: RELAY OFFLINE"), 8, Color(0.72, 0.80, 0.86)) lock.position = Vector2(lane.position.x, 140) lock.size = Vector2(lane.size.x, 26) lock.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS display_root.add_child(lock) func _render_route_gates() -> void: var lane := _market_route_lane() var caption := _outlined_label(_tr("远征门", "ROUTES"), 11, GOLD_LIGHT) caption.position = Vector2(lane.position.x, 52) caption.size = Vector2(lane.size.x, 24) display_root.add_child(caption) var route_ids: Array = market.state.available_battle_offer_ids var gap := 4.0 var gate_width := (lane.size.x - gap * float(maxi(0, route_ids.size() - 1))) / maxf(1.0, float(route_ids.size())) for index in range(route_ids.size()): var route_id := String(route_ids[index]) var route: Dictionary = content.routes[route_id] var x := lane.position.x + float(index) * (gate_width + gap) var gate := ColorRect.new() gate.name = "P97Gate_%s" % route_id gate.position = Vector2(x, 78) gate.size = Vector2(gate_width, 88) gate.color = _risk_color(int(route.risk), market.state.focused_route_offer == route_id) gate.mouse_filter = Control.MOUSE_FILTER_IGNORE display_root.add_child(gate) var risk := _outlined_label([_tr("稳", "S"), _tr("险", "P"), _tr("精", "E")][int(route.risk)], 10, WHITE) risk.position = Vector2(x, 88) risk.size = Vector2(gate_width, 44) risk.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER display_root.add_child(risk) var reward := _outlined_label("+%d" % int(route.reward), 9, GOLD_LIGHT) reward.position = Vector2(x, 126) reward.size = Vector2(gate_width, 30) reward.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER display_root.add_child(reward) var target := _actor_target("P97RouteTarget_%s" % route_id, Vector2(x - 2, 74), Vector2(maxf(48.0, gate_width + 4), 96), tap_route.bind(route_id)) display_root.add_child(target) func _market_route_lane() -> Rect2: var lane_width := clampf(size.x * 0.24, 176.0, 250.0) return Rect2(Vector2(size.x - MARKET_SIDE_MARGIN - lane_width, 50), Vector2(lane_width, 120)) func _market_equipment_lane() -> Rect2: var route_lane := _market_route_lane() var lane_width := clampf(size.x * 0.245, 200.0, 240.0) return Rect2(Vector2(route_lane.position.x - 12.0 - lane_width, 50), Vector2(lane_width, 120)) func _market_recruit_lane() -> Rect2: var equipment_lane := _market_equipment_lane() var left := maxf(136.0, size.x * 0.18) var available := equipment_lane.position.x - 12.0 - left return Rect2(Vector2(left, 50), Vector2(minf(320.0, available), 120)) func _render_market_context() -> void: var ribbon := ColorRect.new() ribbon.name = "P97ContextRibbon" ribbon.position = Vector2(0, size.y - MARKET_CONTEXT_HEIGHT) ribbon.size = Vector2(size.x, MARKET_CONTEXT_HEIGHT) ribbon.color = Color(0.015, 0.035, 0.075, 0.92) display_root.add_child(ribbon) var text := _market_context_text() var detail := _outlined_label(text, 11, WHITE) detail.name = "P97ContextText" detail.position = Vector2(16, size.y - 69) detail.size = Vector2(size.x * 0.65, 62) detail.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART display_root.add_child(detail) if not market.state.pending_dismissal.is_empty(): var confirm := _action_button("P97DismissConfirmButton", _tr("确认解雇", "CONFIRM DISMISS"), confirm_focused_dismissal) confirm.position = Vector2(size.x - 286, size.y - 62) confirm.size = Vector2(132, 48) display_root.add_child(confirm) var cancel := _secondary_button("P97DismissCancelButton", _tr("取消", "CANCEL"), cancel_dismissal) cancel.position = Vector2(size.x - 146, size.y - 62) cancel.size = Vector2(130, 48) display_root.add_child(cancel) return if market.state.recovery_required: var recover := _action_button("P97RecoveryButton", _tr("恢复 ◆%d" % market.state.recovery_cost, "RECOVER ◆%d" % market.state.recovery_cost), recover_company) recover.position = Vector2(size.x - 220, size.y - 62) recover.size = Vector2(204, 48) display_root.add_child(recover) return var rally := _action_button("P97CommitLineButton", _tr("列阵 %d/%d" % [pending_line_ids.size(), market.state.line_capacity], "RALLY %d/%d" % [pending_line_ids.size(), market.state.line_capacity]), commit_pending_line) rally.position = Vector2(size.x - 196, size.y - 62) rally.size = Vector2(180, 48) display_root.add_child(rally) if not focused_company_id.is_empty(): var dismiss := _secondary_button("P97DismissPreviewButton", _tr("解雇", "DISMISS"), preview_focused_dismissal) dismiss.position = Vector2(size.x - 320, size.y - 62) dismiss.size = Vector2(112, 48) display_root.add_child(dismiss) func _market_context_text() -> String: if not market.state.pending_dismissal.is_empty(): return _tr("解雇 %s?装备会返回仓库,返还 ◆%d。" % [_recruit_name(String(market.state.pending_dismissal.recruit_id)), int(market.state.pending_dismissal.rebate)], "DISMISS %s? Gear returns to inventory. Refund ◆%d." % [_recruit_name(String(market.state.pending_dismissal.recruit_id)), int(market.state.pending_dismissal.rebate)]) if market.state.recovery_required: return _tr("队员倒下。支付恢复费用后可重新选择路线。", "RECRUITS ARE DOWN. Recover before choosing another route.") if not selected_inventory_instance.is_empty(): var catalog_id := String(market.state.inventory[selected_inventory_instance].catalog_id) return _tr("已购买 %s。点击一名兼容队员装备。" % _equipment_name(catalog_id), "%s PURCHASED. Tap a compatible Recruit to equip." % _equipment_name(catalog_id)) if not market.state.focused_recruit_offer.is_empty(): var recruit: Dictionary = content.recruits[market.state.focused_recruit_offer] var profession: Dictionary = _profession_record(String(recruit.profession_id)) return "%s · Lv.%d · %dHP · ◆%d\n%s / %s" % [_recruit_name(String(recruit.id)), int(recruit.level), int(recruit.current_hp), int(recruit.list_price), _tr(String(profession.get("strength_zh", "")), String(profession.get("strength_en", ""))), _tr(String(profession.get("limit_zh", "")), String(profession.get("limit_en", "")))] if not market.state.focused_equipment_offer.is_empty(): var offer: Dictionary = content.equipment_offers[market.state.focused_equipment_offer] var item: Dictionary = content.equipment[offer.catalog_id] return "%s · ◆%d\n%s / %s" % [_equipment_name(String(offer.catalog_id)), int(offer.list_price), _tr(String(item.benefit_zh), String(item.benefit_en)), _tr(String(item.cost_zh), String(item.cost_en))] if not market.state.focused_route_offer.is_empty(): var route: Dictionary = content.routes[market.state.focused_route_offer] return "%s · %s · ◆+%d\n%s" % [_tr(String(route.name_zh), String(route.name_en)), _tr(["稳妥", "压力", "精英"][int(route.risk)], ["STEADY", "PRESSURED", "ELITE"][int(route.risk)]), int(route.reward), _tr(String(route.pressure_zh), String(route.pressure_en))] if not focused_company_id.is_empty(): var record: Dictionary = market.state.recruit_records[focused_company_id] return _tr("%s · Lv.%d · %dHP。再次点击可加入/移出战线。" % [_recruit_name(focused_company_id), int(record.level), int(market.state.member_condition[focused_company_id])], "%s · Lv.%d · %dHP. Tap again to add/remove from the Line." % [_recruit_name(focused_company_id), int(record.level), int(market.state.member_condition[focused_company_id])]) return _tr("点击站立队员、装备台或远征门查看;再次点击执行。", "Tap a standing Recruit, gear pedestal or route gate; tap again to commit.") func _render_relay_threshold() -> void: var glow := ColorRect.new() glow.name = "P97RelayGlow" glow.position = Vector2(size.x * 0.58, 50) glow.size = Vector2(size.x * 0.42, size.y - 50) glow.color = Color(0.24, 0.86, 0.82, 0.18) glow.mouse_filter = Control.MOUSE_FILTER_IGNORE display_root.add_child(glow) for index in range(market.state.company.size()): var recruit_id: String = market.state.company[index] var x := 28.0 + float(index) * minf(78.0, (size.x * 0.52 - 40.0) / maxf(1.0, float(market.state.company.size()))) var actor := _add_actor(_recruit_rig(recruit_id), Vector2(x, size.y * 0.48), 92, "P97RelayCompany_%s" % recruit_id, true, "victory") actor.self_modulate = _recruit_color(recruit_id) var title := _outlined_label(_tr("风晶中继站已修复", "WINDGLASS RELAY RESTORED"), 24, GOLD_LIGHT) title.name = "P97RelayTitle" title.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART title.position = Vector2(size.x * 0.53, size.y * 0.24) title.size = Vector2(size.x * 0.45 - 18.0, 78) title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER display_root.add_child(title) var body := _outlined_label(_tr("第 8 集市已保存\n队伍、预备队、装备、HP 与路线记录全部保留", "MARKET 8 SAVED\nCompany, Reserve, gear, HP and route history preserved"), 13, WHITE) body.name = "P97RelayBody" body.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART body.position = Vector2(size.x * 0.53, size.y * 0.47) body.size = Vector2(size.x * 0.45 - 18.0, 104) body.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER display_root.add_child(body) func _render_battle() -> void: _add_background(WINDGLASS_BACKGROUND) _add_battle_hud() for recruit_id in battle.state.member_ids: _add_battle_actor(recruit_id, false) for enemy_id in battle.state.enemy_ids: _add_battle_actor(enemy_id, true) _update_battle_visuals() if battle_result_visible: _render_battle_result() func _add_battle_hud() -> void: var route: Dictionary = content.routes[battle.state.route_id] var context := _outlined_label(_tr(String(route.name_zh), String(route.name_en)), 15, WHITE) context.position = Vector2(16, 6) context.size = Vector2(size.x * 0.48, 42) display_root.add_child(context) var pause := _secondary_button("P97PauseButton", "▶" if battle_paused else "Ⅱ", toggle_battle_pause) pause.position = Vector2(size.x - 112, 6) pause.size = Vector2(48, 44) display_root.add_child(pause) var speed := _secondary_button("P97SpeedButton", "%dx" % battle_speed, toggle_battle_speed) speed.position = Vector2(size.x - 58, 6) speed.size = Vector2(50, 44) display_root.add_child(speed) func _add_battle_actor(actor_id: String, enemy: bool) -> void: var rig := "Riftblade" if enemy else _recruit_rig(actor_id) var actor_size := clampf(size.y * 0.28, 100.0, 148.0) if enemy: actor_size *= _enemy_visual_scale(actor_id) var actor := _add_actor(rig, Vector2.ZERO, actor_size, "P97BattleActor_%s" % actor_id, false) actor.self_modulate = _enemy_color(actor_id) if enemy else _recruit_color(actor_id) actor.set_meta("enemy", enemy) battle_actors[actor_id] = actor var bar := ColorRect.new() bar.name = "P97BattleHP_%s" % actor_id bar.size = Vector2(clampf(actor_size * 0.62, 52.0, 72.0), 8) bar.color = Color(0.02, 0.04, 0.07, 0.94) bar.mouse_filter = Control.MOUSE_FILTER_IGNORE var fill := ColorRect.new() fill.name = "Fill" fill.position = Vector2(1, 1) fill.size = Vector2(bar.size.x - 2.0, bar.size.y - 2.0) fill.color = CORAL if enemy else JADE fill.mouse_filter = Control.MOUSE_FILTER_IGNORE bar.add_child(fill) bar.set_meta("fill", fill) bar.set_meta("max_hp", int(battle.state.enemy_max_hp[actor_id]) if enemy else int(battle.state.member_max_hp[actor_id])) display_root.add_child(bar) battle_hp_bars[actor_id] = bar var name_label := _outlined_label(_battle_actor_name(actor_id), 8, WHITE) name_label.name = "P97BattleName_%s" % actor_id name_label.size = Vector2(maxf(86.0, actor_size + 12.0), 26) name_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER display_root.add_child(name_label) actor.set_meta("name_label", name_label) func _update_battle_visuals() -> void: if not battle_active: return for actor_id in battle_actors: var actor: TextureRect = battle_actors[actor_id] var enemy: bool = actor_id in battle.state.enemy_ids var hp := int(battle.state.enemy_hp[actor_id]) if enemy else int(battle.state.member_hp[actor_id]) var position_value := int(battle.state.enemy_positions[actor_id]) if enemy else int(battle.state.member_positions[actor_id]) var x := _battle_x(position_value) var y := size.y * (0.44 if enemy else 0.51) + (_enemy_visual_y_offset(actor_id) if enemy else 0.0) actor.position = Vector2(x - actor.size.x * 0.5, y) var action: Dictionary = battle_actions.get(actor_id, {}) var state_name := String(action.get("state", "idle" if battle.state.terminal else "advance")) if hp <= 0: state_name = "death" if enemy else "idle" actor.texture = animation.texture("Riftblade" if enemy else _recruit_rig(actor_id), state_name, world_clock) actor.modulate.a = 0.28 if hp <= 0 else 1.0 actor.visible = hp > 0 or not battle.state.terminal var bar: ColorRect = battle_hp_bars[actor_id] bar.position = Vector2(actor.position.x + (actor.size.x - bar.size.x) * 0.5, actor.position.y - 8) var fill: ColorRect = bar.get_meta("fill") var ratio := clampf(float(hp) / maxf(1.0, float(bar.get_meta("max_hp"))), 0.0, 1.0) fill.size.x = maxf(0.0, (bar.size.x - 2.0) * ratio) var name_label: Label = actor.get_meta("name_label") name_label.position = Vector2(actor.position.x + (actor.size.x - name_label.size.x) * 0.5, actor.position.y + actor.size.y - 6) bar.visible = hp > 0 and not battle_result_visible name_label.visible = hp > 0 func _handle_battle_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_enemy_id", event.get("target_member_id", ""))) var healing := String(event.get("action_tag", "")) == "restore" battle_actions[source] = {"state": "lantern_heal" if healing else "draw_release" if _recruit_rig(source) == "Farstring" else "basic_attack"} _spawn_number(int(event.get("delta", 0)), target, JADE if healing else CORAL) if healing: _spawn_vfx(target, HEAL_VFX) if not healing and event.has("target_enemy_id"): _spawn_projectile(source, target) elif kind == "enemy_action": var enemy := String(event.get("source_enemy_id", "")) var target := String(event.get("target_member_id", "")) battle_actions[enemy] = {"state": "attack"} _spawn_number(int(event.get("delta", 0)), target, CORAL) _spawn_vfx(target, HIT_VFX) elif kind in ["equipment_trigger", "profession_rule", "enemy_rule"]: var source_id := String(event.get("member_id", event.get("enemy_id", ""))) var word := _battle_event_word(String(event.get("rule", event.get("catalog_id", "")))) _spawn_word(source_id, word, GOLD_LIGHT if kind != "enemy_rule" else CORAL) func _render_battle_result() -> void: var band := ColorRect.new() band.name = "P97BattleResultBand" band.position = Vector2(0, size.y - 72) band.size = Vector2(size.x, 72) band.color = Color(0.015, 0.035, 0.075, 0.94) display_root.add_child(band) var victory: bool = battle.state.outcome == "victory" var route: Dictionary = content.routes[battle.state.route_id] var result := _outlined_label(_tr("胜利 ◆ +%d" % int(route.reward), "VICTORY ◆ +%d" % int(route.reward)) if victory else _tr("队伍战败 · 无奖励", "COMPANY DEFEATED · NO REWARD"), 22, GOLD_LIGHT if victory else CORAL) result.position = Vector2(18, size.y - 66) result.size = Vector2(size.x * 0.62, 56) display_root.add_child(result) var return_button := _action_button("P97SettleButton", _tr("返回集市", "RETURN TO MARKET"), settle_and_return) return_button.position = Vector2(size.x - 230, size.y - 62) return_button.size = Vector2(212, 52) display_root.add_child(return_button) func _update_battle_control_labels() -> void: var pause := find_child("P97PauseButton", true, false) as Button var speed := find_child("P97SpeedButton", true, false) as Button if pause != null: pause.text = "▶" if battle_paused else "Ⅱ" if speed != null: speed.text = "%dx" % battle_speed func _spawn_number(delta_value: int, actor_id: String, color: Color) -> void: var actor: TextureRect = battle_actors.get(actor_id) if actor == null or delta_value == 0: return var label := _outlined_label(("+" if delta_value > 0 else "") + str(delta_value), 21, color) label.name = "P97FloatValue" label.position = actor.position + Vector2(actor.size.x * 0.30, -18) label.size = Vector2(90, 36) label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER label.z_index = 30 display_root.add_child(label) var tween := label.create_tween() tween.set_parallel(true) tween.tween_property(label, "position:y", label.position.y - (18 if reduced_motion else 42), 0.55) tween.tween_property(label, "modulate:a", 0.0, 0.55).set_delay(0.12) tween.chain().tween_callback(label.queue_free) func _spawn_projectile(source_id: String, target_id: String) -> void: var source: TextureRect = battle_actors.get(source_id) var target: TextureRect = battle_actors.get(target_id) if source == null or target == null: return var projectile := _outlined_label("➤", 24, CYAN) projectile.name = "P97Projectile" projectile.position = source.position + source.size * 0.45 projectile.size = Vector2(42, 32) projectile.z_index = 25 display_root.add_child(projectile) var tween := projectile.create_tween() tween.tween_property(projectile, "position", target.position + target.size * 0.42, 0.16 if reduced_motion else 0.38) tween.tween_callback(projectile.queue_free) func _spawn_vfx(actor_id: String, texture: Texture2D) -> void: var actor: TextureRect = battle_actors.get(actor_id) if actor == null: return var vfx := TextureRect.new() vfx.name = "P97HitVFX" vfx.texture = texture vfx.expand_mode = TextureRect.EXPAND_IGNORE_SIZE vfx.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED vfx.custom_minimum_size = Vector2.ZERO vfx.position = actor.position + actor.size * 0.32 vfx.size = actor.size * 0.36 vfx.z_index = 24 display_root.add_child(vfx) var tween := vfx.create_tween() tween.tween_property(vfx, "modulate:a", 0.0, 0.30) tween.tween_callback(vfx.queue_free) func _spawn_word(actor_id: String, value: String, color: Color) -> void: var actor: TextureRect = battle_actors.get(actor_id) if actor == null or value.is_empty(): return var label := _outlined_label(value, 11, color) label.name = "P97EventWord" label.position = actor.position + Vector2(-25, -32) label.size = Vector2(actor.size.x + 50, 30) label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER label.z_index = 29 display_root.add_child(label) var tween := label.create_tween() tween.tween_property(label, "modulate:a", 0.0, 0.62).set_delay(0.18) tween.tween_callback(label.queue_free) func _render_rotate_notice() -> void: var background := ColorRect.new() background.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) background.color = INK display_root.add_child(background) var message := _outlined_label(_tr("请将手机旋转为横屏\n队伍会在同一条战线上行动", "ROTATE TO LANDSCAPE\nThe Company fights on one continuous line"), 20, WHITE) message.name = "P97RotateNotice" message.position = Vector2(24, size.y * 0.36) message.size = Vector2(size.x - 48, 130) message.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER display_root.add_child(message) func _render_missing_entry() -> void: _add_background(HALL_BACKGROUND) var label := _outlined_label(_tr("需要已完成的裂潮存档", "A SETTLED RIFT TIDE SAVE IS REQUIRED"), 18, WHITE) label.position = Vector2(40, size.y * 0.42) label.size = Vector2(size.x - 80, 72) label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER display_root.add_child(label) func _add_background(texture: Texture2D) -> void: var background := TextureRect.new() background.name = "P97WorldBackground" background.texture = texture background.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) background.expand_mode = TextureRect.EXPAND_IGNORE_SIZE background.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_COVERED background.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS background.mouse_filter = Control.MOUSE_FILTER_IGNORE display_root.add_child(background) func _add_actor(rig: String, at: Vector2, actor_height: float, node_name: String, ambient: bool, forced_state := "idle") -> TextureRect: var actor := TextureRect.new() actor.name = node_name actor.texture = animation.texture(rig, forced_state, world_clock) actor.expand_mode = TextureRect.EXPAND_IGNORE_SIZE actor.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED actor.custom_minimum_size = Vector2.ZERO actor.position = at actor.size = Vector2(actor_height, actor_height) actor.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS actor.mouse_filter = Control.MOUSE_FILTER_IGNORE actor.set_meta("rig", rig) actor.set_meta("animation_state", forced_state) display_root.add_child(actor) if ambient: ambient_actors[node_name] = actor return actor func _update_ambient_actors() -> void: for actor_name in ambient_actors: var actor: TextureRect = ambient_actors[actor_name] if is_instance_valid(actor): actor.texture = animation.texture(String(actor.get_meta("rig", "Hearthguard")), String(actor.get_meta("animation_state", "idle")), world_clock) func _add_ground_ring(center: Vector2, diameter: float, color: Color, node_name: String) -> void: var marker := ColorRect.new() marker.name = node_name marker.position = center - Vector2(diameter * 0.5, 5) marker.size = Vector2(diameter, 3) marker.color = color marker.mouse_filter = Control.MOUSE_FILTER_IGNORE display_root.add_child(marker) func _actor_target(node_name: String, at: Vector2, target_size: Vector2, callback: Callable) -> Button: var button := Button.new() button.name = node_name button.position = at button.size = Vector2(maxf(48.0, target_size.x), maxf(48.0, target_size.y)) button.flat = true button.focus_mode = Control.FOCUS_ALL button.add_theme_stylebox_override("normal", StyleBoxEmpty.new()) button.add_theme_stylebox_override("hover", _flat_style(Color(1, 1, 1, 0.04), WHITE, 2, 4)) button.add_theme_stylebox_override("pressed", _flat_style(Color(GOLD, 0.08), GOLD, 2, 4)) button.add_theme_stylebox_override("focus", _flat_style(Color(1, 1, 1, 0.02), GOLD_LIGHT, 3, 4)) button.pressed.connect(callback) return button func _action_button(node_name: String, value: String, callback: Callable) -> Button: var button := Button.new() button.name = node_name button.text = value button.focus_mode = Control.FOCUS_ALL button.add_theme_font_size_override("font_size", _scaled(14)) button.add_theme_color_override("font_color", INK) button.add_theme_stylebox_override("normal", _flat_style(GOLD, GOLD_LIGHT, 2, 4)) button.add_theme_stylebox_override("hover", _flat_style(GOLD_LIGHT, WHITE, 2, 4)) button.add_theme_stylebox_override("pressed", _flat_style(Color("#d9a947"), GOLD_LIGHT, 2, 4)) button.add_theme_stylebox_override("focus", _flat_style(GOLD, WHITE, 3, 4)) button.pressed.connect(callback) return button func _secondary_button(node_name: String, value: String, callback: Callable) -> Button: var button := Button.new() button.name = node_name button.text = value button.focus_mode = Control.FOCUS_ALL button.add_theme_font_size_override("font_size", _scaled(13)) button.add_theme_color_override("font_color", WHITE) button.add_theme_stylebox_override("normal", _flat_style(Color(0.03, 0.08, 0.15, 0.88), CYAN, 2, 4)) button.add_theme_stylebox_override("hover", _flat_style(Color(0.06, 0.15, 0.23, 0.96), WHITE, 2, 4)) button.add_theme_stylebox_override("pressed", _flat_style(Color(0.02, 0.05, 0.10, 0.96), GOLD, 2, 4)) button.add_theme_stylebox_override("focus", _flat_style(Color(0.03, 0.08, 0.15, 0.88), GOLD_LIGHT, 3, 4)) button.pressed.connect(callback) return button func _outlined_label(value: String, font_size: int, color: Color) -> Label: var label := Label.new() label.text = value label.add_theme_font_size_override("font_size", _scaled(font_size)) label.add_theme_color_override("font_color", color) label.add_theme_color_override("font_outline_color", Color(0.015, 0.035, 0.075, 0.96)) label.add_theme_constant_override("outline_size", 4) label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER label.mouse_filter = Control.MOUSE_FILTER_IGNORE return label func _flat_style(fill: Color, border: Color, border_width: int, radius: int) -> StyleBoxFlat: var style := StyleBoxFlat.new() style.bg_color = fill style.border_color = border style.set_border_width_all(border_width) style.set_corner_radius_all(radius) style.content_margin_left = 7 style.content_margin_right = 7 style.content_margin_top = 4 style.content_margin_bottom = 4 return style func _battle_x(position_value: int) -> float: return clampf(64.0 + float(position_value + 60) * ((size.x - 128.0) / 190.0), 54.0, size.x - 54.0) func _recruit_rig(recruit_id: String) -> String: var profession_id := String(_recruit_record(recruit_id).get("profession_id", "PF-B01")) if content.professions.has(profession_id): return String(content.professions[profession_id].visual_rig) return {"PF-B01": "Hearthguard", "PF-B04": "Wayrunner", "PF-B05": "Farstring", "PF-B07": "Kindhand"}.get(profession_id, "Hearthguard") func _profession_record(profession_id: String) -> Dictionary: if content.professions.has(profession_id): return content.professions[profession_id] return {"name_zh": {"PF-B01": "守卫", "PF-B04": "游侠", "PF-B05": "弓手", "PF-B07": "治疗"}.get(profession_id, profession_id), "name_en": {"PF-B01": "Hearthguard", "PF-B04": "Wayrunner", "PF-B05": "Farstring", "PF-B07": "Kindhand"}.get(profession_id, profession_id), "strength_zh": "稳定的既有职责", "strength_en": "Established reliable role", "limit_zh": "需要队友配合", "limit_en": "Needs team support"} func _profession_name(recruit_id: String) -> String: var profession_id := String(_recruit_record(recruit_id).get("profession_id", "")) var row := _profession_record(profession_id) return _tr(String(row.name_zh), String(row.name_en)) func _recruit_name(recruit_id: String) -> String: var record: Dictionary = _recruit_record(recruit_id) return _tr(String(record.get("name_zh", recruit_id)), String(record.get("name_en", recruit_id))) func _recruit_record(recruit_id: String) -> Dictionary: if market != null and market.state.recruit_records.has(recruit_id): return market.state.recruit_records[recruit_id] return content.recruits.get(recruit_id, {}) func _equipment_name(catalog_id: String) -> String: if content.equipment.has(catalog_id): return _tr(String(content.equipment[catalog_id].name_zh), String(content.equipment[catalog_id].name_en)) return {"EQ-002": _tr("定步护胫", "Frontpin Greaves"), "EQ-012": _tr("开线绳结", "Open-Line Cord")}.get(catalog_id, catalog_id) func _equipment_texture(catalog_id: String) -> Texture2D: return OPEN_LINE_TEXTURE if catalog_id in ["EQ-012", "EQ-023", "EQ-036"] else FRONTPIN_TEXTURE func _equipment_color(catalog_id: String) -> Color: return {"EQ-021": CYAN, "EQ-023": GOLD_LIGHT, "EQ-028": CORAL, "EQ-036": VIOLET}.get(catalog_id, WHITE) func _recruit_color(recruit_id: String) -> Color: var profession_id := String(_recruit_record(recruit_id).get("profession_id", "")) return {"PF-B02": CORAL, "PF-B03": JADE, "PF-B06": VIOLET, "PF-B08": Color("#ef9cbc"), "PF-B09": Color("#78d9d0"), "PF-B11": GOLD_LIGHT}.get(profession_id, WHITE) func _enemy_color(enemy_id: String) -> Color: return {"EN-R1-01": Color("#d2ecf3"), "EN-R1-02": CYAN, "EN-R1-03": Color("#e7b779"), "EN-R1-05": Color("#a9c0db"), "EN-R1-08": JADE, "EN-R1-11": VIOLET}.get(enemy_id, WHITE) func _enemy_visual_scale(enemy_id: String) -> float: return {"EN-R1-01": 0.90, "EN-R1-02": 0.82, "EN-R1-03": 1.08, "EN-R1-05": 1.02, "EN-R1-08": 0.88, "EN-R1-11": 1.10}.get(enemy_id, 1.0) func _enemy_visual_y_offset(enemy_id: String) -> float: return {"EN-R1-02": 9.0, "EN-R1-03": -4.0, "EN-R1-08": 7.0, "EN-R1-11": -5.0}.get(enemy_id, 0.0) func _risk_color(risk: int, focused: bool) -> Color: var base: Color = [Color(0.15, 0.46, 0.34, 0.88), Color(0.56, 0.38, 0.12, 0.90), Color(0.48, 0.16, 0.24, 0.92)][risk] return base.lightened(0.22) if focused else base func _battle_actor_name(actor_id: String) -> String: if content.enemies.has(actor_id): return _tr(String(content.enemies[actor_id].name_zh), String(content.enemies[actor_id].name_en)) return _profession_name(actor_id) func _battle_event_word(event_id: String) -> String: var words := { "first_projectile_reflect": [_tr("镜反", "REFLECT")], "front_pushed": [_tr("推位", "PUSH")], "growth": [_tr("生长", "GROW")], "command_interrupt": [_tr("打断", "INTERRUPT")], "needle_post_fired": [_tr("针塔", "NEEDLE")], "windup_delayed": [_tr("迟滞", "DELAY")], "EQ-021": [_tr("识破蓄力", "WIND-UP READ")], "EQ-023": [_tr("后排锁定", "REAR LOCK")], "EQ-028": [_tr("破护", "GUARD BREAK")], "EQ-036": [_tr("双目标", "TWIN MARK")], } if words.has(event_id): return String(words[event_id][0]) return event_id.replace("_", " ").to_upper().left(14) func _tr(zh: String, en: String) -> String: return en if locale == "en" else zh func _scaled(value: int) -> int: return maxi(9, int(round(float(value) * text_scale)))