class_name P102RuntimeController extends "res://p10_1/runtime_controller.gd" const P101State = preload("res://p10_1/region_run_state.gd") const P101Save = preload("res://p10_1/save_v7_service.gd") const P102Content = preload("res://p10_2/content_registry.gd") const P102BoardService = preload("res://p10_2/market_board_service.gd") const P102State = preload("res://p10_2/run_state.gd") const P102MarketService = preload("res://p10_2/market_service.gd") const P102BattleService = preload("res://p10_2/mid_contract_battle_service.gd") const P102ResolutionService = preload("res://p10_2/mid_contract_resolution_service.gd") const P102Save = preload("res://p10_2/save_v8_service.gd") const P102Motion = preload("res://p10_2/motion_presenter.gd") const P102_SAVE_PATH := "user://aetherbound_guild_p10_2.json" const P101_SAVE_PATH := "user://aetherbound_guild_p10_1.json" const P102_SCREEN_MARKET := "siltwake_market" const P102_SCREEN_PLAN := "siltwake_plan" const P102_SCREEN_BATTLE := "siltwake_battle" const P102_SCREEN_RECOVERY := "siltwake_recovery" const P102_SCREEN_MARKET_5 := "siltwake_market_5" var p102_mode := false var p102_catalog: Dictionary = P102Content.snapshot() var p102_save = P102Save.new() var p102_board = P102BoardService.new() var p102_focused_company := "" func _bootstrap_market() -> void: p102_mode = false var snapshot := _p102_entry_snapshot() if snapshot.is_empty() and persistence_enabled: var loaded: Dictionary = p102_save.load_slot(ProjectSettings.globalize_path(P102_SAVE_PATH)) if loaded.ok: snapshot = Dictionary(loaded.snapshot).duplicate(true) region_save_sequence = int(loaded.sequence) region_save_commit_hash = String(loaded.commit_hash) if snapshot.is_empty() and persistence_enabled: var legacy: Dictionary = P101Save.new().load_slot(ProjectSettings.globalize_path(P101_SAVE_PATH)) if legacy.ok: snapshot = _migrate_market_two(Dictionary(legacy.snapshot)) region_save_sequence = int(legacy.get("sequence", 0)) region_save_commit_hash = "" if snapshot.is_empty(): super._bootstrap_market() return _activate_p102(snapshot) func _p102_entry_snapshot() -> Dictionary: if String(entry_snapshot.get("state_kind", "")) == P102State.STATE_KIND: return entry_snapshot.duplicate(true) if String(entry_snapshot.get("state_kind", "")) == P101State.STATE_KIND: return _migrate_market_two(entry_snapshot) return {} func _migrate_market_two(snapshot: Dictionary) -> Dictionary: var source := P101State.new() if not source.restore_snapshot(snapshot): return {} var run: Dictionary = source.current_run if String(run.get("phase", "")) != "market_2" or int(run.get("market_index", -1)) != 2: return {} var generated: Dictionary = p102_board.generate(String(run.run_seed), 2, 0) if not generated.ok: return {} var migrated := P102State.new() return migrated.snapshot() if migrated.initialize_from_p101(source.snapshot(), generated.board) else {} func _activate_p102(snapshot: Dictionary) -> bool: var service = P102MarketService.new(snapshot) if not service.valid: return false region_market = service region_mode = true p102_mode = true region_catalog = p102_catalog region_motion = P102Motion.new() _prepare_base_content() _restore_p102_screen() _reset_region_focus() p102_focused_company = "" last_code = "p102_resumed" _queue_render() return true func _restore_region_screen() -> void: if not p102_mode: super._restore_region_screen() return _restore_p102_screen() func _restore_p102_screen() -> void: if region_market == null or region_market.state.current_run.is_empty(): screen = P102_SCREEN_MARKET return match String(region_market.state.current_run.phase): "prepared": screen = P102_SCREEN_PLAN "recovery": screen = P102_SCREEN_RECOVERY "market_5": screen = P102_SCREEN_MARKET_5 _: screen = P102_SCREEN_MARKET func _process(delta: float) -> void: if not p102_mode: super._process(delta) return if screen == P102_SCREEN_BATTLE and not region_battle_paused: region_motion_clock += maxf(0.0, delta) if not low_power: world_clock += maxf(0.0, delta) _update_ambient_actors() _animate_region_battle_actors() if ( screen != P102_SCREEN_BATTLE or region_battle_complete or region_battle_paused or not battle_auto_advance ): return if region_motion.playback_held(region_motion_clock, reduced_motion): return region_battle_accumulator += maxf(0.0, delta) * float(region_battle_speed) var ticks := int(floor(region_battle_accumulator / REGION_TICK_SECONDS)) if ticks > 0: region_battle_accumulator -= float(ticks) * REGION_TICK_SECONDS advance_region_battle_ticks(ticks) func _render_current() -> void: if not p102_mode: super._render_current() return if display_root == null: return _clear_display() line_touch_rects.clear() set_meta("visible_world_ratio", 0.88) if size.y > size.x: _render_rotate_notice() else: match screen: P102_SCREEN_PLAN: _render_p102_plan() P102_SCREEN_BATTLE: _render_region_battle() P102_SCREEN_RECOVERY: _render_p102_recovery() P102_SCREEN_MARKET_5: _render_p102_market_five() _: _render_p102_market() _add_settings_button() if settings_open: _render_settings_band() func tap_region_recruit(offer_id: String) -> void: if not p102_mode: super.tap_region_recruit(offer_id) return if not _p102_market_ready(): return if region_focused_recruit != offer_id: region_focused_recruit = offer_id region_focused_equipment = "" region_focused_route = "" last_code = "recruit_inspected" _queue_render() return var offer := _region_offer("recruit", offer_id) if offer.is_empty(): return var result: Dictionary = region_market.hire( _region_operation("hire", offer_id), region_market.state.revision, offer_id, int(offer.list_price) ) _accept_region_result(result) if result.ok: region_focused_recruit = "" pending_line.assign(region_market.state.current_run.deployed_recruit_ids) func tap_region_equipment(offer_id: String) -> void: if not p102_mode: super.tap_region_equipment(offer_id) return if not _p102_market_ready(): return if region_focused_equipment != offer_id: region_focused_equipment = offer_id region_focused_recruit = "" region_focused_route = "" last_code = "equipment_inspected" _queue_render() return var offer := _region_offer("equipment", offer_id) if offer.is_empty(): return var result: Dictionary = region_market.buy_equipment( _region_operation("buy", offer_id), region_market.state.revision, offer_id, int(offer.list_price) ) _accept_region_result(result) if result.ok: region_selected_instance = String(result.receipt.instance_id) region_focused_equipment = "" func tap_region_company(recruit_id: String) -> void: if not p102_mode: super.tap_region_company(recruit_id) return if not _p102_market_ready() or recruit_id not in region_market.state.current_run.company: return if not region_selected_instance.is_empty(): var equipped: Dictionary = region_market.equip( _region_operation("equip", recruit_id), region_market.state.revision, recruit_id, region_selected_instance ) _accept_region_result(equipped) if equipped.ok: region_selected_instance = "" p102_focused_company = recruit_id return var run: Dictionary = region_market.state.current_run if p102_focused_company == recruit_id: var deployment: Array = Array(run.deployed_recruit_ids).duplicate() if recruit_id in deployment and deployment.size() > 1: deployment.erase(recruit_id) _commit_p102_deployment(deployment, "to-reserve") return if recruit_id in run.reserve_recruit_ids and deployment.size() < int(run.line_capacity): deployment.append(recruit_id) _commit_p102_deployment(deployment, "to-line") return if not p102_focused_company.is_empty() and p102_focused_company != recruit_id: var one_deployed: bool = p102_focused_company in run.deployed_recruit_ids var other_deployed: bool = recruit_id in run.deployed_recruit_ids if one_deployed != other_deployed: _swap_deployed_and_reserve(p102_focused_company, recruit_id) return p102_focused_company = recruit_id region_focused_company = recruit_id last_code = "company_inspected" _queue_render() func tap_region_route(offer_id: String) -> void: if not p102_mode: super.tap_region_route(offer_id) return if not _p102_market_ready(): return region_focused_route = offer_id region_focused_recruit = "" region_focused_equipment = "" last_code = "route_inspected" _queue_render() func commit_region_route() -> void: if not p102_mode: super.commit_region_route() return if not _p102_market_ready() or region_focused_route.is_empty(): return var result: Dictionary = region_market.commit_route( _region_operation("route", region_focused_route), region_market.state.revision, region_focused_route, String(region_market.state.current_run.market_board.board_hash) ) _accept_region_result(result) if result.ok: screen = P102_SCREEN_PLAN _save_region() _queue_render() func choose_region_plan(plan_id: String) -> void: if not p102_mode: super.choose_region_plan(plan_id) return if region_market == null or String(region_market.state.current_run.get("phase", "")) != "prepared": return var result: Dictionary = P102BattleService.new(region_market.state.snapshot()).run_to_terminal(plan_id) last_code = String(result.code) if not result.ok: _queue_render() return region_plan_id = plan_id region_battle_snapshot = Dictionary(result.snapshot).duplicate(true) region_battle_tick = 0 region_battle_terminal_tick = int(region_battle_snapshot.tick) region_battle_accumulator = 0.0 region_battle_complete = false region_battle_paused = false region_battle_speed = 1 region_event_cursor = 0 region_recent_events.clear() region_event_counts.clear() region_actor_order = Array(region_market.state.current_run.deployed_recruit_ids).duplicate() region_display_hp.clear() for recruit_id in region_actor_order: region_display_hp[recruit_id] = int(region_market.state.current_run.recruit_records[recruit_id].current_hp) region_safe_index = int(region_market.state.current_run.pending_encounter.tide_disclosure.current_safe_index) region_motion_clock = 0.0 region_motion.reset(region_actor_order, region_safe_index) screen = P102_SCREEN_BATTLE _queue_render() func advance_region_battle_ticks(ticks: int) -> Dictionary: if not p102_mode: return super.advance_region_battle_ticks(ticks) if screen != P102_SCREEN_BATTLE or region_battle_complete or ticks <= 0: return {"ok": false, "code": "battle_inactive", "events": []} region_battle_tick = mini(region_battle_terminal_tick, region_battle_tick + ticks) var emitted: Array[Dictionary] = [] var events: Array = region_battle_snapshot.get("event_log", []) while region_event_cursor < events.size() and int(events[region_event_cursor].tick) <= region_battle_tick: var event: Dictionary = Dictionary(events[region_event_cursor]).duplicate(true) region_event_cursor += 1 emitted.append(event) _consume_region_event(event) if region_battle_tick >= region_battle_terminal_tick: region_battle_complete = true last_code = "mid_contract_playback_terminal" else: last_code = "mid_contract_playing" _queue_render() return {"ok": true, "code": last_code, "events": emitted, "tick": region_battle_tick} func toggle_region_battle_pause() -> void: if not p102_mode: super.toggle_region_battle_pause() return if screen == P102_SCREEN_BATTLE and not region_battle_complete: region_battle_paused = not region_battle_paused _queue_render() func toggle_region_battle_speed() -> void: if not p102_mode: super.toggle_region_battle_speed() return if screen == P102_SCREEN_BATTLE and not region_battle_complete: region_battle_speed = 2 if region_battle_speed == 1 else 1 _queue_render() func settle_region_battle() -> void: if not p102_mode: super.settle_region_battle() return if not region_battle_complete or region_battle_snapshot.is_empty(): return var resolver = P102ResolutionService.new(region_market.state.snapshot()) var result: Dictionary = resolver.settle( _region_operation("settle", String(region_battle_snapshot.snapshot_hash).left(8)), resolver.state.revision, region_battle_snapshot ) last_code = String(result.code) if not result.ok: _queue_render() return region_market = P102MarketService.new(result.snapshot) _reset_region_focus() p102_focused_company = "" _save_region() _restore_p102_screen() _queue_render() func recover_region_company() -> void: if not p102_mode: super.recover_region_company() return if region_market == null or String(region_market.state.current_run.get("phase", "")) != "recovery": return var run: Dictionary = region_market.state.current_run var report: Dictionary = run.last_battle_report var resolver = P102ResolutionService.new(region_market.state.snapshot()) var result: Dictionary = resolver.recover( _region_operation("recover", String(report.get("battle_hash", "")).left(8)), resolver.state.revision, String(report.get("battle_hash", "")), int(run.recovery_cost) ) last_code = String(result.code) if result.ok: region_market = P102MarketService.new(result.snapshot) pending_line.assign(region_market.state.current_run.deployed_recruit_ids) _reset_region_focus() p102_focused_company = "" _save_region() screen = P102_SCREEN_MARKET _queue_render() func _commit_line_drag(source_id: String, target_id: String) -> void: if not p102_mode: super._commit_line_drag(source_id, target_id) return if not _p102_market_ready() or source_id.is_empty() or target_id.is_empty() or source_id == target_id: return var run: Dictionary = region_market.state.current_run var source_deployed: bool = source_id in run.deployed_recruit_ids var target_deployed: bool = target_id in run.deployed_recruit_ids if source_deployed != target_deployed: _swap_deployed_and_reserve(source_id, target_id) return if not source_deployed: return var order: Array = Array(run.deployed_recruit_ids).duplicate() order.erase(source_id) order.insert(order.find(target_id), source_id) _commit_p102_deployment(order, "drag") func _swap_deployed_and_reserve(first_id: String, second_id: String) -> void: var run: Dictionary = region_market.state.current_run var deployed_id := first_id if first_id in run.deployed_recruit_ids else second_id var reserve_id := second_id if deployed_id == first_id else first_id if reserve_id not in run.reserve_recruit_ids: return var order: Array = Array(run.deployed_recruit_ids).duplicate() var index := order.find(deployed_id) if index < 0: return order[index] = reserve_id _commit_p102_deployment(order, "reserve-swap") func _commit_p102_deployment(order: Array, suffix: String) -> void: var result: Dictionary = region_market.commit_deployment( _region_operation("deploy", suffix), region_market.state.revision, order ) _accept_region_result(result) if result.ok: pending_line.assign(region_market.state.current_run.deployed_recruit_ids) p102_focused_company = "" func _consume_region_event(event: Dictionary) -> void: if not p102_mode: super._consume_region_event(event) return var kind := String(event.get("kind", "")) var payload: Dictionary = Dictionary(event.get("payload", {})) region_motion.consume(event, region_motion_clock) region_actor_order = Array(region_motion.actor_order).duplicate() region_event_counts[kind] = int(region_event_counts.get(kind, 0)) + 1 region_recent_events.append(event.duplicate(true)) if region_recent_events.size() > 4: region_recent_events.pop_front() if kind == "tide_shifted": region_safe_index = int(payload.get("safe_index", region_safe_index)) elif kind == "authoritative_damage": region_display_hp[String(event.get("target_id", ""))] = int(payload.get("hp_after", 0)) elif kind == "fallen": region_display_hp[String(event.get("target_id", ""))] = 0 func _save_region() -> bool: if not p102_mode: return super._save_region() if not persistence_enabled or region_market == null: return true var result: Dictionary = p102_save.write_slot( ProjectSettings.globalize_path(P102_SAVE_PATH), region_market.state.snapshot(), region_save_sequence + 1, region_save_commit_hash ) if not result.ok: last_code = "p102_%s" % String(result.code) return false region_save_sequence = int(result.sequence) region_save_commit_hash = String(result.commit_hash) return true func _region_operation(kind: String, target: String) -> String: if not p102_mode: return super._region_operation(kind, target) operation_index += 1 return "p102-ui-%s-%d-%d-%s" % [kind, region_market.state.revision, operation_index, target] func _region_offer(kind: String, offer_id: String) -> Dictionary: if not p102_mode: return super._region_offer(kind, offer_id) if region_market == null or region_market.state.current_run.is_empty(): return {} var field := String({"recruit": "recruit_offers", "equipment": "equipment_offers", "route": "battle_offers"}.get(kind, "")) var id_field := "source_offer_id" if kind == "recruit" else "offer_id" for offer in region_market.state.current_run.market_board.get(field, []): if String(offer.get(id_field, "")) == offer_id: return Dictionary(offer).duplicate(true) return {} func _p102_market_ready() -> bool: return ( p102_mode and region_market != null and not region_market.state.current_run.is_empty() and String(region_market.state.current_run.phase) == "market" ) func _render_p102_market() -> void: _add_siltwake_world() var run: Dictionary = region_market.state.current_run var tide: Dictionary = run.market_board.tide_disclosure _add_header( _tr("淤潮地 · 集市 %d" % run.market_index, "SILTWAKE · MARKET %d" % run.market_index), _tr("◆%d 战线 %d/%d 预备 %d 安全位 %d→%d" % [run.coin, run.deployed_recruit_ids.size(), run.line_capacity, run.reserve_recruit_ids.size(), tide.current_safe_index, tide.next_safe_index], "◆%d LINE %d/%d RESERVE %d SAFE %d→%d" % [run.coin, run.deployed_recruit_ids.size(), run.line_capacity, run.reserve_recruit_ids.size(), tide.current_safe_index, tide.next_safe_index]) ) _render_tide_notches(tide, 0) _render_p102_recruit_pier() _render_p102_equipment_bank() _render_p102_route_posts() _render_p102_company() _render_p102_context() func _render_p102_recruit_pier() -> void: var run: Dictionary = region_market.state.current_run var offers: Array = run.market_board.recruit_offers var actor_size := clampf(size.y * 0.18, 58.0, 72.0) var caption := _outlined_label(_tr("渡口候选", "FERRY RECRUITS"), 9, GOLD_LIGHT) caption.position = Vector2(18, 56) caption.size = Vector2(size.x * 0.34, 26) display_root.add_child(caption) for index in range(offers.size()): var offer: Dictionary = offers[index] var offer_id := String(offer.source_offer_id) var available: bool = offer_id in run.available_recruit_offer_ids var x := 18.0 + index * (actor_size + 8.0) var y := 82.0 _add_ground_ring(Vector2(x + actor_size * 0.5, y + actor_size - 3), actor_size * 0.72, GOLD if region_focused_recruit == offer_id else CYAN, "P102RecruitRing_%s" % offer_id) var actor := _add_actor(_p100_recruit_rig(offer), Vector2(x, y), actor_size, "P102RecruitActor_%s" % offer_id, true) actor.self_modulate = _p100_profession_color(String(offer.profession_id)) actor.modulate.a = 1.0 if available else 0.24 if available: display_root.add_child(_actor_target("P102RecruitTarget_%s" % offer_id, Vector2(x - 3, y - 4), Vector2(actor_size + 6, actor_size + 38), tap_region_recruit.bind(offer_id))) var label := _outlined_label("%s\nLv.%d ◆%d" % [_generated_name(offer).left(10), int(offer.level), int(offer.list_price)], 7, GOLD_LIGHT if region_focused_recruit == offer_id else WHITE) label.position = Vector2(x - 7, y + actor_size - 2) label.size = Vector2(actor_size + 14, 38) label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER display_root.add_child(label) func _render_p102_equipment_bank() -> void: var run: Dictionary = region_market.state.current_run var offers: Array = run.market_board.equipment_offers var x0 := size.x * 0.35 var width := size.x * 0.36 var spacing := width / 4.0 var caption := _outlined_label(_tr("防潮装备", "TIDE GEAR"), 9, GOLD_LIGHT) caption.position = Vector2(x0, 56) caption.size = Vector2(width, 26) display_root.add_child(caption) for index in range(offers.size()): var offer: Dictionary = offers[index] var offer_id := String(offer.offer_id) var available: bool = offer_id in run.available_equipment_offer_ids var x := x0 + index * spacing var color := _equipment_color(String(offer.catalog_id)) var icon := _outlined_label(_equipment_symbol(String(offer.catalog_id)), 22, color) icon.position = Vector2(x, 82) icon.size = Vector2(spacing - 4, 48) icon.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER display_root.add_child(icon) if available: display_root.add_child(_actor_target("P102EquipmentTarget_%s" % offer_id, Vector2(x, 80), Vector2(spacing - 4, 86), tap_region_equipment.bind(offer_id))) var label := _outlined_label("%s\n◆%d" % [_region_name(String(offer.catalog_id)).left(8), int(offer.list_price)], 7, GOLD_LIGHT if region_focused_equipment == offer_id else WHITE) label.position = Vector2(x - 4, 126) label.size = Vector2(spacing + 4, 40) label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER label.modulate.a = 1.0 if available else 0.3 display_root.add_child(label) func _render_p102_route_posts() -> void: var offers: Array = region_market.state.current_run.market_board.battle_offers var x := size.x - 202.0 var caption := _outlined_label(_tr("三条水路", "THREE ROUTES"), 9, GOLD_LIGHT) caption.position = Vector2(x, 56) caption.size = Vector2(184, 26) display_root.add_child(caption) for index in range(offers.size()): var offer: Dictionary = offers[index] var offer_id := String(offer.offer_id) var y := 82.0 + index * 54.0 var selected := region_focused_route == offer_id var line := ColorRect.new() line.name = "P102RouteLine_%s" % offer_id line.position = Vector2(x, y + 38) line.size = Vector2(174, 4) line.color = GOLD if selected else [JADE, CYAN, CORAL][int(offer.risk)] line.mouse_filter = Control.MOUSE_FILTER_IGNORE display_root.add_child(line) var label := _outlined_label(_tr("风险%d %s" % [offer.risk, _region_name(String(offer.route_id))], "RISK %d %s" % [offer.risk, _region_name(String(offer.route_id))]), 8, GOLD_LIGHT if selected else WHITE) label.position = Vector2(x, y) label.size = Vector2(174, 38) label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS display_root.add_child(label) display_root.add_child(_actor_target("P102RouteTarget_%s" % offer_id, Vector2(x - 4, y), Vector2(182, 48), tap_region_route.bind(offer_id))) func _render_p102_company() -> void: var run: Dictionary = region_market.state.current_run var actor_size := clampf(size.y * 0.15, 52.0, 64.0) var y := size.y - 150.0 var x0 := size.x * 0.20 var available_width := size.x * 0.52 var spacing := minf(actor_size + 8.0, available_width / maxf(1.0, float(run.line_capacity))) var caption := _outlined_label(_tr("战线 %d/%d · 预备 %d" % [run.deployed_recruit_ids.size(), run.line_capacity, run.reserve_recruit_ids.size()], "LINE %d/%d · RESERVE %d" % [run.deployed_recruit_ids.size(), run.line_capacity, run.reserve_recruit_ids.size()]), 9, GOLD_LIGHT) caption.position = Vector2(x0, y - 24) caption.size = Vector2(available_width, 22) display_root.add_child(caption) for slot in range(int(run.line_capacity)): var x := x0 + slot * spacing _add_ground_ring(Vector2(x + actor_size * 0.5, y + actor_size - 3), actor_size * 0.68, Color(0.36, 0.82, 0.64, 0.36), "P102CapacitySlot_%d" % slot) if slot >= run.deployed_recruit_ids.size(): continue var recruit_id := String(run.deployed_recruit_ids[slot]) _render_p102_company_actor(recruit_id, Vector2(x, y), actor_size, false) var reserve_x := size.x * 0.73 for index in range(run.reserve_recruit_ids.size()): var recruit_id := String(run.reserve_recruit_ids[index]) _render_p102_company_actor(recruit_id, Vector2(reserve_x + index * (actor_size * 0.72), y + 10), actor_size * 0.76, true) func _render_p102_company_actor(recruit_id: String, position_value: Vector2, actor_size: float, reserve: bool) -> void: var run: Dictionary = region_market.state.current_run var record: Dictionary = run.recruit_records[recruit_id] var prefix := "P102Reserve" if reserve else "P102Company" var ring_color := GOLD if p102_focused_company == recruit_id else VIOLET if reserve else JADE _add_ground_ring(position_value + Vector2(actor_size * 0.5, actor_size - 3), actor_size * 0.70, ring_color, "%sRing_%s" % [prefix, recruit_id]) var actor := _add_actor(_p100_recruit_rig(record), position_value, actor_size, "%sActor_%s" % [prefix, recruit_id], true) actor.self_modulate = _p100_profession_color(String(record.profession_id)) var rect := Rect2(position_value - Vector2(3, 4), Vector2(actor_size + 6, actor_size + 34)) line_touch_rects[recruit_id] = rect display_root.add_child(_actor_target("%sTarget_%s" % [prefix, recruit_id], rect.position, rect.size, tap_region_company.bind(recruit_id))) var item_id := String(run.equipped_by_recruit.get(recruit_id, "")) var catalog_id := String(run.inventory.get(item_id, {}).get("catalog_id", "")) var label := _outlined_label("Lv.%d %dHP\n%s" % [record.level, record.current_hp, _region_name(catalog_id).left(7) if catalog_id.begins_with("EQ-") else _short_profession(String(record.profession_id))], 7, WHITE) label.position = position_value + Vector2(-6, actor_size - 2) label.size = Vector2(actor_size + 12, 36) label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER display_root.add_child(label) func _render_p102_context() -> void: var band := ColorRect.new() band.name = "P102MarketContextBand" band.position = Vector2(0, size.y - 58) band.size = Vector2(size.x, 58) band.color = Color(0.015, 0.035, 0.075, 0.95) band.mouse_filter = Control.MOUSE_FILTER_IGNORE display_root.add_child(band) var context := _outlined_label(_p102_context_text(), 9, WHITE) context.position = Vector2(14, size.y - 54) context.size = Vector2(size.x - 242, 48) context.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART display_root.add_child(context) var depart := _action_button("P102PrepareRouteButton", _tr("准备第%d轮" % region_market.state.current_run.round_index, "PREPARE ROUND %d" % region_market.state.current_run.round_index), commit_region_route) depart.position = Vector2(size.x - 220, size.y - 54) depart.size = Vector2(204, 48) depart.disabled = region_focused_route.is_empty() display_root.add_child(depart) func _p102_context_text() -> String: if not region_selected_instance.is_empty(): var item: Dictionary = region_market.state.current_run.inventory.get(region_selected_instance, {}) return _tr("已购买 %s;点战线或预备队员替换其唯一装备。" % _region_name(String(item.get("catalog_id", ""))), "BOUGHT %s; TAP A LINE OR RESERVE RECRUIT TO REPLACE THEIR ONE ITEM." % _region_name(String(item.get("catalog_id", "")))) if not region_focused_recruit.is_empty(): var recruit := _region_offer("recruit", region_focused_recruit) return _tr("%s · Lv.%d · ◆%d;再点一次雇佣,超出容量会进入预备队。" % [_generated_name(recruit), recruit.level, recruit.list_price], "%s · LV.%d · ◆%d; TAP AGAIN TO HIRE. OVER CAPACITY ENTERS RESERVE." % [_generated_name(recruit), recruit.level, recruit.list_price]) if not region_focused_equipment.is_empty(): var equipment := _region_offer("equipment", region_focused_equipment) return _tr("%s · ◆%d;再点购买,然后点队员装备。" % [_region_name(String(equipment.catalog_id)), equipment.list_price], "%s · ◆%d; TAP AGAIN TO BUY, THEN TAP A RECRUIT." % [_region_name(String(equipment.catalog_id)), equipment.list_price]) if not region_focused_route.is_empty(): return _route_summary(int(_region_offer("route", region_focused_route).risk)) if not p102_focused_company.is_empty(): var run: Dictionary = region_market.state.current_run return _tr("已选队员:再点本人切换战线/预备,或点另一侧队员交换。", "RECRUIT SELECTED: TAP AGAIN TO MOVE LINE/RESERVE, OR TAP THE OTHER SIDE TO SWAP.") if p102_focused_company in run.company else "" return _tr("先看路线压力,再调整战线、预备队与装备;容量会在第2、4轮后增长。", "READ ROUTE PRESSURE, THEN ADAPT LINE, RESERVE AND GEAR. CAPACITY GROWS AFTER ROUNDS 2 AND 4.") func _render_p102_plan() -> void: _add_siltwake_world() var encounter: Dictionary = region_market.state.current_run.pending_encounter var tide: Dictionary = encounter.tide_disclosure _add_header(_tr("第%d轮 · 选择应对" % encounter.round_index, "ROUND %d · CHOOSE RESPONSE" % encounter.round_index), _tr("%s · 风险%d · 安全位 %d→%d" % [_region_name(String(encounter.route_id)), encounter.risk, tide.current_safe_index, tide.next_safe_index], "%s · RISK %d · SAFE %d→%d" % [_region_name(String(encounter.route_id)), encounter.risk, tide.current_safe_index, tide.next_safe_index])) _render_tide_notches(tide, 0) _render_plan_company() var hint := _outlined_label(_route_summary(int(encounter.risk)), 10, WHITE) hint.position = Vector2(size.x * 0.13, 62) hint.size = Vector2(size.x * 0.74, 48) hint.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER hint.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART display_root.add_child(hint) var specs := [ ["hold_line", _tr("稳住战线", "HOLD LINE"), _tr("保持顺序,抵抗拉拽", "KEEP ORDER; RESIST PULL"), GOLD], ["follow_tide", _tr("顺潮移动", "FOLLOW TIDE"), _tr("保留移动,避开标记", "SAVE MOVEMENT; EVADE MARKS"), CYAN], ["focus_source", _tr("集火源头", "FOCUS SOURCE"), _tr("先打断治疗或控制", "INTERRUPT HEAL OR CONTROL"), CORAL], ["guard_center", _tr("守护中位", "GUARD CENTER"), _tr("诱导攻击,抓住显形", "BAIT ATTACK; USE REVEAL"), VIOLET], ] var w := size.x * 0.38 var h := 62.0 for index in range(specs.size()): var spec: Array = specs[index] var at := Vector2(size.x * (0.08 if index % 2 == 0 else 0.54), 188 + int(index / 2) * 72) _render_p102_plan_target(String(spec[0]), String(spec[1]), String(spec[2]), at, Vector2(w, h), Color(spec[3])) func _render_p102_plan_target(plan_id: String, title: String, consequence: String, at: Vector2, target_size: Vector2, color: Color) -> void: var line := ColorRect.new() line.name = "P102PlanLine_%s" % plan_id line.position = Vector2(at.x, at.y + target_size.y - 5) line.size = Vector2(target_size.x, 5) line.color = color line.mouse_filter = Control.MOUSE_FILTER_IGNORE display_root.add_child(line) var label := _outlined_label("%s\n%s" % [title, consequence], 10, WHITE) label.position = at label.size = target_size label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART display_root.add_child(label) display_root.add_child(_actor_target("P102PlanTarget_%s" % plan_id, at, target_size, choose_region_plan.bind(plan_id))) func _render_region_battle() -> void: super._render_region_battle() var header := display_root.find_child("P100Header", false, false) if header != null: var title := display_root.get_child(header.get_index() + 1) as Label if title != null: title.text = _tr("第%d轮 · %s" % [region_market.state.current_run.round_index, _region_name(String(region_market.state.current_run.pending_encounter.route_id))], "ROUND %d · %s" % [region_market.state.current_run.round_index, _region_name(String(region_market.state.current_run.pending_encounter.route_id))]) func _render_region_persistent_feedback() -> void: super._render_region_persistent_feedback() if not p102_mode: return var mechanics: Dictionary = region_motion.mechanic_effects(region_motion_clock) if not String(mechanics.drownseed.state).is_empty(): var seed := _motion_sprite("P102DrownseedMarker", PROTECT_ABSORB_VFX, Vector2(38, 38), CORAL if mechanics.drownseed.state == "burst" else GOLD) seed.set_meta("target_id", String(mechanics.drownseed.target_id)) seed.set_meta("mechanic_state", String(mechanics.drownseed.state)) display_root.add_child(seed) if not String(mechanics.medic.state).is_empty(): var heal := _motion_sprite("P102MedicPulse", PROTECT_GAIN_VFX, Vector2(42, 42), CORAL if mechanics.medic.state == "interrupted" else JADE) heal.set_meta("target_id", String(mechanics.medic.target_id)) heal.set_meta("mechanic_state", String(mechanics.medic.state)) display_root.add_child(heal) if String(mechanics.fogjaw.state) == "revealed": var reveal := _motion_sprite("P102FogjawReveal", DAMAGE_VFX, Vector2(48, 48), CYAN) reveal.set_meta("target_id", "EN-R2-10") display_root.add_child(reveal) func _animate_region_persistent_feedback() -> void: super._animate_region_persistent_feedback() for node_name in ["P102DrownseedMarker", "P102MedicPulse", "P102FogjawReveal"]: var sprite := display_root.find_child(node_name, false, false) as Sprite2D if sprite == null: continue sprite.position = _region_actor_center(String(sprite.get_meta("target_id", ""))) var pulse := 0.72 if low_power else 0.68 + sin(region_motion_clock * 8.0) * 0.22 sprite.modulate.a = pulse func _render_p102_recovery() -> void: _add_siltwake_world() var run: Dictionary = region_market.state.current_run var report: Dictionary = run.last_battle_report _add_header(_tr("第%d轮 · 撤回渡口" % report.round_index, "ROUND %d · FERRY RECOVERY" % report.round_index), _tr("◆%d 救援 ◆%d 信用 %d" % [run.coin, run.recovery_cost, run.replacement_credit], "◆%d RECOVERY ◆%d CREDIT %d" % [run.coin, run.recovery_cost, run.replacement_credit])) var title := _outlined_label(_tr("阵亡永久保留,幸存者可恢复", "FALLEN STAY LOST; SURVIVORS CAN RECOVER"), 23, CORAL) title.position = Vector2(size.x * 0.10, 76) title.size = Vector2(size.x * 0.80, 48) title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER display_root.add_child(title) var body := _outlined_label(_tr("失败原因:%s\n支付救援费后回到同一个集市,路线和商品不会重抽。" % _battle_cause_text(report), "CAUSE: %s\nPAY RECOVERY TO RETURN TO THE SAME MARKET. ROUTES AND OFFERS DO NOT REROLL." % _battle_cause_text(report)), 11, WHITE) body.position = Vector2(size.x * 0.16, 128) body.size = Vector2(size.x * 0.68, 72) body.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER body.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART display_root.add_child(body) _render_p102_survivors(Vector2(size.x * 0.24, size.y * 0.54), 68) var recover := _action_button("P102RecoverButton", _tr("支付 ◆%d 救援" % run.recovery_cost, "PAY ◆%d TO RECOVER" % run.recovery_cost), recover_region_company) recover.position = Vector2(size.x - 236, size.y - 62) recover.size = Vector2(220, 52) recover.disabled = int(run.coin) < int(run.recovery_cost) display_root.add_child(recover) func _render_p102_market_five() -> void: _add_siltwake_world() var run: Dictionary = region_market.state.current_run _add_header(_tr("淤潮地 · 集市 5", "SILTWAKE · MARKET 5"), _tr("◆%d 战线容量 %d 已保存" % [run.coin, run.line_capacity], "◆%d LINE CAPACITY %d SAVED" % [run.coin, run.line_capacity])) var title := _outlined_label(_tr("芦苇渡船已修复", "REED FERRY RESTORED"), 28, GOLD_LIGHT) title.position = Vector2(size.x * 0.43, 82) title.size = Vector2(size.x * 0.50, 56) title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER display_root.add_child(title) var body := _outlined_label(_tr("四轮已清除 · 战线容量 6 · 队员生命、等级、经验、装备与路线历史均已写入 Save v8。", "FOUR ROUNDS CLEARED · LINE CAPACITY 6 · HP, LEVELS, XP, GEAR AND ROUTE HISTORY ARE SAVED IN V8."), 12, WHITE) body.position = Vector2(size.x * 0.45, 142) body.size = Vector2(size.x * 0.46, 80) body.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER body.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART display_root.add_child(body) _render_p102_survivors(Vector2(size.x * 0.07, size.y * 0.46), clampf(size.y * 0.13, 62.0, 92.0)) var seal := _outlined_label(_tr("中段远征完成 · 后续集市尚未开放", "MID-CONTRACT COMPLETE · LATER MARKETS NOT YET OPEN"), 13, JADE) seal.position = Vector2(size.x * 0.44, size.y - 78) seal.size = Vector2(size.x * 0.48, 48) seal.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER display_root.add_child(seal) func _render_p102_survivors(origin: Vector2, actor_size: float) -> void: var run: Dictionary = region_market.state.current_run for index in range(run.company.size()): var recruit_id := String(run.company[index]) var record: Dictionary = run.recruit_records[recruit_id] var actor := _add_actor(_p100_recruit_rig(record), origin + Vector2(index * (actor_size * 0.78), 0), actor_size, "P102Survivor_%s" % recruit_id, true, "victory" if screen == P102_SCREEN_MARKET_5 else "idle") actor.self_modulate = _p100_profession_color(String(record.profession_id)) func _add_siltwake_world() -> void: super._add_siltwake_world() if not p102_mode: return var restored := region_market != null and bool(region_market.state.current_run.get("reed_ferry_restored", false)) var ferry_width := size.x * (0.32 if restored else 0.20) var ferry_origin := Vector2(size.x * 0.05, size.y * 0.31) var hull := Polygon2D.new() hull.name = "P102ReedFerryRestored" if restored else "P102ReedFerryScaffold" hull.polygon = PackedVector2Array([ ferry_origin + Vector2(0, 4), ferry_origin + Vector2(ferry_width, 0), ferry_origin + Vector2(ferry_width - 18, 24), ferry_origin + Vector2(18, 28), ]) hull.color = Color("#bd873d") if restored else Color(0.34, 0.25, 0.17, 0.62) hull.z_index = 5 display_root.add_child(hull) var deck := ColorRect.new() deck.name = "P102FerryDeck" deck.position = ferry_origin + Vector2(8, -2) deck.size = Vector2(ferry_width - 16, 8) deck.color = GOLD if restored else Color(0.44, 0.36, 0.24, 0.58) deck.rotation = -0.025 deck.mouse_filter = Control.MOUSE_FILTER_IGNORE deck.z_index = 6 display_root.add_child(deck) for index in range(6 if restored else 2): var post := ColorRect.new() post.name = "P102FerryPost_%d" % index post.position = ferry_origin + Vector2(14 + index * (ferry_width - 28) / maxf(1.0, float(5 if restored else 1)), -8) post.size = Vector2(4, 24) post.color = deck.color post.mouse_filter = Control.MOUSE_FILTER_IGNORE post.z_index = 7 display_root.add_child(post) if restored: var mast := ColorRect.new() mast.name = "P102FerryMast" mast.position = ferry_origin + Vector2(ferry_width * 0.52, -54) mast.size = Vector2(5, 62) mast.color = Color("#efd17a") mast.mouse_filter = Control.MOUSE_FILTER_IGNORE mast.z_index = 7 display_root.add_child(mast) var flag := Polygon2D.new() flag.name = "P102FerryFlag" flag.polygon = PackedVector2Array([ mast.position + Vector2(5, 3), mast.position + Vector2(42, 12), mast.position + Vector2(5, 23), ]) flag.color = CYAN flag.z_index = 7 display_root.add_child(flag) var lantern := _outlined_label("◆", 18, GOLD_LIGHT) lantern.name = "P102FerryLantern" lantern.position = mast.position + Vector2(-11, -20) lantern.size = Vector2(28, 28) lantern.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER lantern.z_index = 8 display_root.add_child(lantern) func _region_event_text(event: Dictionary) -> String: if not p102_mode: return super._region_event_text(event) var kind := String(event.get("kind", "battle_started")) var payload: Dictionary = Dictionary(event.get("payload", {})) return { "battle_started": _tr("第%d轮开始:%s" % [payload.get("round_index", 0), _plan_name(String(payload.get("plan_id", "")))], "ROUND %d: %s" % [payload.get("round_index", 0), _plan_name(String(payload.get("plan_id", "")))]), "wave_entered": _tr("%s进入战线" % _region_name(String(event.get("source_id", ""))), "%s ENTERS THE LINE" % _region_name(String(event.get("source_id", "")))), "drownseed_placed": _tr("沉籽落地:两拍后爆裂", "DROWNSEED PLACED: BURST IN TWO CADENCES"), "drownseed_evaded": _tr("队员移出沉籽标记", "RECRUIT CLEARS THE DROWNSEED MARK"), "drownseed_burst": _tr("沉籽爆裂并冲击战线", "DROWNSEED BURSTS INTO THE LINE"), "mire_medic_heal": _tr("泥沼医师治疗并缩短迟滞", "MIRE MEDIC HEALS AND SHORTENS HINDRANCE"), "source_interrupted": _tr("源头被集火打断", "SOURCE INTERRUPTED BY FOCUS FIRE"), "enemy_sustain_resolved": _tr("敌方治疗完整生效", "ENEMY SUSTAIN RESOLVES"), "fogjaw_hidden": _tr("雾颚兽藏入雾中,远程无法锁定", "FOGJAW HIDES; LONG REACH CANNOT TARGET"), "fogjaw_center_attack": _tr("雾颚兽扑向中位并短暂显形", "FOGJAW STRIKES CENTER AND REVEALS"), "center_guarded": _tr("中位守护接住突袭", "CENTER GUARD CATCHES THE STRIKE"), "center_exposed": _tr("中位暴露,突袭命中", "CENTER EXPOSED; STRIKE CONNECTS"), "rear_pull": _tr("芦钩尝试拉动后排", "REED HOOK PULLS THE REAR"), "healing_denial_attached": _tr("泥灯水蛭封锁最低生命队员", "MUDLAMP LEECH BLOCKS THE LOWEST-HP RECRUIT"), "indexed_mud_marked": _tr("当前安全位被淤泥标记", "CURRENT SAFE INDEX IS MARKED"), "enemy_order_changed": _tr("敌方前后顺序改变", "ENEMY ORDER CHANGES"), "tide_shift_warning": _tr("换潮警告:下一安全位已亮起", "TIDE WARNING: NEXT SAFE INDEX LIT"), "tide_shifted": _tr("队伍随潮移动", "THE LINE FOLLOWS THE TIDE") if bool(payload.get("followed", false)) else _tr("战线留在原位承受换潮", "THE LINE HOLDS THROUGH THE TIDE"), "allied_basic": _tr("队员攻击产生接触", "RECRUIT ATTACK CONNECTS"), "authoritative_damage": _tr("伤害 -%d" % int(payload.get("amount", 0)), "DAMAGE -%d" % int(payload.get("amount", 0))), "fallen": _tr("一名队员永久阵亡", "A RECRUIT IS PERMANENTLY FALLEN"), "prepared_response_succeeded": _tr("战术应对命中关键窗口", "THE RESPONSE HITS THE DECISIVE WINDOW"), "battle_terminal": _tr("本轮结果已确定", "ROUND RESULT LOCKED"), }.get(kind, kind.replace("_", " ").to_upper()) func _region_event_color(kind: String) -> Color: if not p102_mode: return super._region_event_color(kind) if kind in ["drownseed_burst", "enemy_sustain_resolved", "center_exposed", "authoritative_damage", "fallen"]: return CORAL if kind in ["drownseed_evaded", "source_interrupted", "center_guarded", "prepared_response_succeeded", "battle_terminal"]: return JADE return GOLD_LIGHT func _route_summary(risk: int) -> String: if not p102_mode: return super._route_summary(risk) var offer := _region_offer("route", region_focused_route) if offer.is_empty() and region_market != null: offer = Dictionary(region_market.state.current_run.get("pending_encounter", {})) if offer.is_empty(): return _tr("比较路线压力与当前战线,再选择应对。", "COMPARE ROUTE PRESSURE WITH THE CURRENT LINE, THEN CHOOSE A RESPONSE.") return String(offer.preview) if locale == "en" else _p102_route_hint(String(offer.route_id)) func _p102_route_hint(route_id: String) -> String: return { "RO-R2-M02-Q0": "稳住后排,让镜像目标保持可预测。", "RO-R2-M02-Q1": "集火打断笛手,避免敌方换位后牵引战线。", "RO-R2-M02-Q2": "保留一次移动,在沉籽爆裂前离开标记。", "RO-R2-M03-Q0": "等待迟滞过半,或先处理泥沼医师。", "RO-R2-M03-Q1": "优先打断水蛭或医师,别让治疗反复生效。", "RO-R2-M03-Q2": "守住中位,等雾颚兽攻击显形后反击。", "RO-R2-M04-Q0": "抵抗拉拽,把移动留给沉籽倒计时。", "RO-R2-M04-Q1": "先喂给镜像安全目标,再守住中位显形窗口。", "RO-R2-M04-Q2": "先集火医师,再带着可用移动击破沉籽。", }.get(route_id, "路线会检验当前战线的站位、目标和时机。") func _region_name(row_id: String) -> String: if not p102_mode: return super._region_name(row_id) var p102_name := P102Content.localized_name(p102_catalog, row_id, locale) return super._region_name(row_id) if p102_name.is_empty() else p102_name func _equipment_symbol(catalog_id: String) -> String: if catalog_id in ["EQ-049", "EQ-050", "EQ-051", "EQ-052", "EQ-053", "EQ-054", "EQ-055", "EQ-056"]: return {"EQ-049": "◇", "EQ-050": "▽", "EQ-051": "⊘", "EQ-052": "♨", "EQ-053": "□", "EQ-054": "Ⅲ", "EQ-055": "↔", "EQ-056": "⇥"}[catalog_id] return super._equipment_symbol(catalog_id) func _plan_name(plan_id: String) -> String: return { "hold_line": _tr("稳住战线", "HOLD LINE"), "follow_tide": _tr("顺潮移动", "FOLLOW TIDE"), "focus_source": _tr("集火源头", "FOCUS SOURCE"), "guard_center": _tr("守护中位", "GUARD CENTER"), }.get(plan_id, plan_id) func _battle_cause_text(report: Dictionary) -> String: var event: Dictionary = Dictionary(report.get("first_decisive_event", {})) return _region_event_text(event) func reset_progress() -> void: if not p102_mode: super.reset_progress() return if not reset_confirmation: reset_confirmation = true last_code = "reset_confirmation_required" _queue_render() return for save_name in [P102_SAVE_PATH, P101_SAVE_PATH]: for suffix in ["", ".bak", ".candidate"]: var path := ProjectSettings.globalize_path(save_name + suffix) if FileAccess.file_exists(path): DirAccess.remove_absolute(path) p102_mode = false region_mode = false region_market = null entry_snapshot = {} region_save_sequence = 0 region_save_commit_hash = "" super.reset_progress()