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

1518 lines
64 KiB
GDScript

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