class_name P100RuntimeController extends "res://p9_7/runtime_controller.gd" const P100Content = preload("res://p10_0/content_registry.gd") const P100MarketService = preload("res://p10_0/market_service.gd") const P100BattleService = preload("res://p10_0/wave_battle_service.gd") const P100ResolutionService = preload("res://p10_0/resolution_service.gd") const P100ContractService = preload("res://p10_0/contract_service.gd") const P100BossBattleService = preload("res://p10_0/boss_battle_service.gd") const P100SaveService = preload("res://p10_0/save_service.gd") const SAVE_PATH := "user://aetherbound_guild_p10.json" const TOUCH_DRAG_SLOP := 18.0 const DIFFICULTIES := ["wayfinder", "standard", "stormbound", "iron_oath"] const ROLE_RIGS := { "front_hold": "Hearthguard", "single_pressure": "Wayrunner", "reach_control": "Farstring", "reorder": "Wayrunner", "projectile": "Farstring", "area": "Farstring", "restore": "Kindhand", "enable": "Hearthguard", "construct": "Hearthguard", "impair": "Wayrunner", "tempo": "Wayrunner", "supply": "Kindhand", } var profession_rows: Dictionary = {} var trait_rows: Dictionary = {} var equipment_rows: Dictionary = {} var enemy_rows: Dictionary = {} var boss_rows: Dictionary = {} var contract_rows: Dictionary = {} var screen := "title" var selected_difficulty := "standard" var opening_selection: Array[String] = [] var focused_recruit_offer := "" var focused_equipment_offer := "" var focused_battle_offer := "" var focused_company_actor := "" var focused_boss_plan := "" var pending_line: Array[String] = [] var boss_battle var boss_battle_active := false var battle_snapshot: Dictionary = {} var battle_event_cursor := 0 var recent_events: Array[Dictionary] = [] var save_sequence := 0 var save_commit_hash := "" var settings_open := false var reset_confirmation := false var input_touch_count := 0 var input_drag_count := 0 var drag_source_recruit := "" var drag_start := Vector2.ZERO var drag_active := false var suppress_actor_tap_until_frame := -1 var line_touch_rects: Dictionary = {} var save_path := SAVE_PATH var owned_touch_index := -1 var owned_touch_button: Button var owned_touch_mouse_filter := Control.MOUSE_FILTER_STOP var owned_touch_start := Vector2.ZERO var owned_touch_dragged := false var drag_touch_index := -1 func _bootstrap_market() -> void: content = P100Content.snapshot() profession_rows = P100Content.rows_by_id(content, "professions") trait_rows = P100Content.rows_by_id(content, "traits") equipment_rows = P100Content.rows_by_id(content, "equipment") enemy_rows = P100Content.rows_by_id(content, "enemies") boss_rows = P100Content.rows_by_id(content, "bosses") contract_rows = P100Content.rows_by_id(content, "contracts") saves = P100SaveService.new() market = P100MarketService.new() var restored := false if not entry_snapshot.is_empty(): restored = market.restore_snapshot(entry_snapshot) elif persistence_enabled: var loaded: Dictionary = saves.load_slot(ProjectSettings.globalize_path(save_path)) if loaded.ok: restored = market.restore_snapshot(loaded.snapshot) save_sequence = int(loaded.sequence) save_commit_hash = String(loaded.commit_hash) if not entry_snapshot.is_empty() and not restored: last_code = "p100_snapshot_invalid" screen = "title" elif restored: last_code = "p100_resumed" _restore_runtime_phase() else: last_code = "p100_title_ready" screen = "title" opening_selection.clear() focused_recruit_offer = "" focused_equipment_offer = "" focused_battle_offer = "" focused_company_actor = "" focused_boss_plan = "" battle_active = false if screen != "battle" else battle_active battle_result_visible = false pending_line.assign(market.state.current_run.get("deployed_recruit_ids", [])) _queue_render() func _restore_runtime_phase() -> void: if not market.state.run_creation_draft.is_empty(): screen = "opening" opening_selection.assign(market.state.run_creation_draft.selected_offer_ids) return if market.state.current_run.is_empty(): screen = "guild" if not market.state.guild_profile.company_records.is_empty() else "title" return var phase := String(market.state.current_run.phase) if phase == "encounter": battle = P100BattleService.new(market.state.snapshot()) boss_battle_active = false _start_visible_battle() elif phase == "boss_encounter": boss_battle = P100BossBattleService.new(market.state.snapshot()) boss_battle_active = true _start_visible_battle() elif phase == "boss_muster": screen = "boss_muster" elif phase == "contract_result": screen = "contract_result" else: screen = "reward" if not market.state.current_run.pending_reward.is_empty() else "market" func _process(delta: float) -> void: if not low_power: world_clock += maxf(0.0, delta) _update_ambient_actors() _animate_runtime_battle_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.10)) if ticks <= 0: return battle_tick_accumulator -= float(ticks) * 0.10 advance_battle_ticks(ticks) func _input(event: InputEvent) -> void: if event is InputEventScreenTouch: var touch := event as InputEventScreenTouch if touch.pressed: input_touch_count += 1 if owned_touch_index >= 0: return owned_touch_index = touch.index owned_touch_button = _button_at_point(touch.position) if owned_touch_button != null: owned_touch_mouse_filter = owned_touch_button.mouse_filter owned_touch_button.mouse_filter = Control.MOUSE_FILTER_IGNORE owned_touch_start = touch.position owned_touch_dragged = false drag_start = touch.position drag_source_recruit = _recruit_at_point(touch.position) drag_touch_index = touch.index if not drag_source_recruit.is_empty() else -1 drag_active = false if owned_touch_button != null or drag_touch_index >= 0: get_viewport().set_input_as_handled() else: _clear_touch_ownership() elif touch.index == owned_touch_index: get_viewport().set_input_as_handled() var release_button := _button_at_point(touch.position, owned_touch_button) var activate_button := ( not touch.canceled and not owned_touch_dragged and owned_touch_button != null and is_instance_valid(owned_touch_button) and not owned_touch_button.is_queued_for_deletion() and release_button == owned_touch_button ) var button_to_activate := owned_touch_button var button_instance_id := button_to_activate.get_instance_id() if is_instance_valid(button_to_activate) else 0 var mouse_filter_to_restore := owned_touch_mouse_filter if not drag_source_recruit.is_empty() and touch.index == drag_touch_index: if drag_active: var target: String = _recruit_at_point(touch.position) _commit_line_drag(drag_source_recruit, target) suppress_actor_tap_until_frame = Engine.get_process_frames() + 1 _clear_touch_ownership() call_deferred("_restore_touch_button_mouse_filter", button_instance_id, mouse_filter_to_restore) if activate_button and is_instance_valid(button_to_activate): button_to_activate.emit_signal(&"pressed") elif event is InputEventScreenDrag and event.index == owned_touch_index: var drag := event as InputEventScreenDrag get_viewport().set_input_as_handled() if drag.position.distance_to(owned_touch_start) >= TOUCH_DRAG_SLOP: owned_touch_dragged = true if drag_touch_index == drag.index and not drag_source_recruit.is_empty(): input_drag_count += 1 if drag.position.distance_to(drag_start) >= TOUCH_DRAG_SLOP: drag_active = true func begin_expedition() -> void: if screen != "title" and screen != "guild": return var contract_id: String = "CTR-R1-02" if "CLEAR-RG01-C01" in market.state.guild_profile.contract_clear_ids and "CLEAR-RG01-C02" not in market.state.guild_profile.contract_clear_ids else "CTR-R1-01" var begun: Dictionary = market.begin_run_creation(_operation("run", "begin"), market.state.revision, contract_id, selected_difficulty) if not begun.ok: last_code = String(begun.code) _queue_render() return var draft_id := String(market.state.run_creation_draft.draft_id) var materialized: Dictionary = market.materialize_opening_offers(_operation("run", "offers"), market.state.revision, draft_id) last_code = String(materialized.code) if materialized.ok: screen = "opening" opening_selection.clear() if persistence_enabled and not _save_stable(): last_code = "p100_opening_save_failed" _queue_render() func choose_difficulty(difficulty: String) -> void: if difficulty in DIFFICULTIES and screen in ["title", "guild"]: selected_difficulty = difficulty last_code = "difficulty_selected" _queue_render() func tap_opening_offer(offer_id: String) -> void: if screen != "opening" or market.state.run_creation_draft.is_empty(): return if offer_id in opening_selection: opening_selection.erase(offer_id) elif opening_selection.size() < 2: opening_selection.append(offer_id) else: opening_selection.pop_front() opening_selection.append(offer_id) last_code = "opening_selection_local" if opening_selection.size() == 2: var selected: Dictionary = market.set_opening_selection(_operation("run", "selection"), market.state.revision, opening_selection) last_code = String(selected.code) if selected.ok: opening_selection.assign(market.state.run_creation_draft.selected_offer_ids) _save_stable() _queue_render() func commit_opening_company() -> void: if screen != "opening" or opening_selection.size() != 2 or market.state.run_creation_draft.is_empty(): return var draft: Dictionary = market.state.run_creation_draft var result: Dictionary = market.commit_run_creation( _operation("run", "commit"), market.state.revision, String(draft.draft_id), int(draft.selection_revision), opening_selection, String(draft.opening_offer_set_hash) ) last_code = String(result.code) if result.ok: screen = "market" pending_line.assign(market.state.current_run.deployed_recruit_ids) _save_stable() _queue_render() func tap_recruit_offer(offer_id: String) -> void: if not _market_ready(): return var offer_result: Dictionary = market.inspect_recruit_offer(offer_id) if not offer_result.ok: last_code = String(offer_result.code) _queue_render() return if focused_recruit_offer != offer_id: focused_recruit_offer = offer_id focused_equipment_offer = "" focused_battle_offer = "" last_code = "recruit_offer_inspected" _queue_render() return var offer: Dictionary = offer_result.receipt var hired: Dictionary = market.hire(_operation("hire", offer_id), market.state.revision, offer_id, int(offer.list_price), true) last_code = String(hired.code) if hired.ok: focused_recruit_offer = "" pending_line.assign(market.state.current_run.formation_proposal) _save_stable() _queue_render() func tap_equipment_offer(offer_id: String) -> void: if not _market_ready(): return var inspected: Dictionary = market.inspect_equipment_offer(offer_id) last_code = String(inspected.code) if inspected.ok: focused_equipment_offer = offer_id focused_recruit_offer = "" focused_battle_offer = "" _queue_render() func tap_company_actor(recruit_id: String) -> void: if Engine.get_process_frames() <= suppress_actor_tap_until_frame or not _market_ready() or recruit_id not in market.state.current_run.company: return if not focused_equipment_offer.is_empty(): _buy_and_equip(focused_equipment_offer, recruit_id) return if focused_company_actor != recruit_id: focused_company_actor = recruit_id last_code = "company_actor_inspected" else: if recruit_id in pending_line: if pending_line.size() > 1: pending_line.erase(recruit_id) last_code = "line_member_moved_to_reserve" else: if pending_line.size() < int(market.state.current_run.line_capacity): pending_line.append(recruit_id) last_code = "reserve_member_added" _queue_render() func commit_pending_line() -> void: if not _market_ready() or pending_line.is_empty(): return var committed: Dictionary = market.commit_deployment(_operation("deploy", "line"), market.state.revision, pending_line) last_code = String(committed.code) if committed.ok: pending_line.assign(market.state.current_run.deployed_recruit_ids) _save_stable() _queue_render() func use_automatic_line() -> void: if not _market_ready(): return pending_line.assign(market.automatic_formation_candidate()) commit_pending_line() func refresh_market() -> void: if not _market_ready(): return var cost: int = P100MarketService.refresh_cost(int(market.state.current_run.refresh_count), market.state.current_run.locks.size()) var refreshed: Dictionary = market.refresh_market(_operation("market", "refresh"), market.state.revision, cost) last_code = String(refreshed.code) if refreshed.ok: focused_recruit_offer = "" focused_equipment_offer = "" _save_stable() _queue_render() func tap_battle_offer(offer_id: String) -> void: if not _market_ready(): return var preview: Dictionary = market.preview_battle_offer(offer_id) if not preview.ok: last_code = String(preview.code) _queue_render() return if focused_battle_offer != offer_id: focused_battle_offer = offer_id focused_recruit_offer = "" focused_equipment_offer = "" last_code = "battle_offer_inspected" _queue_render() return var committed: Dictionary = market.commit_battle_offer(_operation("battle", offer_id), market.state.revision, offer_id, String(market.state.current_run.market_board.board_hash)) last_code = String(committed.code) if committed.ok: battle = P100BattleService.new(market.state.snapshot()) boss_battle_active = false _start_visible_battle() _save_stable() _queue_render() func enter_boss_muster() -> void: if not _market_ready() or int(market.state.current_run.cleared_rounds) != 12: return var contract := P100ContractService.new(market.state.snapshot()) var entered := contract.enter_boss_muster(_operation("boss", "muster"), contract.state.revision) last_code = String(entered.code) if entered.ok and _replace_market_snapshot(entered.snapshot): screen = "boss_muster" focused_boss_plan = "" _save_stable() _queue_render() func tap_boss_plan(plan_id: String) -> void: if market == null or market.state.current_run.is_empty() or String(market.state.current_run.phase) != "boss_muster": return if focused_boss_plan != plan_id: focused_boss_plan = plan_id last_code = "boss_plan_inspected" _queue_render() return var contract := P100ContractService.new(market.state.snapshot()) var committed := contract.commit_boss_plan(_operation("boss", plan_id), contract.state.revision, String(contract.state.current_run.boss_muster.muster_hash), plan_id) last_code = String(committed.code) if committed.ok and _replace_market_snapshot(committed.snapshot): boss_battle = P100BossBattleService.new(market.state.snapshot()) boss_battle_active = true _start_visible_battle() _save_stable() _queue_render() func advance_battle_ticks(ticks: int) -> Dictionary: if not battle_active or battle_result_visible: return {"ok": false, "code": "battle_inactive", "snapshot": battle_snapshot} var result: Dictionary = boss_battle.advance_ticks(ticks) if boss_battle_active else battle.advance_ticks(ticks) last_code = String(result.code) if result.ok: battle_snapshot = Dictionary(result.snapshot).duplicate(true) _consume_battle_events() battle_result_visible = bool(battle_snapshot.terminal) _queue_render() return result func request_retreat() -> void: if not battle_active or battle_result_visible or boss_battle_active: return var result: Dictionary = battle.begin_retreat(int(battle_snapshot.tick)) last_code = String(result.code) if result.ok: battle_snapshot = result.snapshot _consume_battle_events() _queue_render() func settle_and_return() -> void: if not battle_active or not bool(battle_snapshot.get("terminal", false)): return var result: Dictionary if boss_battle_active: var contract := P100ContractService.new(market.state.snapshot()) result = contract.settle_boss(_operation("boss", "settle"), contract.state.revision, String(battle_snapshot.snapshot_hash), battle_snapshot) else: var resolver := P100ResolutionService.new(market.state.snapshot()) result = resolver.settle_battle(_operation("battle", "settle"), resolver.state.revision, String(battle_snapshot.snapshot_hash), battle_snapshot) last_code = String(result.code) if not result.ok or not _replace_market_snapshot(result.snapshot): _queue_render() return battle_active = false boss_battle_active = false battle_result_visible = false battle_snapshot = {} recent_events.clear() pending_line.assign(market.state.current_run.get("deployed_recruit_ids", [])) _restore_runtime_phase() _save_stable() _queue_render() func claim_reward_card(card_id: String, artifact_id: String = "") -> void: if market.state.current_run.is_empty() or market.state.current_run.pending_reward.is_empty(): return var resolver := P100ResolutionService.new(market.state.snapshot()) var draft: Dictionary = resolver.state.current_run.pending_reward var selected_artifact := artifact_id if not selected_artifact.is_empty() and selected_artifact in resolver.state.current_run.active_artifact_ids: selected_artifact = "" var replace_id := "" if not selected_artifact.is_empty() and resolver.state.current_run.active_artifact_ids.size() >= 3: replace_id = String(resolver.state.current_run.active_artifact_ids[0]) var claimed := resolver.claim_reward(_operation("reward", card_id), resolver.state.revision, String(draft.draft_hash), card_id, selected_artifact, replace_id) last_code = String(claimed.code) if claimed.ok and _replace_market_snapshot(claimed.snapshot): screen = "market" _save_stable() _queue_render() func close_contract() -> void: if market.state.current_run.is_empty() or String(market.state.current_run.phase) != "contract_result": return var contract := P100ContractService.new(market.state.snapshot()) var close_id := String(contract.state.current_run.pending_close.close_id) var closed := contract.close_contract(_operation("contract", "close"), contract.state.revision, close_id) last_code = String(closed.code) if closed.ok and _replace_market_snapshot(closed.snapshot): screen = "guild" _save_stable() _queue_render() func toggle_settings() -> void: settings_open = not settings_open reset_confirmation = false _queue_render() func toggle_locale() -> void: locale = "en" if locale != "en" else "zh_CN" _queue_render() func toggle_text_scale() -> void: text_scale = 1.0 if text_scale > 1.0 else 1.3 _queue_render() func toggle_reduced_motion() -> void: reduced_motion = not reduced_motion _queue_render() func toggle_mute() -> void: muted = not muted _queue_render() func toggle_low_power() -> void: low_power = not low_power _queue_render() func reset_progress() -> void: if not reset_confirmation: reset_confirmation = true _queue_render() return var absolute := ProjectSettings.globalize_path(save_path) for path in [absolute, "%s.bak" % absolute, "%s.candidate" % absolute]: if FileAccess.file_exists(path): DirAccess.remove_absolute(path) market = P100MarketService.new() save_sequence = 0 save_commit_hash = "" screen = "title" settings_open = false reset_confirmation = false last_code = "progress_reset" _queue_render() func _start_visible_battle() -> void: battle_active = true battle_result_visible = false battle_paused = false battle_speed = 1 battle_tick_accumulator = 0.0 battle_event_cursor = 0 recent_events.clear() battle_snapshot = boss_battle.snapshot() if boss_battle_active else battle.snapshot() _consume_battle_events() screen = "battle" func _buy_and_equip(offer_id: String, recruit_id: String) -> void: var inspected: Dictionary = market.inspect_equipment_offer(offer_id) if not inspected.ok: last_code = String(inspected.code) _queue_render() return var offer: Dictionary = inspected.receipt var bought: Dictionary = market.buy_equipment(_operation("equipment", "buy"), market.state.revision, offer_id, int(offer.list_price)) if not bought.ok: last_code = String(bought.code) _queue_render() return var instance_id := String(bought.receipt.instance_id) var previous := String(market.state.current_run.equipped_by_recruit.get(recruit_id, "")) var equipped: Dictionary = market.equip(_operation("equipment", "equip"), market.state.revision, recruit_id, instance_id, previous) last_code = String(equipped.code) if equipped.ok: focused_equipment_offer = "" focused_company_actor = recruit_id _save_stable() _queue_render() func _commit_line_drag(source_id: String, target_id: String) -> void: if not _market_ready() or source_id.is_empty() or target_id.is_empty() or source_id == target_id: return var ordered: Array = market.state.current_run.deployed_recruit_ids.duplicate() if source_id not in ordered or target_id not in ordered: return ordered.erase(source_id) ordered.insert(ordered.find(target_id), source_id) var result: Dictionary = market.commit_deployment(_operation("deploy", "drag"), market.state.revision, ordered) last_code = String(result.code) if result.ok: pending_line.assign(market.state.current_run.deployed_recruit_ids) _save_stable() _queue_render() func _recruit_at_point(point: Vector2) -> String: var canvas_point := _canvas_point(point) for recruit_id in line_touch_rects: if Rect2(line_touch_rects[recruit_id]).has_point(canvas_point): return String(recruit_id) return "" func _button_at_point(point: Vector2, allowed_ignored_button: Button = null) -> Button: if display_root == null or not is_instance_valid(display_root): return null var canvas_point := _canvas_point(point) var best: Button var best_z := -2147483648 for node in display_root.find_children("*", "Button", true, false): var button := node as Button if button == null or button.disabled: continue if button.mouse_filter == Control.MOUSE_FILTER_IGNORE and button != allowed_ignored_button: continue if not button.is_visible_in_tree() or button.is_queued_for_deletion(): continue if settings_open and _effective_z_index(button) < 79: continue if not button.get_global_rect().has_point(canvas_point): continue var effective_z := _effective_z_index(button) if best == null or effective_z >= best_z: best = button best_z = effective_z return best func _canvas_point(viewport_point: Vector2) -> Vector2: return get_viewport().get_final_transform().affine_inverse() * viewport_point func _effective_z_index(item: CanvasItem) -> int: var result := item.z_index var ancestor := item.get_parent() while item.z_as_relative and ancestor is CanvasItem: var canvas_ancestor := ancestor as CanvasItem result += canvas_ancestor.z_index item = canvas_ancestor ancestor = item.get_parent() return result func _clear_touch_ownership() -> void: owned_touch_index = -1 owned_touch_button = null owned_touch_mouse_filter = Control.MOUSE_FILTER_STOP owned_touch_start = Vector2.ZERO owned_touch_dragged = false drag_touch_index = -1 drag_source_recruit = "" drag_active = false func _restore_touch_button_mouse_filter(instance_id: int, mouse_filter: int) -> void: var button := instance_from_id(instance_id) as Button if instance_id > 0 else null if button != null and not button.is_queued_for_deletion(): button.mouse_filter = mouse_filter func _consume_battle_events() -> void: var events: Array = battle_snapshot.get("event_log", []) while battle_event_cursor < events.size(): var event: Dictionary = events[battle_event_cursor] var kind := String(event.kind) presented_event_counts[kind] = int(presented_event_counts.get(kind, 0)) + 1 var retained := event.duplicate(true) retained["expires"] = world_clock + (0.8 if reduced_motion else 1.2) recent_events.append(retained) battle_event_cursor += 1 while recent_events.size() > 6: recent_events.pop_front() func _save_stable() -> bool: if not persistence_enabled or market == null: return false var next_sequence := save_sequence + 1 var saved: Dictionary = saves.write_slot(ProjectSettings.globalize_path(save_path), market.state.snapshot(), next_sequence, save_commit_hash) if saved.ok: save_sequence = next_sequence save_commit_hash = String(saved.commit_hash) return bool(saved.ok) func _replace_market_snapshot(snapshot: Dictionary) -> bool: var replacement := P100MarketService.new() if not replacement.restore_snapshot(snapshot): last_code = "%s_restore_failed" % last_code return false market = replacement return true func _operation(kind: String, target: String) -> String: operation_index += 1 return "p100-ui-%s-%d-%d-%s" % [kind, market.state.revision, operation_index, target] func _market_ready() -> bool: return market != null and not battle_active and not market.state.current_run.is_empty() and String(market.state.current_run.phase) == "market" and market.state.current_run.pending_reward.is_empty() func _render_current() -> void: if display_root == null: return _clear_display() line_touch_rects.clear() set_meta("visible_world_ratio", 0.84) if size.y > size.x: _render_rotate_notice() return match screen: "title": _render_title() "opening": _render_opening() "battle": _render_battle() "reward": _render_reward() "boss_muster": _render_boss_muster() "contract_result": _render_contract_result() "guild": _render_guild_result() _: _render_market() _add_settings_button() if settings_open: _render_settings_band() func _render_title() -> void: _add_background(HALL_BACKGROUND) var veil := ColorRect.new() veil.name = "P100TitleVeil" veil.position = Vector2.ZERO veil.size = Vector2(size.x * 0.52, size.y) veil.color = Color(0.015, 0.035, 0.075, 0.66) veil.mouse_filter = Control.MOUSE_FILTER_IGNORE display_root.add_child(veil) var title := _outlined_label(_tr("以太边境公会", "AETHERBOUND GUILD"), 30, GOLD_LIGHT) title.name = "P100Title" title.position = Vector2(28, 42) title.size = Vector2(size.x * 0.52 - 44, 62) display_root.add_child(title) var subtitle := _outlined_label(_tr("组建无名佣兵队 · 穿越风晶荒野 · 让每次损失改变下一次选择", "BUILD A COMPANY · CROSS THE WINDGLASS · LET EVERY LOSS CHANGE THE NEXT CHOICE"), 12, WHITE) subtitle.name = "P100TitleSubtitle" subtitle.position = Vector2(30, 102) subtitle.size = Vector2(size.x * 0.49 - 40, 70) subtitle.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART display_root.add_child(subtitle) for index in range(4): var rig: String = ["Hearthguard", "Wayrunner", "Farstring", "Kindhand"][index] var actor := _add_actor(rig, Vector2(size.x * 0.54 + index * 68.0, size.y * 0.42 + (index % 2) * 8.0), 76, "P100TitleActor_%d" % index, true, "advance") actor.self_modulate = [GOLD_LIGHT, CORAL, CYAN, JADE][index] var difficulty_title := _outlined_label(_tr("远征誓约", "EXPEDITION OATH"), 11, GOLD_LIGHT) difficulty_title.position = Vector2(28, size.y - 146) difficulty_title.size = Vector2(150, 26) display_root.add_child(difficulty_title) for index in range(DIFFICULTIES.size()): var difficulty := String(DIFFICULTIES[index]) var button := _secondary_button("P100Difficulty_%s" % difficulty, _difficulty_label(difficulty), choose_difficulty.bind(difficulty)) button.position = Vector2(28 + index * 116.0, size.y - 116) button.size = Vector2(108, 48) if difficulty == selected_difficulty: button.add_theme_stylebox_override("normal", _flat_style(Color(0.32, 0.20, 0.05, 0.94), GOLD_LIGHT, 3, 4)) display_root.add_child(button) var start := _action_button("P100NewExpeditionButton", _tr("开始远征", "NEW EXPEDITION"), begin_expedition) start.position = Vector2(size.x - 232, size.y - 66) start.size = Vector2(212, 52) display_root.add_child(start) func _render_opening() -> void: _add_background(HALL_BACKGROUND) _add_header(_tr("从四名候选人中选两名", "CHOOSE TWO OF FOUR RECRUITS"), _tr("已选择 %d / 2" % opening_selection.size(), "%d / 2 SELECTED" % opening_selection.size())) var offers: Array = market.state.run_creation_draft.get("opening_offers", []) var lane_left := 34.0 var lane_width := size.x - 68.0 var spacing := lane_width / 4.0 for index in range(offers.size()): var offer: Dictionary = offers[index] var offer_id := String(offer.source_offer_id) var selected := offer_id in opening_selection var actor_size := clampf(size.y * 0.30, 84.0, 118.0) var x := lane_left + spacing * index + (spacing - actor_size) * 0.5 var y := size.y * 0.23 _add_ground_ring(Vector2(x + actor_size * 0.5, y + actor_size - 4), actor_size * 0.72, GOLD if selected else CYAN, "P100OpeningRing_%s" % offer_id) var actor := _add_actor(_p100_recruit_rig(offer), Vector2(x, y), actor_size, "P100OpeningActor_%s" % offer_id, true) actor.self_modulate = GOLD_LIGHT if selected else _p100_profession_color(String(offer.profession_id)) display_root.add_child(_actor_target("P100OpeningTarget_%s" % offer_id, Vector2(x - 6, y - 6), Vector2(actor_size + 12, actor_size + 66), tap_opening_offer.bind(offer_id))) var label := _outlined_label("%s\n%s · Lv.%d\n%s" % [_generated_name(offer), _row_name(String(offer.profession_id), "profession"), int(offer.level), _row_name(String(offer.trait_id), "trait")], 9, GOLD_LIGHT if selected else WHITE) label.position = Vector2(x - 18, y + actor_size - 2) label.size = Vector2(actor_size + 36, 70) label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART display_root.add_child(label) var consequence := _outlined_label(_tr("未选择者不会保留。系统会按职业自动排定前后顺序。", "UNCHOSEN RECRUITS LEAVE. THE SYSTEM SETS THE FIRST LINE ORDER."), 10, WHITE) consequence.position = Vector2(24, size.y - 58) consequence.size = Vector2(size.x - 280, 48) consequence.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART display_root.add_child(consequence) var commit := _action_button("P100CommitRunButton", _tr("组建队伍", "FORM COMPANY"), commit_opening_company) commit.position = Vector2(size.x - 238, size.y - 62) commit.size = Vector2(218, 52) commit.disabled = opening_selection.size() != 2 display_root.add_child(commit) func _render_market() -> void: _add_background(WINDGLASS_BACKGROUND) if market.state.current_run.is_empty(): screen = "guild" _render_guild_result() return var run: Dictionary = market.state.current_run _add_header( _tr("%s · 集市 %d" % [_contract_name(String(run.contract_id)), int(run.market_index)], "%s · MARKET %d" % [_contract_name(String(run.contract_id)), int(run.market_index)]), _tr("◆%d 信用%d 战线%d/%d 胜利%d/12" % [run.coin, run.replacement_credit, run.deployed_recruit_ids.size(), run.line_capacity, run.cleared_rounds], "◆%d CREDIT %d LINE %d/%d CLEARS %d/12" % [run.coin, run.replacement_credit, run.deployed_recruit_ids.size(), run.line_capacity, run.cleared_rounds]) ) if int(run.cleared_rounds) == 12: if bool(run.recovery_required): _render_market_recruits() _render_market_equipment() else: _render_boss_gate_in_market() _render_company_line() _render_market_context() return _render_market_recruits() _render_market_equipment() _render_battle_gates() _render_company_line() _render_market_context() func _render_market_recruits() -> void: var offers: Array = market.state.current_run.market_board.recruit_offers var available: Array = market.state.current_run.available_recruit_offer_ids for index in range(offers.size()): var offer: Dictionary = offers[index] var offer_id := String(offer.source_offer_id) if offer_id not in available: continue var actor_size := 58.0 var x := 18.0 + index * 82.0 var y := 62.0 var focused := focused_recruit_offer == offer_id var protected := offer_id == String(market.state.current_run.get("protected_recovery_offer_id", "")) _add_ground_ring(Vector2(x + 29, y + 54), 46, GOLD if focused else JADE if protected else CYAN, "P100RecruitRing_%s" % offer_id) var actor := _add_actor(_p100_recruit_rig(offer), Vector2(x, y), actor_size, "P100RecruitActor_%s" % offer_id, true) actor.self_modulate = GOLD_LIGHT if focused else _p100_profession_color(String(offer.profession_id)) display_root.add_child(_actor_target("P100RecruitTarget_%s" % offer_id, Vector2(x - 4, y - 4), Vector2(66, 102), tap_recruit_offer.bind(offer_id))) var offer_label := _short_profession(String(offer.profession_id)) if protected: offer_label = _tr("救援 · %s" % offer_label, "RECOVERY · %s" % offer_label) var label := _outlined_label("%s\n◆%d" % [offer_label, int(offer.list_price)], 8, WHITE) label.position = Vector2(x - 12, y + 54) label.size = Vector2(82, 38) label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER display_root.add_child(label) func _render_market_equipment() -> void: var offers: Array = market.state.current_run.market_board.equipment_offers var available: Array = market.state.current_run.available_equipment_offer_ids for index in range(offers.size()): var offer: Dictionary = offers[index] var offer_id := String(offer.offer_id) if offer_id not in available: continue var x := 272.0 + index * 66.0 var focused := focused_equipment_offer == offer_id var pedestal := TextureRect.new() pedestal.name = "P100EquipmentObject_%s" % offer_id pedestal.texture = _equipment_texture(String(offer.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(48, 48) pedestal.self_modulate = GOLD_LIGHT if focused else _equipment_color(String(offer.catalog_id)) pedestal.mouse_filter = Control.MOUSE_FILTER_IGNORE display_root.add_child(pedestal) display_root.add_child(_actor_target("P100EquipmentTarget_%s" % offer_id, Vector2(x - 5, 62), Vector2(58, 96), tap_equipment_offer.bind(offer_id))) var label := _outlined_label("%s\n◆%d" % [String(offer.catalog_id), int(offer.list_price)], 8, WHITE) label.position = Vector2(x - 10, 116) label.size = Vector2(68, 38) label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER display_root.add_child(label) func _render_battle_gates() -> void: var offers: Array = market.state.current_run.market_board.battle_offers var available: Array = market.state.current_run.available_battle_offer_ids var x := size.x - 176.0 for index in range(offers.size()): var offer: Dictionary = offers[index] var offer_id := String(offer.offer_id) if offer_id not in available: continue var y := 58.0 + index * 68.0 var focused := focused_battle_offer == offer_id var gate := _secondary_button("P100BattleTarget_%s" % offer_id, _tr("风险 %d · %d 波" % [int(offer.risk), int(offer.wave_count)], "RISK %d · %d WAVES" % [int(offer.risk), int(offer.wave_count)]), tap_battle_offer.bind(offer_id)) gate.position = Vector2(x, y) gate.size = Vector2(158, 58) gate.add_theme_stylebox_override("normal", _flat_style(_risk_color(int(offer.risk), focused), GOLD_LIGHT if focused else WHITE, 2, 4)) display_root.add_child(gate) func _render_company_line() -> void: var run: Dictionary = market.state.current_run var company: Array = run.company var line_right := size.x - 190.0 var actor_size := clampf((line_right - 30.0) / maxf(6.0, float(company.size())), 44.0, 64.0) var spacing := (line_right - 28.0 - actor_size) / maxf(1.0, float(company.size() - 1)) var y := size.y - 152.0 for index in range(company.size()): var recruit_id := String(company[index]) var record: Dictionary = run.recruit_records[recruit_id] var in_line := recruit_id in pending_line var x := 16.0 + index * spacing var actor_y := y if in_line else y + 14.0 _add_ground_ring(Vector2(x + actor_size * 0.5, actor_y + actor_size - 3), actor_size * 0.70, GOLD if in_line else CYAN, "P100LineRing_%s" % recruit_id) var actor := _add_actor(_p100_recruit_rig(record), Vector2(x, actor_y), actor_size, "P100CompanyActor_%s" % recruit_id, true) actor.self_modulate = GOLD_LIGHT if focused_company_actor == recruit_id else _p100_profession_color(String(record.profession_id)) var rect := Rect2(Vector2(x - 3, actor_y - 4), Vector2(actor_size + 6, actor_size + 30)) line_touch_rects[recruit_id] = rect display_root.add_child(_actor_target("P100CompanyTarget_%s" % recruit_id, rect.position, rect.size, tap_company_actor.bind(recruit_id))) var state_text := _tr("战", "LINE") if in_line else _tr("备", "RES") var label := _outlined_label("%s Lv%d\n%d/%d" % [state_text, int(record.level), int(record.current_hp), int(record.max_hp)], 7, WHITE) label.position = Vector2(x - 5, actor_y + actor_size - 1) label.size = Vector2(actor_size + 10, 32) label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER display_root.add_child(label) func _render_market_context() -> void: var band := ColorRect.new() band.name = "P100MarketContextBand" band.position = Vector2(0, size.y - 68) band.size = Vector2(size.x, 68) band.color = Color(0.015, 0.035, 0.075, 0.94) band.mouse_filter = Control.MOUSE_FILTER_IGNORE display_root.add_child(band) var detail := _outlined_label(_market_context_text(), 9, WHITE) detail.name = "P100ContextText" detail.position = Vector2(14, size.y - 64) detail.size = Vector2(size.x - 400, 58) detail.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART display_root.add_child(detail) var auto_line := _secondary_button("P100AutoLineButton", _tr("自动排阵", "AUTO LINE"), use_automatic_line) auto_line.position = Vector2(size.x - 380, size.y - 62) auto_line.size = Vector2(110, 52) display_root.add_child(auto_line) var commit := _action_button("P100CommitLineButton", _tr("确认战线", "COMMIT LINE"), commit_pending_line) commit.position = Vector2(size.x - 262, size.y - 62) commit.size = Vector2(136, 52) commit.disabled = pending_line == run_deployed() display_root.add_child(commit) var refresh_cost := P100MarketService.refresh_cost(int(market.state.current_run.refresh_count), market.state.current_run.locks.size()) var refresh := _secondary_button("P100RefreshButton", _tr("刷新 ◆%d" % refresh_cost, "REFRESH ◆%d" % refresh_cost), refresh_market) refresh.position = Vector2(size.x - 120, size.y - 62) refresh.size = Vector2(112, 52) refresh.disabled = int(market.state.current_run.coin) < refresh_cost display_root.add_child(refresh) func _render_boss_gate_in_market() -> void: var boss_id := String(contract_rows[String(market.state.current_run.contract_id)].boss_id) var boss := _add_actor("Riftblade", Vector2(size.x * 0.64, size.y * 0.28), 132, "P100BossGateActor", true, "telegraph") boss.self_modulate = VIOLET var title := _outlined_label(_tr("区域首领:%s" % _row_name(boss_id, "boss"), "REGION BOSS: %s" % _row_name(boss_id, "boss")), 21, GOLD_LIGHT) title.position = Vector2(size.x * 0.43, 72) title.size = Vector2(size.x * 0.52, 54) display_root.add_child(title) var enter := _action_button("P100EnterBossButton", _tr("进入集结", "ENTER MUSTER"), enter_boss_muster) enter.position = Vector2(size.x - 230, size.y * 0.56) enter.size = Vector2(206, 54) display_root.add_child(enter) func _render_reward() -> void: _add_background(WINDGLASS_BACKGROUND) _add_header(_tr("战利品已落地", "REWARD LANDED"), _tr("只选择一项", "CHOOSE ONE")) _render_surviving_company(Vector2(22, size.y * 0.42), 58) var draft: Dictionary = market.state.current_run.pending_reward var cards: Array = draft.cards var start_x := size.x * 0.47 var width := (size.x * 0.50 - 24.0) / maxf(1.0, float(cards.size())) for index in range(cards.size()): var card: Dictionary = cards[index] var card_id := String(card.card_id) var x := start_x + index * width var object := TextureRect.new() object.name = "P100RewardObject_%s" % card_id object.texture = _equipment_texture(String(card.get("catalog_id", "EQ-002"))) object.position = Vector2(x + width * 0.20, size.y * 0.35) object.size = Vector2(width * 0.60, 70) object.expand_mode = TextureRect.EXPAND_IGNORE_SIZE object.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED object.self_modulate = _reward_color(String(card.kind)) object.mouse_filter = Control.MOUSE_FILTER_IGNORE display_root.add_child(object) if String(card.kind) == "artifact": for option_index in range(card.artifact_ids.size()): var artifact_id := String(card.artifact_ids[option_index]) var option := _secondary_button("P100RewardArtifact_%s" % artifact_id, artifact_id, claim_reward_card.bind(card_id, artifact_id)) option.position = Vector2(x + 2, size.y - 64 - option_index * 50) option.size = Vector2(width - 8, 46) display_root.add_child(option) else: var label := _reward_label(card) var choose := _action_button("P100RewardTarget_%s" % card_id, label, claim_reward_card.bind(card_id, "")) choose.position = Vector2(x + 2, size.y - 66) choose.size = Vector2(width - 8, 52) display_root.add_child(choose) func _render_boss_muster() -> void: _add_background(WINDGLASS_BACKGROUND) var muster: Dictionary = market.state.current_run.boss_muster var boss_id := String(muster.boss_id) _add_header(_tr("首领集结:%s" % _row_name(boss_id, "boss"), "BOSS MUSTER: %s" % _row_name(boss_id, "boss")), _tr("选择一个战术并再次确认", "INSPECT, THEN CONFIRM A PLAN")) _render_surviving_company(Vector2(24, size.y * 0.48), 64) var boss := _add_actor("Riftblade", Vector2(size.x * 0.70, size.y * 0.25), 150, "P100MusterBoss", true, "telegraph") boss.self_modulate = VIOLET for index in range(muster.plans.size()): var plan_id := String(muster.plans[index]) var plan := _secondary_button("P100BossPlan_%s" % plan_id, _boss_plan_label(plan_id), tap_boss_plan.bind(plan_id)) plan.position = Vector2(size.x - 330 + index * 158, size.y - 78) plan.size = Vector2(148, 58) if focused_boss_plan == plan_id: plan.add_theme_stylebox_override("normal", _flat_style(Color(0.30, 0.18, 0.05, 0.94), GOLD_LIGHT, 3, 4)) display_root.add_child(plan) func _render_battle() -> void: _add_background(WINDGLASS_BACKGROUND) var boss_id := String(battle_snapshot.get("boss_id", "")) var heading := _row_name(boss_id, "boss") if boss_battle_active else _tr("第 %d 集市 · 风险 %d" % [int(battle_snapshot.get("market_index", 0)), int(battle_snapshot.get("risk", 0))], "MARKET %d · RISK %d" % [int(battle_snapshot.get("market_index", 0)), int(battle_snapshot.get("risk", 0))]) _add_header(heading, _tr("第 %d 刻" % int(battle_snapshot.get("tick", 0)), "TICK %d" % int(battle_snapshot.get("tick", 0)))) var allies: Array = battle_snapshot.get("ally_order", []) var actor_rows: Dictionary = battle_snapshot.get("actors", {}) var ally_spacing := minf(64.0, (size.x * 0.46 - 50.0) / maxf(1.0, float(allies.size()))) for index in range(allies.size()): var recruit_id := String(allies[index]) if not actor_rows.has(recruit_id): continue _add_runtime_battle_actor(recruit_id, actor_rows[recruit_id], Vector2(34 + index * ally_spacing, size.y * 0.48), false) if boss_battle_active: _add_runtime_boss(Vector2(size.x * 0.72, size.y * 0.34)) else: var enemies: Array = battle_snapshot.get("enemy_order", []) var enemy_spacing := minf(70.0, (size.x * 0.42 - 30.0) / maxf(1.0, float(enemies.size()))) for index in range(enemies.size()): var enemy_id := String(enemies[index]) if actor_rows.has(enemy_id): _add_runtime_battle_actor(enemy_id, actor_rows[enemy_id], Vector2(size.x - 84 - index * enemy_spacing, size.y * 0.43), true) _render_recent_feedback() var pause := _secondary_button("P100PauseButton", "▶" if battle_paused else "Ⅱ", toggle_battle_pause) pause.position = Vector2(size.x - 166, 1) pause.size = Vector2(48, 48) display_root.add_child(pause) var speed := _secondary_button("P100SpeedButton", "%dx" % battle_speed, toggle_battle_speed) speed.position = Vector2(size.x - 112, 1) speed.size = Vector2(48, 48) display_root.add_child(speed) if not boss_battle_active and not battle_result_visible: var retreat := _secondary_button("P100RetreatButton", _tr("撤退", "RETREAT"), request_retreat) retreat.position = Vector2(12, size.y - 58) retreat.size = Vector2(92, 48) retreat.disabled = int(battle_snapshot.get("retreat_requested_tick", -1)) >= 0 display_root.add_child(retreat) if battle_result_visible: _render_battle_result_band() func _render_contract_result() -> void: _add_background(WINDGLASS_BACKGROUND) var pending: Dictionary = market.state.current_run.pending_close _render_surviving_company(Vector2(24, size.y * 0.46), 66) var title := _outlined_label(_tr("契约完成", "CONTRACT COMPLETE"), 28, GOLD_LIGHT) title.position = Vector2(size.x * 0.48, size.y * 0.24) title.size = Vector2(size.x * 0.48, 62) title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER display_root.add_child(title) var body := _outlined_label(_tr("首领 %s 已击败\n首次通关声望 +%d\n幸存队伍将写入公会记录" % [_row_name(String(pending.boss_id), "boss"), int(pending.first_clear_renown)], "BOSS %s DEFEATED\nFIRST-CLEAR RENOWN +%d\nSURVIVORS ENTER THE COMPANY RECORD" % [_row_name(String(pending.boss_id), "boss"), int(pending.first_clear_renown)]), 13, WHITE) body.position = Vector2(size.x * 0.51, size.y * 0.41) body.size = Vector2(size.x * 0.42, 110) body.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER body.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART display_root.add_child(body) var close := _action_button("P100CloseContractButton", _tr("登记通关", "RECORD CLEAR"), close_contract) close.position = Vector2(size.x - 238, size.y - 66) close.size = Vector2(218, 52) display_root.add_child(close) func _render_guild_result() -> void: _add_background(HALL_BACKGROUND) var profile: Dictionary = market.state.guild_profile var clears: Array = profile.contract_clear_ids _add_header(_tr("风晶公会记录", "WINDGLASS GUILD RECORD"), _tr("声望 %d · 通关 %d/2" % [profile.persistent_renown, clears.size()], "RENOWN %d · CLEARS %d/2" % [profile.persistent_renown, clears.size()])) for index in range(profile.company_records.size()): var record: Dictionary = profile.company_records[index] var seal := ColorRect.new() seal.name = "P100CompanyRecord_%d" % index seal.position = Vector2(34 + index * 260, 92) seal.size = Vector2(224, 6) seal.color = GOLD if index == 0 else CYAN seal.mouse_filter = Control.MOUSE_FILTER_IGNORE display_root.add_child(seal) var contract_name := _contract_name(String(record.contract_id)) var label := _outlined_label(_tr("队伍记录 %d\n幸存 %d · 阵亡 %d\n%s" % [index + 1, record.survivors.size(), record.fallen_total, contract_name], "COMPANY RECORD %d\nSURVIVORS %d · FALLEN %d\n%s" % [index + 1, record.survivors.size(), record.fallen_total, contract_name]), 12, WHITE) label.position = Vector2(34 + index * 260, 104) label.size = Vector2(224, 100) label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART display_root.add_child(label) var rg2_visible: bool = "RG-02" in profile.unlocked_region_ids var region := _outlined_label(_tr("RG-02 已显现\n下一地区尚未开放" if rg2_visible else "第二份风晶契约已开放", "RG-02 REVEALED\nNEXT REGION NOT YET PLAYABLE" if rg2_visible else "SECOND WINDGLASS CONTRACT OPEN"), 22, GOLD_LIGHT if rg2_visible else CYAN) region.name = "P100RegionSeal_RG-02" if rg2_visible else "P100ContractTwoSeal" region.position = Vector2(size.x * 0.56, size.y * 0.30) region.size = Vector2(size.x * 0.38, 112) region.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER region.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART display_root.add_child(region) if not rg2_visible: var next := _action_button("P100NextContractButton", _tr("开始第二契约", "START CONTRACT TWO"), begin_expedition) next.position = Vector2(size.x - 246, size.y - 68) next.size = Vector2(226, 54) display_root.add_child(next) func _render_battle_result_band() -> void: var band := ColorRect.new() band.name = "P100BattleResultBand" band.position = Vector2(0, size.y - 72) band.size = Vector2(size.x, 72) band.color = Color(0.015, 0.035, 0.075, 0.95) band.mouse_filter = Control.MOUSE_FILTER_IGNORE display_root.add_child(band) var victory := String(battle_snapshot.outcome) == "victory" var fallen_count: int = battle_snapshot.get("fallen_recruit_ids", []).size() var label := _outlined_label(_tr("胜利" if victory else "战斗结束", "VICTORY" if victory else "BATTLE ENDED") + _tr(" · 永久阵亡 %d" % fallen_count, " · PERMANENT FALLEN %d" % fallen_count), 19, GOLD_LIGHT if victory else CORAL) label.position = Vector2(18, size.y - 66) label.size = Vector2(size.x - 260, 56) display_root.add_child(label) var settle := _action_button("P100SettleButton", _tr("结算后返回", "SETTLE RESULT"), settle_and_return) settle.position = Vector2(size.x - 230, size.y - 62) settle.size = Vector2(212, 52) display_root.add_child(settle) func _render_surviving_company(origin: Vector2, actor_size: float) -> void: if market.state.current_run.is_empty(): return var company: Array = market.state.current_run.company var spacing := minf(actor_size + 4, size.x * 0.43 / maxf(1.0, float(company.size()))) for index in range(company.size()): var recruit_id := String(company[index]) var record: Dictionary = market.state.current_run.recruit_records[recruit_id] var actor := _add_actor(_p100_recruit_rig(record), origin + Vector2(index * spacing, 0), actor_size, "P100Survivor_%s" % recruit_id, true) actor.self_modulate = _p100_profession_color(String(record.profession_id)) func _add_runtime_battle_actor(actor_id: String, row: Dictionary, at: Vector2, enemy: bool) -> void: var rig := "Riftblade" if enemy else _p100_recruit_rig(row) var actor_size := 68.0 if enemy else 64.0 var state_name := _battle_state_for(actor_id, enemy) var actor := _add_actor(rig, at, actor_size, "P100BattleActor_%s" % actor_id, false, state_name) actor.self_modulate = _enemy_color(String(row.get("blueprint_id", ""))) if enemy else _p100_profession_color(String(row.get("profession_id", ""))) actor.set_meta("base_position", at) actor.set_meta("rig", rig) actor.set_meta("animation_state", state_name) battle_actors[actor_id] = actor _add_hp_bar(actor_id, actor, int(row.hp), int(row.max_hp), enemy) func _add_runtime_boss(at: Vector2) -> void: var actor := _add_actor("Riftblade", at, 128, "P100BattleBoss", false, "attack" if not battle_result_visible else "death") actor.self_modulate = VIOLET actor.set_meta("base_position", at) actor.set_meta("rig", "Riftblade") actor.set_meta("animation_state", "attack") battle_actors[String(battle_snapshot.boss_id)] = actor _add_hp_bar(String(battle_snapshot.boss_id), actor, int(battle_snapshot.boss_hp), int(battle_snapshot.boss_max_hp), true) func _add_hp_bar(actor_id: String, actor: TextureRect, hp: int, max_hp: int, enemy: bool) -> void: var bar := ColorRect.new() bar.name = "P100HP_%s" % actor_id bar.position = actor.position + Vector2(4, -7) bar.size = Vector2(actor.size.x - 8, 7) bar.color = Color(0.01, 0.02, 0.04, 0.94) bar.mouse_filter = Control.MOUSE_FILTER_IGNORE display_root.add_child(bar) var fill := ColorRect.new() fill.name = "Fill" fill.position = Vector2(1, 1) fill.size = Vector2((bar.size.x - 2) * clampf(float(hp) / maxf(1.0, float(max_hp)), 0.0, 1.0), 5) fill.color = CORAL if enemy else JADE fill.mouse_filter = Control.MOUSE_FILTER_IGNORE bar.add_child(fill) battle_hp_bars[actor_id] = bar var name := _outlined_label(_battle_actor_name(actor_id), 7, WHITE) name.position = actor.position + Vector2(-8, actor.size.y - 4) name.size = Vector2(actor.size.x + 16, 24) name.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER name.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS display_root.add_child(name) func _render_recent_feedback() -> void: for event in recent_events: if float(event.get("expires", 0.0)) < world_clock: continue var kind := String(event.kind) var target_id := String(event.get("target_id", "")) var actor: TextureRect = battle_actors.get(target_id) if actor == null: continue var detail: Dictionary = event.get("detail", {}) var value := "" var color := WHITE if kind == "damage": value = "-%d" % int(detail.get("effective", detail.get("amount", 0))) color = CORAL elif kind == "heal": value = "+%d" % int(detail.get("amount", 0)) color = JADE elif kind == "fallen": value = _tr("阵亡", "FALLEN") color = CORAL elif kind in ["guard", "intercept", "push_resisted"]: value = _tr("守护", "GUARD") color = CYAN if value.is_empty(): continue var label := _outlined_label(value, 14, color) label.name = "P100Feedback_%s" % String(event.event_id) label.position = actor.position + Vector2(-10, -34) label.size = Vector2(actor.size.x + 20, 30) label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER label.z_index = 30 display_root.add_child(label) func _animate_runtime_battle_actors() -> void: if reduced_motion or battle_actors.is_empty(): return for actor_id in battle_actors: var actor: TextureRect = battle_actors[actor_id] if not is_instance_valid(actor) or not actor.has_meta("base_position"): continue var base: Vector2 = actor.get_meta("base_position") var phase := float(abs(String(actor_id).hash()) % 17) * 0.17 actor.position = base + Vector2(sin(world_clock * 2.6 + phase) * 2.0, sin(world_clock * 5.2 + phase) * 1.5) actor.texture = animation.texture(String(actor.get_meta("rig", "Hearthguard")), String(actor.get_meta("animation_state", "idle")), world_clock) func _add_header(title_text: String, status_text: String) -> void: var header := ColorRect.new() header.name = "P100Header" header.position = Vector2.ZERO header.size = Vector2(size.x, 50) header.color = Color(0.015, 0.035, 0.075, 0.90) header.mouse_filter = Control.MOUSE_FILTER_IGNORE display_root.add_child(header) var title := _outlined_label(title_text, 15, GOLD_LIGHT) title.position = Vector2(14, 3) title.size = Vector2(size.x * 0.52, 43) title.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS display_root.add_child(title) var status := _outlined_label(status_text, 10, WHITE) status.position = Vector2(size.x * 0.48, 3) status.size = Vector2(size.x * 0.45 - 8, 43) status.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT status.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS display_root.add_child(status) func _add_settings_button() -> void: var button := _secondary_button("P100SettingsButton", "⚙", toggle_settings) button.position = Vector2(size.x - 52, 1) button.size = Vector2(48, 48) button.tooltip_text = _tr("设置", "SETTINGS") display_root.add_child(button) func _render_settings_band() -> void: var scrim := ColorRect.new() scrim.name = "P100SettingsScrim" scrim.position = Vector2.ZERO scrim.size = size scrim.color = Color(0.01, 0.025, 0.05, 0.92) scrim.z_index = 79 scrim.mouse_filter = Control.MOUSE_FILTER_STOP display_root.add_child(scrim) var band := ColorRect.new() band.name = "P100SettingsBand" band.position = Vector2(size.x * 0.18, 54) band.size = Vector2(size.x * 0.64, size.y - 108) band.color = Color(0.015, 0.035, 0.075, 1.0) band.z_index = 80 band.mouse_filter = Control.MOUSE_FILTER_STOP display_root.add_child(band) var title := _outlined_label(_tr("设置", "SETTINGS"), 21, GOLD_LIGHT) title.position = band.position + Vector2(18, 10) title.size = Vector2(180, 42) title.z_index = 81 display_root.add_child(title) var values := [ [_tr("语言", "LANGUAGE"), "中文" if locale != "en" else "EN", toggle_locale], [_tr("文字", "TEXT"), "130%" if text_scale > 1.0 else "100%", toggle_text_scale], [_tr("动态", "MOTION"), _tr("减少", "REDUCED") if reduced_motion else _tr("完整", "FULL"), toggle_reduced_motion], [_tr("声音", "AUDIO"), _tr("静音", "MUTED") if muted else _tr("开启", "ON"), toggle_mute], [_tr("功耗", "POWER"), _tr("低", "LOW") if low_power else _tr("标准", "NORMAL"), toggle_low_power], ] for index in range(values.size()): var row: Array = values[index] var button := _secondary_button("P100Setting_%d" % index, "%s %s" % [row[0], row[1]], row[2]) button.position = band.position + Vector2(18 + (index % 3) * ((band.size.x - 48) / 3.0), 60 + int(index / 3) * 60) button.size = Vector2((band.size.x - 66) / 3.0, 50) button.z_index = 82 display_root.add_child(button) var reset := _secondary_button("P100ResetButton", _tr("再次确认重置" if reset_confirmation else "重置进度", "CONFIRM RESET" if reset_confirmation else "RESET PROGRESS"), reset_progress) reset.position = band.position + Vector2(18, band.size.y - 62) reset.size = Vector2(174, 48) reset.z_index = 82 display_root.add_child(reset) var close := _action_button("P100CloseSettingsButton", _tr("返回", "BACK"), toggle_settings) close.position = band.position + Vector2(band.size.x - 152, band.size.y - 62) close.size = Vector2(134, 48) close.z_index = 82 display_root.add_child(close) func _market_context_text() -> String: if not focused_recruit_offer.is_empty(): var offer: Dictionary = market.inspect_recruit_offer(focused_recruit_offer) if offer.ok: return _tr("再次点选招募:%s · %s · %s · ◆%d" % [_generated_name(offer.receipt), _row_name(String(offer.receipt.profession_id), "profession"), _row_name(String(offer.receipt.trait_id), "trait"), int(offer.receipt.list_price)], "TAP AGAIN TO HIRE: %s · %s · %s · ◆%d" % [_generated_name(offer.receipt), _row_name(String(offer.receipt.profession_id), "profession"), _row_name(String(offer.receipt.trait_id), "trait"), int(offer.receipt.list_price)]) if not focused_equipment_offer.is_empty(): var offer: Dictionary = market.inspect_equipment_offer(focused_equipment_offer) if offer.ok: return _tr("选择一名队员购买并装备 %s · ◆%d" % [_row_name(String(offer.receipt.catalog_id), "equipment"), int(offer.receipt.list_price)], "CHOOSE A RECRUIT TO BUY AND EQUIP %s · ◆%d" % [_row_name(String(offer.receipt.catalog_id), "equipment"), int(offer.receipt.list_price)]) if not focused_battle_offer.is_empty(): var offer: Dictionary = market.preview_battle_offer(focused_battle_offer) if offer.ok: return _tr("再次点选出发:风险 %d · %d 波 · 敌人 %s" % [int(offer.receipt.risk), int(offer.receipt.wave_count), ", ".join(offer.receipt.enemy_ids)], "TAP AGAIN TO DEPART: RISK %d · %d WAVES · %s" % [int(offer.receipt.risk), int(offer.receipt.wave_count), ", ".join(offer.receipt.enemy_ids)]) if not focused_company_actor.is_empty(): var record: Dictionary = market.state.current_run.recruit_records[focused_company_actor] return "%s · %s · %s · Lv.%d %d/%dHP" % [_generated_name(record), _row_name(String(record.profession_id), "profession"), _row_name(String(record.trait_id), "trait"), int(record.level), int(record.current_hp), int(record.max_hp)] if bool(market.state.current_run.get("recovery_required", false)): return _tr("阵亡不会复活。先点选救援招募查看,再点一次用信用与金币补员;然后重新确认战线。", "FALLEN RECRUITS DO NOT RETURN. INSPECT THE PROTECTED RECRUIT, TAP AGAIN TO HIRE WITH CREDIT/COIN, THEN RECOMMIT THE LINE.") return _tr("点角色查看,再点一次切换战线/预备;拖动战线角色可重排。点商品后再点角色可装备。", "TAP A RECRUIT TO INSPECT, AGAIN TO MOVE LINE/RESERVE. DRAG LINE RECRUITS TO REORDER. SELECT GEAR, THEN A RECRUIT.") func run_deployed() -> Array: return Array(market.state.current_run.deployed_recruit_ids).duplicate() func _battle_state_for(actor_id: String, enemy: bool) -> String: for event_index in range(recent_events.size() - 1, -1, -1): var event: Dictionary = recent_events[event_index] if String(event.get("source_id", "")) != actor_id: continue if enemy: return "attack" var record: Dictionary = battle_snapshot.get("actors", {}).get(actor_id, {}) var rig: String = _p100_recruit_rig(record) if String(event.kind) == "heal": return "lantern_heal" if rig == "Kindhand" else "idle" return "draw_release" if rig == "Farstring" else "evade_counter" if rig == "Wayrunner" else "basic_attack" return "advance" func _reward_label(card: Dictionary) -> String: match String(card.kind): "coin": return _tr("金币 +%d" % int(card.amount), "COIN +%d" % int(card.amount)) "credit": return _tr("信用 +%d" % int(card.amount), "CREDIT +%d" % int(card.amount)) "equipment": return _row_name(String(card.catalog_id), "equipment") return String(card.kind).to_upper() func _reward_color(kind: String) -> Color: return {"coin": GOLD_LIGHT, "credit": CYAN, "equipment": WHITE, "artifact": VIOLET}.get(kind, WHITE) func _difficulty_label(value: String) -> String: return { "wayfinder": _tr("引路者", "WAYFINDER"), "standard": _tr("远征者", "STANDARD"), "stormbound": _tr("逐风者", "STORMBOUND"), "iron_oath": _tr("铁誓", "IRON OATH"), }.get(value, value) func _contract_name(contract_id: String) -> String: return _row_name(contract_id, "contract") func _boss_plan_label(plan_id: String) -> String: return { "turn_panes": _tr("转动镜片", "TURN PANES"), "brace_sweep": _tr("稳守横扫", "BRACE SWEEP"), "break_spindle": _tr("击断主轴", "BREAK SPINDLE"), "hold_line": _tr("守住战线", "HOLD LINE"), }.get(plan_id, plan_id.replace("_", " ").to_upper()) func _generated_name(record: Dictionary) -> String: var tokens: Array = record.get("display_name_tokens", []) if tokens.is_empty(): return String(record.get("recruit_id", "Recruit")).left(8) var names: Array[String] = [] for token in tokens: var raw := String(token) names.append(raw.get_slice(".", raw.get_slice_count(".") - 1)) return (" · " if locale != "en" else " ").join(names) func _row_name(row_id: String, family: String) -> String: if row_id.is_empty(): return "" var localized: String = P100Content.localized_name(content, row_id, locale) if not localized.is_empty(): return localized var rows: Dictionary = { "profession": profession_rows, "trait": trait_rows, "equipment": equipment_rows, "enemy": enemy_rows, "boss": boss_rows, "contract": contract_rows, }.get(family, {}) return String(rows.get(row_id, {}).get("name", row_id)) func _short_profession(profession_id: String) -> String: return _row_name(profession_id, "profession").left(10) func _p100_recruit_rig(record: Dictionary) -> String: var profession_id := String(record.get("profession_id", "PF-B01")) var tags: Array = profession_rows.get(profession_id, {}).get("semantic_tags", []) for role in ROLE_RIGS: if role in tags: return String(ROLE_RIGS[role]) return "Hearthguard" func _p100_profession_color(profession_id: String) -> Color: var number := int(profession_id.trim_prefix("PF-B").trim_prefix("PF-A")) return [GOLD_LIGHT, CORAL, CYAN, JADE, Color("#78bfff"), VIOLET][number % 6] func _recruit_record(recruit_id: String) -> Dictionary: if market != null and not market.state.current_run.is_empty() and market.state.current_run.recruit_records.has(recruit_id): return market.state.current_run.recruit_records[recruit_id] return {} func _recruit_rig(recruit_id: String) -> String: return _p100_recruit_rig(_recruit_record(recruit_id)) func _recruit_color(recruit_id: String) -> Color: return _p100_profession_color(String(_recruit_record(recruit_id).get("profession_id", "PF-B01"))) func _equipment_name(catalog_id: String) -> String: return _row_name(catalog_id, "equipment") func _equipment_color(catalog_id: String) -> Color: var number := int(catalog_id.trim_prefix("EQ-")) return [WHITE, CYAN, GOLD_LIGHT, VIOLET, JADE, CORAL][number % 6] func _enemy_color(enemy_id: String) -> Color: var number := int(enemy_id.trim_prefix("EN-R1-")) return [Color("#d2ecf3"), CYAN, Color("#e7b779"), CORAL, Color("#a9c0db"), JADE, VIOLET][number % 7] func _battle_actor_name(actor_id: String) -> String: if actor_id.begins_with("W"): var row: Dictionary = battle_snapshot.get("actors", {}).get(actor_id, {}) return _row_name(String(row.get("blueprint_id", "")), "enemy") if actor_id.begins_with("BO-"): return _row_name(actor_id, "boss") var record := _recruit_record(actor_id) return _short_profession(String(record.get("profession_id", ""))) func _battle_event_word(event_id: String) -> String: return { "wave_arrived": _tr("敌军抵达", "WAVE ARRIVED"), "intercept": _tr("拦截", "INTERCEPT"), "push": _tr("推位", "PUSH"), "reorder": _tr("换位", "REORDER"), "fallen": _tr("阵亡", "FALLEN"), "guard": _tr("守护", "GUARD"), "hinder": _tr("迟滞", "HINDER"), }.get(event_id, event_id.replace("_", " ").to_upper().left(14))