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

1036 lines
45 KiB
GDScript

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()