class_name P910RuntimeController extends "res://p9_9/runtime_controller.gd" const P910Content = preload("res://content/p9_10_content.gd") const P910State = preload("res://p9_10/contract_state.gd") const P910BossService = preload("res://p9_10/boss_service.gd") const P910BattleService = preload("res://p9_10/battle_service.gd") const P910BossBattleService = preload("res://p9_10/boss_battle_service.gd") const P910SaveService = preload("res://p9_10/save_service.gd") var boss_battle = P910BossBattleService.new() var boss_battle_active := false var focused_equipment_id := "" var focused_equipment_target := "" var focused_company_actor := "" var focused_boss_plan := "" var last_boss_direction := "front_to_rear" var last_boss_rule := "" func _bootstrap_market() -> void: if entry_snapshot.is_empty(): last_code = "entry_snapshot_required" _queue_render() return market = P910BossService.new() var restored := false if String(entry_snapshot.get("state_kind", "")) == "p9_10_contract": restored = market.restore_snapshot(entry_snapshot) elif String(entry_snapshot.get("state_kind", "")) == "p99_run": market = P910BossService.new(entry_snapshot) restored = market.state.invariants_hold() if not restored: market = null last_code = "p910_snapshot_invalid" _queue_render() return content = P910Content.snapshot(market.state.selected_charter_claim_id) battle = P910BattleService.new(market.state.selected_charter_claim_id) boss_battle = P910BossBattleService.new() saves = P910SaveService.new() pending_line_ids.assign(market.state.deployed_recruit_ids) focused_hire_id = "" focused_route_id = "" focused_equipment_id = "" focused_equipment_target = "" focused_company_actor = "" focused_boss_plan = "" battle_active = false boss_battle_active = false battle_result_visible = false last_code = "p910_ready" _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_hire_actor(recruit_id: String) -> void: if not _consume_interaction() or not _market_ready(): return if focused_hire_id != recruit_id: focused_hire_id = recruit_id last_code = "hire_inspected" _queue_render() return var result: Dictionary = market.hire(_operation("hire", recruit_id), recruit_id, market.state.revision) last_code = String(result.code) if result.ok: focused_hire_id = "" pending_line_ids.assign(market.state.deployed_recruit_ids) _save_stable() _queue_render() func tap_equipment_offer(offer_id: String) -> void: if not _consume_interaction() or not _market_ready(): return if offer_id not in market.state.available_equipment_offer_ids: return if focused_equipment_id != offer_id: focused_equipment_id = offer_id focused_equipment_target = "" last_code = "equipment_inspected" else: last_code = "equipment_target_required" _queue_render() func tap_company_actor(recruit_id: String) -> void: if not _consume_interaction() or not _market_ready() or recruit_id not in market.state.company: return if not focused_equipment_id.is_empty(): if focused_equipment_target != recruit_id: var preview: Dictionary = market.equipment_preview(focused_equipment_id, recruit_id) last_code = String(preview.code) if preview.ok: focused_equipment_target = recruit_id _queue_render() return var result: Dictionary = market.buy_and_equip(_operation("equip", recruit_id), focused_equipment_id, recruit_id, market.state.revision) last_code = String(result.code) if result.ok: focused_equipment_id = "" focused_equipment_target = "" _save_stable() _queue_render() return focused_company_actor = recruit_id last_code = "company_inspected" _queue_render() func commit_final_reorder() -> void: if not _consume_interaction() or not _market_ready() or market.state.current_market != 12 or market.state.final_order_confirmed: return var result: Dictionary if market.state.proposed_final_order.is_empty(): var proposed: Array[String] = market.state.deployed_recruit_ids.duplicate() proposed.reverse() result = market.propose_final_order(_operation("order", "propose"), proposed, market.state.revision) else: result = market.confirm_final_order(_operation("order", "confirm"), market.state.revision) last_code = String(result.code) if result.ok: pending_line_ids.assign(market.state.deployed_recruit_ids) _save_stable() _queue_render() func tap_route(route_id: String) -> void: if not _consume_interaction() or not _market_ready(): return if focused_route_id != route_id: focused_route_id = route_id last_code = "route_preview" _queue_render() return var result: Dictionary = market.commit_route(_operation("route", route_id), route_id, market.state.revision) last_code = String(result.code) if not result.ok: _queue_render() return _save_stable() battle = P910BattleService.new(market.state.selected_charter_claim_id) start_committed_battle() func recover_company() -> void: if not _consume_interaction() or market == null or battle_active: return var result: Dictionary if market.state.recovery_kind == "boss_defeat": result = market.recover_boss_defeat(_operation("recover", "boss")) else: result = market.recover_defeat(_operation("recover", "company")) last_code = String(result.code) if result.ok: _save_stable() _queue_render() func tap_boss_plan(plan: String) -> void: if not _consume_interaction() or market == null or market.state.phase != P910State.Phase.BOSS_MUSTER or market.state.recovery_required: return if focused_boss_plan != plan: focused_boss_plan = plan last_code = "boss_plan_inspected" _queue_render() return var result: Dictionary = market.commit_boss_plan(_operation("boss-plan", plan), plan, market.state.revision) last_code = String(result.code) if not result.ok: _queue_render() return _save_stable() start_committed_boss() func return_to_boss_muster() -> void: if not _consume_interaction() or market == null or market.state.phase != P910State.Phase.BOSS_ENCOUNTER or battle_active: return var result: Dictionary = market.return_to_boss_muster(_operation("boss", "back")) last_code = String(result.code) if result.ok: focused_boss_plan = "" _save_stable() _queue_render() func start_committed_boss() -> void: boss_battle = P910BossBattleService.new() var result: Dictionary = boss_battle.start_from_contract(market.state) last_code = String(result.code) if not result.ok: _queue_render() return boss_battle_active = 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() last_boss_direction = boss_battle.state.gust_direction last_boss_rule = "boss_started" _queue_render() func advance_battle_ticks(ticks: int) -> Dictionary: if not boss_battle_active: return super.advance_battle_ticks(ticks) if not battle_active or battle_result_visible: return {"ok": false, "code": "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 _consume_interaction() or not battle_active: return var result: Dictionary if boss_battle_active: if not boss_battle.state.terminal: return result = market.settle_boss(_operation("boss-settle", boss_battle.state.plan), boss_battle.state.snapshot()) else: if not battle.state.terminal: return result = 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 boss_battle_active = false battle_result_visible = false focused_route_id = "" focused_boss_plan = "" pending_line_ids.assign(market.state.deployed_recruit_ids) _save_stable() _queue_render() func close_contract() -> void: if not _consume_interaction() or market == null or market.state.phase != P910State.Phase.CONTRACT_RESULT: return var result: Dictionary = market.close_contract(_operation("close", "rg01-c02")) last_code = String(result.code) if result.ok: _save_stable() _queue_render() func _market_ready() -> bool: return market != null and not battle_active and market.state.phase == P910State.Phase.MARKET 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 "p910-ui-%s-%d-%d-%s" % [kind, market.state.revision, operation_index, target] 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 and boss_battle_active: _render_boss_battle() elif battle_active: _render_battle() elif market.state.phase == P910State.Phase.BOSS_MUSTER: _render_boss_muster() elif market.state.phase == P910State.Phase.CONTRACT_RESULT: _render_contract_result() elif market.state.phase == P910State.Phase.RUN_CLOSED: _render_persistent_result() else: _render_market() func _render_market() -> void: _add_background(WINDGLASS_BACKGROUND) _add_market_header() if market.state.recovery_required: _render_recovery_ground() _render_market_context() return _render_market_offers() _render_company_line() _render_route_gates() _render_market_context() func _render_battle() -> void: super._render_battle() _compact_battle_name_labels() func _compact_battle_name_labels() -> void: var compact := size.x < 900.0 or text_scale > 1.0 var width := 42.0 if compact else 104.0 var font_size := 7 if compact else 8 for node in display_root.find_children("P97BattleName_*", "Label", true, false): var label := node as Label if label == null: continue label.size.x = width label.add_theme_font_size_override("font_size", _scaled(font_size)) label.clip_text = true label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS var actor_id := String(label.name).trim_prefix("P97BattleName_") var actor: TextureRect = battle_actors.get(actor_id) if actor != null: label.position = Vector2(actor.position.x + (actor.size.x - label.size.x) * 0.5, actor.position.y + actor.size.y - 6) label.position.y += -6.0 if bool(actor.get_meta("enemy", false)) else 4.0 func _add_market_header() -> void: var shade := ColorRect.new() shade.name = "P910MarketHeader" shade.position = Vector2.ZERO shade.size = Vector2(size.x, 50) 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(_tr("第二契约 · 风廊 %d" % market.state.current_market, "CONTRACT 2 · GALEWAY %d" % market.state.current_market), 15, GOLD_LIGHT) title.name = "P910MarketTitle" title.position = Vector2(14, 3) title.size = Vector2(size.x * 0.38, 43) title.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS display_root.add_child(title) var values := _outlined_label(_tr("◆%d 队伍 %d/%d 胜利 %d/12" % [market.state.coin, market.state.company.size(), market.state.line_capacity, market.state.cleared_rounds], "◆%d COMPANY %d/%d CLEARS %d/12" % [market.state.coin, market.state.company.size(), market.state.line_capacity, market.state.cleared_rounds]), 12, WHITE) values.name = "P910MarketValues" values.position = Vector2(size.x * 0.38, 3) values.size = Vector2(size.x * 0.60 - 12, 43) values.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT values.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS display_root.add_child(values) func _render_market_offers() -> void: var right_edge := size.x - 224.0 var recruit_ids: Array = market.state.available_recruit_offer_ids var slot_width := minf(116.0, maxf(82.0, (right_edge - 24.0) / maxf(1.0, float(recruit_ids.size() + 2)))) for index in recruit_ids.size(): var recruit_id := String(recruit_ids[index]) var row: Dictionary = content.recruit_offers[recruit_id] var actor_size := clampf(slot_width * 0.68, 54.0, 76.0) var x := 18.0 + float(index) * slot_width var y := 62.0 var actor := _add_actor(String(content.professions[row.profession_id].visual_rig), Vector2(x, y), actor_size, "P910HireActor_%s" % recruit_id, true) actor.self_modulate = _p910_profession_color(String(row.profession_id)) display_root.add_child(_actor_target("P910HireTarget_%s" % recruit_id, Vector2(x - 4, y - 4), Vector2(actor_size + 8, 102), tap_hire_actor.bind(recruit_id))) var label := _outlined_label("%s\n◆%d" % [_tr(String(row.name_zh), String(row.name_en)), int(row.list_price)], 8, GOLD_LIGHT if focused_hire_id == recruit_id else WHITE) label.position = Vector2(x - 18, y + actor_size - 2) label.size = Vector2(actor_size + 36, 42) label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS display_root.add_child(label) var equipment_ids: Array = market.state.available_equipment_offer_ids for index in equipment_ids.size(): var offer_id := String(equipment_ids[index]) var row: Dictionary = content.equipment_offers[offer_id] var x := 26.0 + float(recruit_ids.size()) * slot_width + float(index) * 80.0 var pedestal := TextureRect.new() pedestal.name = "P910EquipmentObject_%s" % offer_id pedestal.texture = _equipment_texture(String(row.catalog_id)) pedestal.expand_mode = TextureRect.EXPAND_IGNORE_SIZE pedestal.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED pedestal.position = Vector2(x, 70) pedestal.size = Vector2(54, 54) pedestal.self_modulate = GOLD_LIGHT if focused_equipment_id == offer_id else _equipment_color(String(row.catalog_id)) pedestal.mouse_filter = Control.MOUSE_FILTER_IGNORE display_root.add_child(pedestal) display_root.add_child(_actor_target("P910EquipmentTarget_%s" % offer_id, Vector2(x - 4, 64), Vector2(64, 96), tap_equipment_offer.bind(offer_id))) var label := _outlined_label("%s\n◆%d" % [_tr(String(row.name_zh), String(row.name_en)), int(row.list_price)], 8, WHITE) label.position = Vector2(x - 18, 120) label.size = Vector2(90, 40) label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS display_root.add_child(label) func _render_company_line() -> void: var world_bottom := size.y - 58.0 var lane_width := size.x - 228.0 var count: int = market.state.company.size() var actor_size := clampf(minf(size.y * 0.18, (lane_width - 20.0) / maxf(1.0, float(count))), 48.0, 68.0) var spacing := minf(actor_size + 2.0, (lane_width - actor_size - 18.0) / maxf(1.0, float(count - 1))) var y := world_bottom - actor_size - 12.0 for index in count: var recruit_id: String = market.state.company[index] var x := 12.0 + float(index) * spacing var highlighted := recruit_id == focused_company_actor or recruit_id == focused_equipment_target _add_ground_ring(Vector2(x + actor_size * 0.5, y + actor_size - 3), actor_size * 0.72, GOLD_LIGHT if highlighted else CYAN, "P910CompanyRing_%s" % recruit_id) var actor := _add_actor(_recruit_rig(recruit_id), Vector2(x, y), actor_size, "P910CompanyActor_%s" % recruit_id, true) actor.self_modulate = _recruit_color(recruit_id) display_root.add_child(_actor_target("P910CompanyTarget_%s" % recruit_id, Vector2(x - 3, y - 4), Vector2(actor_size + 6, actor_size + 20), tap_company_actor.bind(recruit_id))) var hp := _outlined_label("%d" % int(market.state.member_condition[recruit_id]), 8, WHITE) hp.position = Vector2(x, y + actor_size - 5) hp.size = Vector2(actor_size, 22) hp.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER display_root.add_child(hp) var instance_id := String(market.state.equipped_by_recruit.get(recruit_id, "")) if not instance_id.is_empty(): var dot := _outlined_label("◆", 10, GOLD_LIGHT) dot.position = Vector2(x + actor_size - 14, y + 2) dot.size = Vector2(18, 18) display_root.add_child(dot) if market.state.current_market == 12 and not market.state.final_order_confirmed: var reorder_text := _tr("确认换序", "CONFIRM ORDER") if not market.state.proposed_final_order.is_empty() else _tr("预览换序", "PREVIEW ORDER") var reorder := _secondary_button("P910FinalReorderButton", reorder_text, commit_final_reorder) reorder.position = Vector2(maxf(12, lane_width - 140), 164) reorder.size = Vector2(132, 48) display_root.add_child(reorder) func _render_route_gates() -> void: var routes: Array = market.state.available_battle_offer_ids for index in routes.size(): var route_id := String(routes[index]) var row: Dictionary = content.routes[route_id] var gate := ColorRect.new() gate.name = "P910RouteGate_%s" % route_id gate.position = Vector2(size.x - 214, 58 + float(index) * 82) gate.size = Vector2(198, 70) gate.color = _risk_color(int(row.risk), focused_route_id == route_id) gate.mouse_filter = Control.MOUSE_FILTER_IGNORE display_root.add_child(gate) var label := _outlined_label("%s ◆+%d\n%s" % [_tr(["稳行", "急行", "险行"][int(row.risk)], ["SURVEY", "PRESS", "HAZARD"][int(row.risk)]), int(row.reward), _tr(String(row.pressure_zh), String(row.pressure_en))], 9, WHITE) label.position = gate.position + Vector2(8, 2) label.size = gate.size - Vector2(16, 4) label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART display_root.add_child(label) display_root.add_child(_actor_target("P910RouteTarget_%s" % route_id, gate.position, gate.size, tap_route.bind(route_id))) func _render_market_context() -> void: var ribbon := ColorRect.new() ribbon.name = "P910ContextRibbon" ribbon.position = Vector2(0, size.y - 58) ribbon.size = Vector2(size.x, 58) ribbon.color = Color(0.015, 0.035, 0.075, 0.94) ribbon.mouse_filter = Control.MOUSE_FILTER_IGNORE display_root.add_child(ribbon) var context := _market_context_text() var label := _outlined_label(context, 10, WHITE) label.name = "P910ContextText" label.position = Vector2(14, size.y - 55) label.size = Vector2(size.x - 28, 52) label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS display_root.add_child(label) func _market_context_text() -> String: if market.state.recovery_required: return _tr("队伍战败。支付 ◆%d 恢复后再选择同一集市路线。" % market.state.recovery_cost, "COMPANY DEFEATED. Pay ◆%d to recover, then choose again from the same Market." % market.state.recovery_cost) if not focused_equipment_id.is_empty(): var row: Dictionary = content.equipment_offers[focused_equipment_id] var target_text := _tr("再次点击一名队员确认替换。", "Tap a Recruit again to confirm replacement.") if not focused_equipment_target.is_empty() else _tr("点击一名队员查看替换前后。", "Tap a Recruit to preview the replacement.") return "%s · ◆%d · %s / %s %s" % [_tr(String(row.name_zh), String(row.name_en)), int(row.list_price), _tr(String(row.benefit_zh), String(row.benefit_en)), _tr(String(row.cost_zh), String(row.cost_en)), target_text] if not focused_hire_id.is_empty(): var row: Dictionary = content.recruit_offers[focused_hire_id] return "%s · ◆%d · %s / %s" % [_tr(String(row.name_zh), String(row.name_en)), int(row.list_price), _tr(String(row.benefit_zh), String(row.benefit_en)), _tr(String(row.cost_zh), String(row.cost_en))] if not focused_route_id.is_empty(): var preview: Dictionary = market.route_preview(focused_route_id) return _tr("再次点击进入:%s。奖励 ◆%d。" % [String(preview.pressure_zh), int(preview.reward)], "Tap again to enter: %s. Reward ◆%d." % [String(preview.pressure_en), int(preview.reward)]) if not focused_company_actor.is_empty(): var record: Dictionary = market.state.recruit_records[focused_company_actor] return "%s · %s · %d HP" % [_tr(String(record.get("name_zh", focused_company_actor)), String(record.get("name_en", focused_company_actor))), _profession_name(focused_company_actor), int(market.state.member_condition[focused_company_actor])] return _tr("点击人物、装备或路线查看;再次点击执行。战斗会自动进行。", "Tap a Recruit, gear object or route to inspect; tap again to act. Battles run automatically.") func _render_recovery_ground() -> void: var glow := ColorRect.new() glow.name = "P910RecoveryGround" glow.position = Vector2(size.x * 0.30, size.y * 0.25) glow.size = Vector2(size.x * 0.40, size.y * 0.42) glow.color = Color(0.18, 0.05, 0.09, 0.72) display_root.add_child(glow) var text := _outlined_label(_tr("整备倒下的队伍\n保持当前集市与所有报价\n◆%d" % market.state.recovery_cost, "RECOVER THE COMPANY\nKeep this Market and every offer\n◆%d" % market.state.recovery_cost), 16, WHITE) text.position = glow.position + Vector2(14, 4) text.size = glow.size - Vector2(28, 64) text.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER text.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART display_root.add_child(text) var recover := _action_button("P910RecoveryButton", _tr("支付并恢复", "PAY AND RECOVER"), recover_company) recover.position = Vector2(glow.position.x + glow.size.x * 0.5 - 92, glow.position.y + glow.size.y - 54) recover.size = Vector2(184, 50) recover.disabled = market.state.coin < market.state.recovery_cost display_root.add_child(recover) func _render_boss_muster() -> void: _add_background(WINDGLASS_BACKGROUND) _add_contract_header(_tr("风暴织机 · 集结", "GALE LOOM · MUSTER"), _tr("先看风向与弱轴,再选择计划", "Read the gust and weak spindle before choosing")) _render_gust_path(false) var boss := _add_actor("Riftblade", Vector2(size.x * 0.64, size.y * 0.22), minf(176.0, size.y * 0.46), "P910BossMusterActor", true) boss.self_modulate = Color("#b5e8ef") _add_boss_footprints(Vector2(size.x * 0.69, size.y * 0.69)) if market.state.recovery_required: _render_boss_recovery() return var plans: Array = market.state.boss_muster.plans for index in plans.size(): var row: Dictionary = plans[index] var plan_id := String(row.id) var target := ColorRect.new() target.name = "P910BossPlanGround_%s" % plan_id target.position = Vector2(18 + float(index) * 236, 72) target.size = Vector2(220, 102) target.color = Color(0.08, 0.16, 0.22, 0.90) if focused_boss_plan != plan_id else Color(0.34, 0.23, 0.08, 0.94) target.mouse_filter = Control.MOUSE_FILTER_IGNORE display_root.add_child(target) var label := _outlined_label("%s\n%s" % [_tr(String(row.name_zh), String(row.name_en)), _tr(String(row.summary_zh), String(row.summary_en))], 10, GOLD_LIGHT if focused_boss_plan == plan_id else WHITE) label.position = target.position + Vector2(8, 3) label.size = target.size - Vector2(16, 6) label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART display_root.add_child(label) display_root.add_child(_actor_target("P910BossPlanTarget_%s" % plan_id, target.position, target.size, tap_boss_plan.bind(plan_id))) var context := _outlined_label(_boss_muster_context(), 10, WHITE) context.name = "P910BossMusterContext" context.position = Vector2(18, size.y - 66) context.size = Vector2(size.x - 36, 58) context.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART display_root.add_child(context) func _boss_muster_context() -> String: if focused_boss_plan == "break_spindle": return _tr("再次点击:投射命中弱轴后逆转下一阵风,并打开短核心窗口;队序会移动。", "Tap again: projectile hits reverse the next gust and open a short core window; order will move.") if focused_boss_plan == "hold_line": return _tr("再次点击:保持队序并持续攻击核心;后端承受更强风压。", "Tap again: preserve order and race the core; the rear absorbs stronger pressure.") return _tr("风从前端进入;弱轴只响应投射。半血后两端同时进风。战败恢复 ◆16,不重掷。", "Gust enters from the front; only projectile hits reverse the spindle. Both ends activate at half HP. Recovery costs ◆16 with no reroll.") func _render_boss_recovery() -> void: var band := ColorRect.new() band.name = "P910BossRecoveryGround" band.position = Vector2(22, size.y * 0.45) band.size = Vector2(size.x * 0.52, 112) band.color = Color(0.22, 0.05, 0.09, 0.88) display_root.add_child(band) var label := _outlined_label(_tr("织机仍保持同一风向与阶段\n支付 ◆16 恢复战前存活队员;此前阵亡者不会复活", "THE LOOM KEEPS THE SAME GUST AND PHASE\nPay ◆16 to restore pre-Boss survivors; prior casualties stay down"), 12, WHITE) label.position = band.position + Vector2(10, 4) label.size = Vector2(band.size.x - 20, 60) label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART display_root.add_child(label) var recover := _action_button("P910BossRecoveryButton", _tr("支付 ◆16 · 重新计划", "PAY ◆16 · REPLAN"), recover_company) recover.position = Vector2(band.position.x + 10, band.position.y + 62) recover.size = Vector2(214, 48) recover.disabled = market.state.coin < 16 display_root.add_child(recover) func _render_gust_path(in_battle: bool) -> void: var count: int = boss_battle.state.member_ids.size() if in_battle else market.state.deployed_recruit_ids.size() var direction: String = String(boss_battle.state.gust_direction) if in_battle else String(market.state.boss_muster.initial_gust_direction) var start_x := 20.0 var width := minf(size.x * 0.56, 470.0) var spacing := width / maxf(1.0, float(count)) for index in count: var number := index + 1 if direction == "front_to_rear" else count - index var label := _outlined_label(str(number), 10, CYAN) label.name = "P910GustFootprint_%02d" % index label.position = Vector2(start_x + spacing * index, 184 if not in_battle else 54) label.size = Vector2(maxf(24.0, spacing - 2), 24) label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER display_root.add_child(label) var ends := _outlined_label(_tr("前端 → 后端", "FRONT → REAR") if direction == "front_to_rear" else _tr("后端 → 前端", "REAR → FRONT"), 10, GOLD_LIGHT) ends.name = "P910GustDirection" ends.position = Vector2(start_x, 205 if not in_battle else 75) ends.size = Vector2(width, 24) ends.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER display_root.add_child(ends) func _add_boss_footprints(center: Vector2) -> void: for index in range(2): var footprint := ColorRect.new() footprint.name = "P910BossFootprint_%d" % (index + 1) footprint.position = center + Vector2(float(index) * 64 - 58, 0) footprint.size = Vector2(54, 5) footprint.color = VIOLET if index == 0 else CYAN footprint.mouse_filter = Control.MOUSE_FILTER_IGNORE display_root.add_child(footprint) func _render_boss_battle() -> void: _add_background(WINDGLASS_BACKGROUND) _render_gust_path(true) var boss_size := clampf(size.y * 0.40, 126.0, 166.0) var boss := _add_actor("Riftblade", Vector2(size.x * 0.70, size.y * 0.27), boss_size, "P910BossBattleActor", false) boss.self_modulate = Color("#b5e8ef") battle_actors[P910Content.BOSS_ID] = boss _add_boss_footprints(Vector2(size.x * 0.75, size.y * 0.72)) var member_size := clampf(minf(64.0, (size.x * 0.60 - 24.0) / maxf(1.0, float(boss_battle.state.member_ids.size()))), 48.0, 64.0) for recruit_id in boss_battle.state.member_ids: var actor := _add_actor(_recruit_rig(recruit_id), Vector2.ZERO, member_size, "P910BossMember_%s" % recruit_id, false) actor.self_modulate = _recruit_color(recruit_id) battle_actors[recruit_id] = actor _add_boss_battle_hud() _update_boss_visuals() if battle_result_visible: _render_boss_battle_result() func _add_boss_battle_hud() -> void: var title := _outlined_label(_tr("风暴织机", "GALE LOOM"), 14, GOLD_LIGHT) title.position = Vector2(14, 4) title.size = Vector2(160, 42) display_root.add_child(title) var hp := _outlined_label("%d / %d" % [boss_battle.state.boss_hp, boss_battle.state.boss_max_hp], 12, WHITE) hp.name = "P910BossHPText" hp.position = Vector2(172, 4) hp.size = Vector2(140, 42) display_root.add_child(hp) var status := _outlined_label(_boss_status_text(), 10, CYAN) status.name = "P910BossRuleStatus" status.position = Vector2(310, 4) status.size = Vector2(maxf(120, size.x - 438), 42) status.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS display_root.add_child(status) var pause := _secondary_button("P910BossPauseButton", "▶" 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("P910BossSpeedButton", "%dx" % battle_speed, toggle_battle_speed) speed.position = Vector2(size.x - 58, 4) speed.size = Vector2(50, 48) display_root.add_child(speed) func _boss_status_text() -> String: var phase := _tr("双端风", "OPPOSED GUSTS") if boss_battle.state.phase_two else _tr("单向风", "SINGLE GUST") var spindle := _tr("弱轴就绪", "SPINDLE READY") if boss_battle.state.spindle_ready else _tr("弱轴重整", "SPINDLE REARMING") return "%s · %s · %s" % [_boss_plan_name(), phase, spindle] func _update_boss_visuals() -> void: if not boss_battle_active: return var count: int = boss_battle.state.member_ids.size() var lane_width := size.x * 0.60 var spacing := (lane_width - 72.0) / maxf(1.0, float(count - 1)) for recruit_id in boss_battle.state.member_ids: var actor: TextureRect = battle_actors.get(recruit_id) if actor == null: continue var position := int(boss_battle.state.member_positions[recruit_id]) actor.position = Vector2(18 + spacing * position, size.y * 0.52) var hp := int(boss_battle.state.member_hp[recruit_id]) var action: Dictionary = battle_actions.get(recruit_id, {}) actor.texture = animation.texture(_recruit_rig(recruit_id), String(action.get("state", "idle")), world_clock) actor.modulate.a = 0.25 if hp <= 0 else 1.0 var boss: TextureRect = battle_actors.get(P910Content.BOSS_ID) if boss != null: var action: Dictionary = battle_actions.get(P910Content.BOSS_ID, {}) boss.texture = animation.texture("Riftblade", String(action.get("state", "idle")), world_clock) boss.modulate.a = 0.35 if boss_battle.state.boss_hp <= 0 else 1.0 var hp_label := find_child("P910BossHPText", true, false) as Label if hp_label != null: hp_label.text = "%d / %d" % [boss_battle.state.boss_hp, boss_battle.state.boss_max_hp] var status := find_child("P910BossRuleStatus", true, false) as Label if status != null: status.text = _boss_status_text() 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 healing := String(event.get("action_family", "")) == "restore" var target := String(event.get("target_member_id", P910Content.BOSS_ID)) battle_actions[source] = {"state": "lantern_heal" if healing else "draw_release" if String(event.get("action_family", "")) == "projectile" else "basic_attack"} _spawn_number(int(event.get("delta", 0)), target, JADE if healing else CORAL) if String(event.get("action_family", "")) == "projectile": _spawn_projectile(source, P910Content.BOSS_ID) elif kind == "boss_action": var target := String(event.get("target_member_id", "")) if not target.is_empty(): _spawn_number(int(event.get("delta", 0)), target, CORAL) _spawn_vfx(target, HIT_VFX) battle_actions[P910Content.BOSS_ID] = {"state": "attack"} elif kind == "boss_rule": last_boss_rule = String(event.get("rule", "")) last_boss_direction = String(event.get("new_gust_direction", event.get("direction", boss_battle.state.gust_direction))) var word: String = String({ "weak_spindle_hit": _tr("逆风", "REVERSE"), "gust_through_enemy_footprints": _tr("反穿", "BACKFLOW"), "half_vitality_opposed_gusts": _tr("双端风", "OPPOSED"), "gale_path_advanced": _tr("阵风", "GUST"), "opposed_gusts_resolved": _tr("夹风", "CROSSFLOW"), }.get(last_boss_rule, last_boss_rule.to_upper())) _spawn_word(String(event.get("target_member_id", P910Content.BOSS_ID)), word, CYAN) elif kind == "boss_plan": var target := String(event.get("moved_member_id", event.get("target_member_id", P910Content.BOSS_ID))) _spawn_word(target, _tr("换序", "SHIFT") if String(event.get("plan", "")) == "break_spindle" else _tr("固线", "HOLD"), GOLD_LIGHT) func _render_boss_battle_result() -> void: var band := ColorRect.new() band.name = "P910BossResultBand" band.position = Vector2(0, size.y - 72) band.size = Vector2(size.x, 72) band.color = Color(0.015, 0.035, 0.075, 0.95) display_root.add_child(band) var won: bool = boss_battle.state.outcome == "victory" var text := _outlined_label(_tr("织机停止 · 写入第二契约", "LOOM HALTED · RECORD CONTRACT 2") if won else _tr("队伍战败 · 返回同一集结", "COMPANY DEFEATED · RETURN TO THE SAME MUSTER"), 17, GOLD_LIGHT if won else CORAL) text.position = Vector2(16, size.y - 68) text.size = Vector2(size.x * 0.68, 60) display_root.add_child(text) var settle := _action_button("P910BossSettleButton", _tr("确认战果", "CONFIRM RESULT"), settle_and_return) settle.position = Vector2(size.x - 212, size.y - 64) settle.size = Vector2(194, 52) display_root.add_child(settle) func _handle_battle_event(event: Dictionary) -> void: super._handle_battle_event(event) if String(event.get("kind", "")) == "enemy_rule" and String(event.get("rule", "")) in ["route_gust", "opposed_route_gust"]: var target := String(event.get("target_member_id", "")) if target.is_empty() and not Array(event.get("target_member_ids", [])).is_empty(): target = String(event.target_member_ids[0]) _spawn_word(target, _tr("阵风", "GUST"), CYAN) func _render_contract_result() -> void: _add_background(WINDGLASS_BACKGROUND) _add_contract_header(_tr("第二契约完成", "SECOND CONTRACT COMPLETE"), _tr("永久结算只执行一次", "Persistent settlement applies exactly once")) var loom := _add_actor("Riftblade", Vector2(size.x * 0.42, size.y * 0.22), minf(160.0, size.y * 0.40), "P910StoppedLoom", true, "death") loom.self_modulate = Color("#9ac4cc") var summary := _outlined_label(_tr("风暴织机停止\n完成印记 CLEAR-RG01-C02\n+18 声望 · Company Record · 开启 RG-02", "GALE LOOM HALTED\nCLEAR-RG01-C02\n+18 RENOWN · COMPANY RECORD · RG-02 UNLOCK"), 16, WHITE) summary.name = "P910ContractResultSummary" summary.position = Vector2(size.x * 0.12, size.y * 0.22) summary.size = Vector2(size.x * 0.42, size.y * 0.48) summary.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART display_root.add_child(summary) var close := _action_button("P910CloseContractButton", _tr("写入公会记录", "WRITE GUILD RECORD"), close_contract) close.position = Vector2(size.x * 0.62, size.y * 0.56) close.size = Vector2(minf(248, size.x * 0.30), 54) display_root.add_child(close) func _add_contract_header(title_text: String, subtitle_text: String) -> void: var shade := ColorRect.new() shade.name = "P910ContractHeader" shade.position = Vector2.ZERO shade.size = Vector2(size.x, 56) 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, 16, GOLD_LIGHT) title.position = Vector2(14, 2) title.size = Vector2(size.x * 0.56, 50) title.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS display_root.add_child(title) var subtitle := _outlined_label(subtitle_text, 10, WHITE) subtitle.position = Vector2(size.x * 0.48, 2) subtitle.size = Vector2(size.x * 0.50 - 14, 50) subtitle.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT subtitle.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART display_root.add_child(subtitle) func _render_persistent_result() -> void: _add_background(HALL_BACKGROUND) _add_contract_header(_tr("风晶边境 · 两份契约完成", "WINDGLASS VERGE · TWO CONTRACTS CLEARED"), _tr("下一地区印记已点亮", "The next Region seal is now lit")) var seal_width := (size.x - 36.0) / 8.0 for index in range(8): var seal_id := "RG-%02d" % (index + 1) var unlocked: bool = seal_id in market.state.unlocked_region_ids var seal := _outlined_label("◆\n%s" % seal_id, 9, GOLD_LIGHT if unlocked else Color(0.42, 0.50, 0.58)) seal.name = "P910RegionSeal_%s" % seal_id seal.position = Vector2(18 + seal_width * index, 56) seal.size = Vector2(seal_width, 50) seal.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER display_root.add_child(seal) var record: Dictionary = market.state.company_records[-1] var rows: Array = record.company var actor_size := clampf(minf(68.0, (size.x * 0.60) / maxf(1.0, float(rows.size()))), 46.0, 68.0) var spacing := minf(actor_size + 3.0, (size.x * 0.60 - actor_size) / maxf(1.0, float(rows.size() - 1))) for index in rows.size(): var row: Dictionary = rows[index] var profession: Dictionary = content.professions[String(row.profession_id)] var actor := _add_actor(String(profession.visual_rig), Vector2(18 + spacing * index, size.y * 0.48), actor_size, "P910RecordActor_%s" % String(row.recruit_id), true, "victory") actor.self_modulate = _p910_profession_color(String(row.profession_id)) var summary := _outlined_label(_tr("声望 %d\n完成印记 CLEAR-RG01-C02\nCompany Record %d 名 / 12 回合\nRG-02 已开启" % [market.state.persistent_renown, rows.size()], "RENOWN %d\nCLEAR-RG01-C02\nCOMPANY RECORD %d / 12 ROUNDS\nRG-02 UNLOCKED" % [market.state.persistent_renown, rows.size()]), 15, WHITE) summary.name = "P910PersistentSummary" summary.position = Vector2(size.x * 0.64, size.y * 0.30) summary.size = Vector2(size.x * 0.33, size.y * 0.50) summary.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART display_root.add_child(summary) func _boss_plan_name() -> String: var plan: String = String(boss_battle.state.plan) if boss_battle_active else String(market.state.selected_boss_plan) return _tr("破轴逆风", "BREAK SPINDLE") if plan == "break_spindle" else _tr("固线攻核", "HOLD LINE") func _p910_profession_color(profession_id: String) -> Color: return { "PF-B01": GOLD_LIGHT, "PF-B02": CORAL, "PF-B03": JADE, "PF-B04": CYAN, "PF-B05": GOLD, "PF-B06": VIOLET, "PF-B07": JADE, "PF-B08": Color("#ef9cbc"), "PF-B09": Color("#78d9d0"), "PF-B11": GOLD_LIGHT, }.get(profession_id, WHITE) 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)) var profession_id := String(_recruit_record(actor_id).get("profession_id", "")) return _tr({ "PF-B01": "守卫", "PF-B02": "长弦", "PF-B03": "术士", "PF-B04": "游侠", "PF-B05": "弓手", "PF-B06": "咒师", "PF-B07": "治疗", "PF-B08": "铸师", "PF-B09": "潮语", "PF-B11": "风匠", }.get(profession_id, profession_id), { "PF-B01": "GUARD", "PF-B02": "BOW", "PF-B03": "MAGE", "PF-B04": "SCOUT", "PF-B05": "ARCHER", "PF-B06": "HEXER", "PF-B07": "HEAL", "PF-B08": "SMITH", "PF-B09": "TIDE", "PF-B11": "GALE", }.get(profession_id, profession_id))