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

1100 lines
47 KiB
GDScript

class_name P97RuntimeController
extends Control
const Content = preload("res://content/p9_7_content.gd")
const MarketService = preload("res://p9_7/market_service.gd")
const MarketState = preload("res://p9_7/market_state.gd")
const BattleService = preload("res://p9_7/battle_service.gd")
const SaveService = preload("res://p9_7/save_service.gd")
const AnimationAdapter = preload("res://presentation/animation_adapter.gd")
const HALL_BACKGROUND = preload("res://assets/backgrounds/p9_3_guild_hall.png")
const WINDGLASS_BACKGROUND = preload("res://assets/backgrounds/windglass_verge.webp")
const UI_FONT = preload("res://assets/fonts/NotoSansSC.ttf")
const HEAL_VFX = preload("res://assets/vfx/protect_gain.png")
const HIT_VFX = preload("res://assets/vfx/damage_heavy.png")
const FRONTPIN_TEXTURE = preload("res://assets/equipment/eq_002_frontpin_greaves.png")
const OPEN_LINE_TEXTURE = preload("res://assets/equipment/eq_012_open_line_cord.png")
const ANIMATION_SPECS := {
"Hearthguard": {
"idle": {"root": "hearthguard/idle", "frames": 6, "fps": 6.0, "loop": true},
"advance": {"root": "hearthguard/advance", "frames": 8, "fps": 10.0, "loop": true},
"guard": {"root": "hearthguard/guard", "frames": 4, "fps": 10.0, "loop": false},
"basic_attack": {"root": "hearthguard/basic_attack", "frames": 6, "fps": 12.0, "loop": false},
"victory": {"root": "hearthguard/victory", "frames": 6, "fps": 6.0, "loop": false},
},
"Wayrunner": {
"idle": {"root": "wayrunner/idle", "frames": 2, "fps": 3.0, "loop": true},
"advance": {"root": "wayrunner/advance", "frames": 2, "fps": 6.0, "loop": true},
"evade_counter": {"root": "wayrunner/evade_counter", "frames": 4, "fps": 7.0, "loop": false},
},
"Farstring": {
"idle": {"root": "farstring/idle", "frames": 2, "fps": 3.0, "loop": true},
"advance": {"root": "farstring/advance", "frames": 2, "fps": 6.0, "loop": true},
"draw_release": {"root": "farstring/draw_release", "frames": 4, "fps": 7.0, "loop": false},
},
"Kindhand": {
"idle": {"root": "kindhand/idle", "frames": 2, "fps": 3.0, "loop": true},
"advance": {"root": "kindhand/advance", "frames": 2, "fps": 6.0, "loop": true},
"lantern_heal": {"root": "kindhand/lantern_heal", "frames": 4, "fps": 7.0, "loop": false},
},
"Riftblade": {
"idle": {"root": "riftblade/idle", "frames": 6, "fps": 6.0, "loop": true},
"advance": {"root": "riftblade/advance", "frames": 8, "fps": 10.0, "loop": true},
"telegraph": {"root": "riftblade/telegraph", "frames": 6, "fps": 10.0, "loop": false},
"attack": {"root": "riftblade/attack", "frames": 6, "fps": 12.0, "loop": false},
"hit": {"root": "riftblade/hit", "frames": 4, "fps": 10.0, "loop": false},
"death": {"root": "riftblade/death", "frames": 8, "fps": 10.0, "loop": false},
},
}
const GOLD := Color("#f4c45c")
const GOLD_LIGHT := Color("#ffe8a3")
const INK := Color("#102033")
const WHITE := Color("#f8fbff")
const CYAN := Color("#73e3e6")
const CORAL := Color("#ff7478")
const JADE := Color("#75e0a4")
const VIOLET := Color("#b29aff")
const MARKET_HEADER_HEIGHT := 50.0
const MARKET_CONTEXT_HEIGHT := 74.0
const MARKET_SIDE_MARGIN := 16.0
var entry_snapshot: Dictionary = {}
var market
var battle = BattleService.new()
var saves = SaveService.new()
var content: Dictionary = Content.snapshot()
var animation = AnimationAdapter.new()
var display_root: Control
var locale := "zh_CN"
var text_scale := 1.0
var reduced_motion := false
var muted := true
var low_power := false
var persistence_enabled := true
var selected_inventory_instance := ""
var focused_company_id := ""
var pending_line_ids: Array[String] = []
var operation_index := 0
var last_code := ""
var battle_active := false
var battle_result_visible := false
var battle_auto_advance := true
var battle_paused := false
var battle_speed := 1
var battle_tick_accumulator := 0.0
var world_clock := 0.0
var render_queued := false
var ambient_actors: Dictionary = {}
var battle_actors: Dictionary = {}
var battle_hp_bars: Dictionary = {}
var battle_actions: Dictionary = {}
var presented_event_counts: Dictionary = {}
func configure_entry(snapshot: Dictionary) -> void:
entry_snapshot = snapshot.duplicate(true)
if is_inside_tree():
_bootstrap_market()
func _ready() -> void:
set_meta("visible_world_ratio", 0.86)
var ui_theme := Theme.new()
ui_theme.default_font = UI_FONT
theme = ui_theme
animation.load_specs(ANIMATION_SPECS)
display_root = Control.new()
display_root.name = "P97DisplayRoot"
display_root.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
add_child(display_root)
resized.connect(_queue_render)
_bootstrap_market()
func _bootstrap_market() -> void:
if entry_snapshot.is_empty():
last_code = "entry_snapshot_required"
_queue_render()
return
if entry_snapshot.has("content_revision") and entry_snapshot.has("current_market"):
market = MarketService.new()
if not market.state.restore_snapshot(entry_snapshot):
market = null
last_code = "market_snapshot_invalid"
_queue_render()
return
else:
market = MarketService.new(entry_snapshot)
pending_line_ids.assign(market.state.deployed_recruit_ids)
selected_inventory_instance = ""
focused_company_id = ""
last_code = "market_ready"
battle_active = false
battle_result_visible = false
_save_stable()
_queue_render()
func _process(delta: float) -> void:
if not low_power:
world_clock += maxf(0.0, delta)
_update_ambient_actors()
if not battle_active or battle_result_visible or battle_paused or not battle_auto_advance:
return
battle_tick_accumulator += maxf(0.0, delta) * float(battle_speed)
var ticks := int(floor(battle_tick_accumulator / 0.08))
if ticks <= 0:
return
battle_tick_accumulator -= float(ticks) * 0.08
advance_battle_ticks(ticks)
func tap_recruit_offer(recruit_id: String) -> void:
if not _market_ready():
return
if market.state.focused_recruit_offer != recruit_id:
_accept_market_result(market.focus_recruit_offer(recruit_id))
return
var result: Dictionary = market.hire(_operation("hire", recruit_id), recruit_id)
if result.ok:
pending_line_ids.assign(market.state.deployed_recruit_ids)
_accept_market_result(result, true)
func tap_equipment_offer(offer_id: String) -> void:
if not _market_ready():
return
if market.state.focused_equipment_offer != offer_id:
_accept_market_result(market.focus_equipment_offer(offer_id))
return
var result: Dictionary = market.buy_equipment(_operation("buy", offer_id), offer_id)
if result.ok:
selected_inventory_instance = String(result.receipt.instance_id)
_accept_market_result(result, true)
func tap_company_recruit(recruit_id: String) -> void:
if not _market_ready() or recruit_id not in market.state.company:
return
if not selected_inventory_instance.is_empty():
var equip_result: Dictionary = market.equip(_operation("equip", recruit_id), selected_inventory_instance, recruit_id)
if equip_result.ok:
selected_inventory_instance = ""
_accept_market_result(equip_result, true)
return
if focused_company_id != recruit_id:
focused_company_id = recruit_id
last_code = "company_focused"
_queue_render()
return
if recruit_id in pending_line_ids:
if pending_line_ids.size() > 1:
pending_line_ids.erase(recruit_id)
last_code = "line_removed"
else:
if pending_line_ids.size() < market.state.line_capacity and int(market.state.member_condition.get(recruit_id, 0)) > 0:
pending_line_ids.append(recruit_id)
last_code = "line_added"
_queue_render()
func commit_pending_line() -> void:
if not _market_ready():
return
var result: Dictionary = market.commit_deployment(_operation("deploy", "line"), pending_line_ids)
_accept_market_result(result, true)
func preview_focused_dismissal() -> void:
if not _market_ready() or focused_company_id.is_empty():
return
_accept_market_result(market.preview_dismissal(focused_company_id))
func confirm_focused_dismissal() -> void:
if not _market_ready() or market.state.pending_dismissal.is_empty():
return
var recruit_id := String(market.state.pending_dismissal.recruit_id)
var result: Dictionary = market.confirm_dismissal(_operation("dismiss", recruit_id), recruit_id)
if result.ok:
focused_company_id = ""
pending_line_ids.assign(market.state.deployed_recruit_ids)
_accept_market_result(result, true)
func cancel_dismissal() -> void:
if not _market_ready():
return
_accept_market_result(market.cancel_dismissal())
func recover_company() -> void:
if not _market_ready():
return
var result: Dictionary = market.recover_injured_company(_operation("recover", "company"))
_accept_market_result(result, true)
func tap_route(route_id: String) -> void:
if not _market_ready():
return
if market.state.focused_route_offer != route_id:
_accept_market_result(market.focus_route(route_id))
return
var result: Dictionary = market.commit_route(_operation("route", route_id), route_id)
if not result.ok:
_accept_market_result(result)
return
_save_stable()
start_committed_battle()
func start_committed_battle() -> void:
if market == null:
return
var result: Dictionary = battle.start_from_market(market.state)
last_code = String(result.code)
if not result.ok:
_queue_render()
return
battle_active = true
battle_result_visible = false
battle_paused = false
battle_speed = 1
battle_tick_accumulator = 0.0
battle_actions.clear()
presented_event_counts.clear()
_queue_render()
func advance_battle_ticks(ticks: int) -> Dictionary:
if not battle_active or battle_result_visible:
return {"ok": false, "code": "battle_inactive", "events": []}
var result: Dictionary = battle.advance_ticks(ticks)
last_code = String(result.code)
for event in result.events:
_handle_battle_event(event)
_update_battle_visuals()
if battle.state.terminal:
battle_result_visible = true
_queue_render()
return result
func toggle_battle_pause() -> void:
if not battle_active or battle_result_visible:
return
battle_paused = not battle_paused
_update_battle_control_labels()
func toggle_battle_speed() -> void:
if not battle_active or battle_result_visible:
return
battle_speed = 2 if battle_speed == 1 else 1
_update_battle_control_labels()
func settle_and_return() -> void:
if not battle_active or not battle.state.terminal:
return
var result: Dictionary = market.settle_battle(_operation("settle", battle.state.route_id), battle.state.snapshot())
last_code = String(result.code)
if not result.ok:
_queue_render()
return
battle_active = false
battle_result_visible = false
pending_line_ids.assign(market.state.deployed_recruit_ids)
_save_stable()
_queue_render()
func _market_ready() -> bool:
return market != null and not battle_active and market.state.phase == MarketState.Phase.MARKET
func _accept_market_result(result: Dictionary, persist := false) -> void:
last_code = String(result.get("code", ""))
if result.ok and persist:
_save_stable()
_queue_render()
func _save_stable() -> bool:
if not persistence_enabled or market == null:
return false
return bool(saves.save_snapshot(market.state.snapshot()).ok)
func _operation(kind: String, target: String) -> String:
operation_index += 1
return "p97-ui-%s-%d-%d-%s" % [kind, market.state.revision, operation_index, target]
func _queue_render() -> void:
if display_root == null or render_queued:
return
render_queued = true
call_deferred("_deferred_render")
func _deferred_render() -> void:
render_queued = false
_render_current()
func _render_current() -> void:
if display_root == null:
return
_clear_display()
if size.y > size.x:
_render_rotate_notice()
elif market == null:
_render_missing_entry()
elif battle_active:
_render_battle()
else:
_render_market()
func _clear_display() -> void:
ambient_actors.clear()
battle_actors.clear()
battle_hp_bars.clear()
for child in display_root.get_children():
child.free()
func _render_market() -> void:
_add_background(HALL_BACKGROUND)
_add_market_hud()
if market.state.continuation_locked:
_render_relay_threshold()
return
_render_company_line()
_render_recruit_offers()
_render_equipment_pedestals()
_render_route_gates()
_render_market_context()
func _add_market_hud() -> void:
var header := ColorRect.new()
header.name = "P97MarketHud"
header.position = Vector2.ZERO
header.size = Vector2(size.x, MARKET_HEADER_HEIGHT)
header.color = Color(0.015, 0.035, 0.075, 0.84)
header.mouse_filter = Control.MOUSE_FILTER_IGNORE
display_root.add_child(header)
var title_width := minf(280.0, size.x * 0.34)
var title := _outlined_label(_tr("风晶集市 %d" % market.state.current_market, "WINDGLASS MARKET %d" % market.state.current_market), 16, GOLD_LIGHT)
title.position = Vector2(18, 5)
title.size = Vector2(title_width - 18.0, 40)
title.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
display_root.add_child(title)
var values := _outlined_label(_tr("◆ %d 战线 %d/%d 队伍 %d 预备 %d" % [market.state.coin, market.state.deployed_recruit_ids.size(), market.state.line_capacity, market.state.company.size(), market.state.reserve_recruit_ids.size()], "◆ %d LINE %d/%d COMPANY %d RESERVE %d" % [market.state.coin, market.state.deployed_recruit_ids.size(), market.state.line_capacity, market.state.company.size(), market.state.reserve_recruit_ids.size()]), 13, WHITE)
values.position = Vector2(title_width, 5)
values.size = Vector2(size.x - title_width - 18.0, 40)
values.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
values.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
display_root.add_child(values)
func _render_company_line() -> void:
var world_bottom := size.y - MARKET_CONTEXT_HEIGHT
var actor_size := clampf(size.y * 0.25, 82.0, 132.0)
var actor_y := world_bottom - actor_size - 24.0
var caption := _outlined_label(_tr("出战线 / 预备队", "LINE / RESERVE"), 11, GOLD_LIGHT)
caption.position = Vector2(MARKET_SIDE_MARGIN, actor_y - 24.0)
caption.size = Vector2(210, 28)
display_root.add_child(caption)
var count: int = market.state.company.size()
var spacing := minf(actor_size + 6.0, maxf(48.0, (size.x - MARKET_SIDE_MARGIN * 2.0 - actor_size) / maxf(1.0, float(count - 1))))
for index in range(count):
var recruit_id: String = market.state.company[index]
var in_line := recruit_id in pending_line_ids
var member_size := actor_size if in_line else actor_size - 8.0
var x := MARKET_SIDE_MARGIN + float(index) * spacing
var y := actor_y if in_line else actor_y + 10.0
_add_ground_ring(Vector2(x + member_size * 0.5, y + member_size - 5), member_size * 0.72, GOLD if in_line else CYAN, "P97LineRing_%s" % recruit_id)
var actor := _add_actor(_recruit_rig(recruit_id), Vector2(x, y), member_size, "P97CompanyActor_%s" % recruit_id, true)
actor.self_modulate = _recruit_color(recruit_id)
var target := _actor_target("P97CompanyTarget_%s" % recruit_id, Vector2(x - 4, y - 6), Vector2(member_size + 8.0, member_size + 12.0), tap_company_recruit.bind(recruit_id))
display_root.add_child(target)
var state_text := _tr("战线", "LINE") if in_line else _tr("预备", "RESERVE")
var label := _outlined_label("%s %dHP" % [state_text, int(market.state.member_condition[recruit_id])], 8, GOLD_LIGHT if in_line else CYAN)
label.position = Vector2(x - 8, world_bottom - 26.0)
label.size = Vector2(member_size + 20.0, 24)
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
display_root.add_child(label)
func _render_recruit_offers() -> void:
var lane := _market_recruit_lane()
var caption := _outlined_label(_tr("候选队员", "RECRUITS"), 11, GOLD_LIGHT)
caption.position = Vector2(lane.position.x, 52)
caption.size = Vector2(lane.size.x, 24)
display_root.add_child(caption)
var offer_ids: Array = market.state.available_recruit_offer_ids
var slot_width := lane.size.x / maxf(1.0, float(offer_ids.size()))
for index in range(offer_ids.size()):
var recruit_id := String(offer_ids[index])
var offer_size := clampf(slot_width * 0.88, 72.0, 92.0)
var x := lane.position.x + float(index) * slot_width + (slot_width - offer_size) * 0.5
var actor := _add_actor(_recruit_rig(recruit_id), Vector2(x, 66), offer_size, "P97RecruitActor_%s" % recruit_id, true)
actor.self_modulate = _recruit_color(recruit_id)
var target := _actor_target("P97RecruitTarget_%s" % recruit_id, Vector2(x - 4, 62), Vector2(offer_size + 8.0, 106), tap_recruit_offer.bind(recruit_id))
display_root.add_child(target)
var role := _outlined_label(_profession_name(recruit_id), 8, GOLD_LIGHT if market.state.focused_recruit_offer == recruit_id else WHITE)
role.position = Vector2(lane.position.x + float(index) * slot_width, 148)
role.size = Vector2(slot_width, 26)
role.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
role.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
display_root.add_child(role)
func _render_equipment_pedestals() -> void:
var lane := _market_equipment_lane()
var caption := _outlined_label(_tr("装备台", "GEAR"), 11, GOLD_LIGHT)
caption.position = Vector2(lane.position.x, 52)
caption.size = Vector2(lane.size.x, 24)
display_root.add_child(caption)
var offer_ids: Array = market.state.available_equipment_offer_ids
var gap := 3.0
var slot_width := (lane.size.x - gap * float(maxi(0, offer_ids.size() - 1))) / maxf(1.0, float(offer_ids.size()))
for index in range(offer_ids.size()):
var offer_id := String(offer_ids[index])
var catalog_id := String(content.equipment_offers[offer_id].catalog_id)
var x := lane.position.x + float(index) * (slot_width + gap)
var pedestal := ColorRect.new()
pedestal.name = "P97Pedestal_%s" % offer_id
pedestal.position = Vector2(x + 2.0, 126)
pedestal.size = Vector2(slot_width - 4.0, 10)
pedestal.color = GOLD if market.state.focused_equipment_offer == offer_id else Color(0.30, 0.44, 0.52, 0.90)
pedestal.mouse_filter = Control.MOUSE_FILTER_IGNORE
display_root.add_child(pedestal)
var icon := TextureRect.new()
icon.texture = _equipment_texture(catalog_id)
icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
icon.custom_minimum_size = Vector2.ZERO
icon.position = Vector2(x + (slot_width - 38.0) * 0.5, 82)
icon.size = Vector2(38, 46)
icon.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
icon.self_modulate = _equipment_color(catalog_id)
icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
display_root.add_child(icon)
var target := _actor_target("P97EquipmentTarget_%s" % offer_id, Vector2(x, 76), Vector2(slot_width, 64), tap_equipment_offer.bind(offer_id))
display_root.add_child(target)
var lock := _outlined_label(_tr("锁 刷新:中继站离线", "LOCK REFRESH: RELAY OFFLINE"), 8, Color(0.72, 0.80, 0.86))
lock.position = Vector2(lane.position.x, 140)
lock.size = Vector2(lane.size.x, 26)
lock.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
display_root.add_child(lock)
func _render_route_gates() -> void:
var lane := _market_route_lane()
var caption := _outlined_label(_tr("远征门", "ROUTES"), 11, GOLD_LIGHT)
caption.position = Vector2(lane.position.x, 52)
caption.size = Vector2(lane.size.x, 24)
display_root.add_child(caption)
var route_ids: Array = market.state.available_battle_offer_ids
var gap := 4.0
var gate_width := (lane.size.x - gap * float(maxi(0, route_ids.size() - 1))) / maxf(1.0, float(route_ids.size()))
for index in range(route_ids.size()):
var route_id := String(route_ids[index])
var route: Dictionary = content.routes[route_id]
var x := lane.position.x + float(index) * (gate_width + gap)
var gate := ColorRect.new()
gate.name = "P97Gate_%s" % route_id
gate.position = Vector2(x, 78)
gate.size = Vector2(gate_width, 88)
gate.color = _risk_color(int(route.risk), market.state.focused_route_offer == route_id)
gate.mouse_filter = Control.MOUSE_FILTER_IGNORE
display_root.add_child(gate)
var risk := _outlined_label([_tr("稳", "S"), _tr("险", "P"), _tr("精", "E")][int(route.risk)], 10, WHITE)
risk.position = Vector2(x, 88)
risk.size = Vector2(gate_width, 44)
risk.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
display_root.add_child(risk)
var reward := _outlined_label("+%d" % int(route.reward), 9, GOLD_LIGHT)
reward.position = Vector2(x, 126)
reward.size = Vector2(gate_width, 30)
reward.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
display_root.add_child(reward)
var target := _actor_target("P97RouteTarget_%s" % route_id, Vector2(x - 2, 74), Vector2(maxf(48.0, gate_width + 4), 96), tap_route.bind(route_id))
display_root.add_child(target)
func _market_route_lane() -> Rect2:
var lane_width := clampf(size.x * 0.24, 176.0, 250.0)
return Rect2(Vector2(size.x - MARKET_SIDE_MARGIN - lane_width, 50), Vector2(lane_width, 120))
func _market_equipment_lane() -> Rect2:
var route_lane := _market_route_lane()
var lane_width := clampf(size.x * 0.245, 200.0, 240.0)
return Rect2(Vector2(route_lane.position.x - 12.0 - lane_width, 50), Vector2(lane_width, 120))
func _market_recruit_lane() -> Rect2:
var equipment_lane := _market_equipment_lane()
var left := maxf(136.0, size.x * 0.18)
var available := equipment_lane.position.x - 12.0 - left
return Rect2(Vector2(left, 50), Vector2(minf(320.0, available), 120))
func _render_market_context() -> void:
var ribbon := ColorRect.new()
ribbon.name = "P97ContextRibbon"
ribbon.position = Vector2(0, size.y - MARKET_CONTEXT_HEIGHT)
ribbon.size = Vector2(size.x, MARKET_CONTEXT_HEIGHT)
ribbon.color = Color(0.015, 0.035, 0.075, 0.92)
display_root.add_child(ribbon)
var text := _market_context_text()
var detail := _outlined_label(text, 11, WHITE)
detail.name = "P97ContextText"
detail.position = Vector2(16, size.y - 69)
detail.size = Vector2(size.x * 0.65, 62)
detail.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
display_root.add_child(detail)
if not market.state.pending_dismissal.is_empty():
var confirm := _action_button("P97DismissConfirmButton", _tr("确认解雇", "CONFIRM DISMISS"), confirm_focused_dismissal)
confirm.position = Vector2(size.x - 286, size.y - 62)
confirm.size = Vector2(132, 48)
display_root.add_child(confirm)
var cancel := _secondary_button("P97DismissCancelButton", _tr("取消", "CANCEL"), cancel_dismissal)
cancel.position = Vector2(size.x - 146, size.y - 62)
cancel.size = Vector2(130, 48)
display_root.add_child(cancel)
return
if market.state.recovery_required:
var recover := _action_button("P97RecoveryButton", _tr("恢复 ◆%d" % market.state.recovery_cost, "RECOVER ◆%d" % market.state.recovery_cost), recover_company)
recover.position = Vector2(size.x - 220, size.y - 62)
recover.size = Vector2(204, 48)
display_root.add_child(recover)
return
var rally := _action_button("P97CommitLineButton", _tr("列阵 %d/%d" % [pending_line_ids.size(), market.state.line_capacity], "RALLY %d/%d" % [pending_line_ids.size(), market.state.line_capacity]), commit_pending_line)
rally.position = Vector2(size.x - 196, size.y - 62)
rally.size = Vector2(180, 48)
display_root.add_child(rally)
if not focused_company_id.is_empty():
var dismiss := _secondary_button("P97DismissPreviewButton", _tr("解雇", "DISMISS"), preview_focused_dismissal)
dismiss.position = Vector2(size.x - 320, size.y - 62)
dismiss.size = Vector2(112, 48)
display_root.add_child(dismiss)
func _market_context_text() -> String:
if not market.state.pending_dismissal.is_empty():
return _tr("解雇 %s?装备会返回仓库,返还 ◆%d。" % [_recruit_name(String(market.state.pending_dismissal.recruit_id)), int(market.state.pending_dismissal.rebate)], "DISMISS %s? Gear returns to inventory. Refund ◆%d." % [_recruit_name(String(market.state.pending_dismissal.recruit_id)), int(market.state.pending_dismissal.rebate)])
if market.state.recovery_required:
return _tr("队员倒下。支付恢复费用后可重新选择路线。", "RECRUITS ARE DOWN. Recover before choosing another route.")
if not selected_inventory_instance.is_empty():
var catalog_id := String(market.state.inventory[selected_inventory_instance].catalog_id)
return _tr("已购买 %s。点击一名兼容队员装备。" % _equipment_name(catalog_id), "%s PURCHASED. Tap a compatible Recruit to equip." % _equipment_name(catalog_id))
if not market.state.focused_recruit_offer.is_empty():
var recruit: Dictionary = content.recruits[market.state.focused_recruit_offer]
var profession: Dictionary = _profession_record(String(recruit.profession_id))
return "%s · Lv.%d · %dHP · ◆%d\n%s / %s" % [_recruit_name(String(recruit.id)), int(recruit.level), int(recruit.current_hp), int(recruit.list_price), _tr(String(profession.get("strength_zh", "")), String(profession.get("strength_en", ""))), _tr(String(profession.get("limit_zh", "")), String(profession.get("limit_en", "")))]
if not market.state.focused_equipment_offer.is_empty():
var offer: Dictionary = content.equipment_offers[market.state.focused_equipment_offer]
var item: Dictionary = content.equipment[offer.catalog_id]
return "%s · ◆%d\n%s / %s" % [_equipment_name(String(offer.catalog_id)), int(offer.list_price), _tr(String(item.benefit_zh), String(item.benefit_en)), _tr(String(item.cost_zh), String(item.cost_en))]
if not market.state.focused_route_offer.is_empty():
var route: Dictionary = content.routes[market.state.focused_route_offer]
return "%s · %s · ◆+%d\n%s" % [_tr(String(route.name_zh), String(route.name_en)), _tr(["稳妥", "压力", "精英"][int(route.risk)], ["STEADY", "PRESSURED", "ELITE"][int(route.risk)]), int(route.reward), _tr(String(route.pressure_zh), String(route.pressure_en))]
if not focused_company_id.is_empty():
var record: Dictionary = market.state.recruit_records[focused_company_id]
return _tr("%s · Lv.%d · %dHP。再次点击可加入/移出战线。" % [_recruit_name(focused_company_id), int(record.level), int(market.state.member_condition[focused_company_id])], "%s · Lv.%d · %dHP. Tap again to add/remove from the Line." % [_recruit_name(focused_company_id), int(record.level), int(market.state.member_condition[focused_company_id])])
return _tr("点击站立队员、装备台或远征门查看;再次点击执行。", "Tap a standing Recruit, gear pedestal or route gate; tap again to commit.")
func _render_relay_threshold() -> void:
var glow := ColorRect.new()
glow.name = "P97RelayGlow"
glow.position = Vector2(size.x * 0.58, 50)
glow.size = Vector2(size.x * 0.42, size.y - 50)
glow.color = Color(0.24, 0.86, 0.82, 0.18)
glow.mouse_filter = Control.MOUSE_FILTER_IGNORE
display_root.add_child(glow)
for index in range(market.state.company.size()):
var recruit_id: String = market.state.company[index]
var x := 28.0 + float(index) * minf(78.0, (size.x * 0.52 - 40.0) / maxf(1.0, float(market.state.company.size())))
var actor := _add_actor(_recruit_rig(recruit_id), Vector2(x, size.y * 0.48), 92, "P97RelayCompany_%s" % recruit_id, true, "victory")
actor.self_modulate = _recruit_color(recruit_id)
var title := _outlined_label(_tr("风晶中继站已修复", "WINDGLASS RELAY RESTORED"), 24, GOLD_LIGHT)
title.name = "P97RelayTitle"
title.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
title.position = Vector2(size.x * 0.53, size.y * 0.24)
title.size = Vector2(size.x * 0.45 - 18.0, 78)
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
display_root.add_child(title)
var body := _outlined_label(_tr("第 8 集市已保存\n队伍、预备队、装备、HP 与路线记录全部保留", "MARKET 8 SAVED\nCompany, Reserve, gear, HP and route history preserved"), 13, WHITE)
body.name = "P97RelayBody"
body.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
body.position = Vector2(size.x * 0.53, size.y * 0.47)
body.size = Vector2(size.x * 0.45 - 18.0, 104)
body.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
display_root.add_child(body)
func _render_battle() -> void:
_add_background(WINDGLASS_BACKGROUND)
_add_battle_hud()
for recruit_id in battle.state.member_ids:
_add_battle_actor(recruit_id, false)
for enemy_id in battle.state.enemy_ids:
_add_battle_actor(enemy_id, true)
_update_battle_visuals()
if battle_result_visible:
_render_battle_result()
func _add_battle_hud() -> void:
var route: Dictionary = content.routes[battle.state.route_id]
var context := _outlined_label(_tr(String(route.name_zh), String(route.name_en)), 15, WHITE)
context.position = Vector2(16, 6)
context.size = Vector2(size.x * 0.48, 42)
display_root.add_child(context)
var pause := _secondary_button("P97PauseButton", "▶" if battle_paused else "Ⅱ", toggle_battle_pause)
pause.position = Vector2(size.x - 112, 6)
pause.size = Vector2(48, 44)
display_root.add_child(pause)
var speed := _secondary_button("P97SpeedButton", "%dx" % battle_speed, toggle_battle_speed)
speed.position = Vector2(size.x - 58, 6)
speed.size = Vector2(50, 44)
display_root.add_child(speed)
func _add_battle_actor(actor_id: String, enemy: bool) -> void:
var rig := "Riftblade" if enemy else _recruit_rig(actor_id)
var actor_size := clampf(size.y * 0.28, 100.0, 148.0)
if enemy:
actor_size *= _enemy_visual_scale(actor_id)
var actor := _add_actor(rig, Vector2.ZERO, actor_size, "P97BattleActor_%s" % actor_id, false)
actor.self_modulate = _enemy_color(actor_id) if enemy else _recruit_color(actor_id)
actor.set_meta("enemy", enemy)
battle_actors[actor_id] = actor
var bar := ColorRect.new()
bar.name = "P97BattleHP_%s" % actor_id
bar.size = Vector2(clampf(actor_size * 0.62, 52.0, 72.0), 8)
bar.color = Color(0.02, 0.04, 0.07, 0.94)
bar.mouse_filter = Control.MOUSE_FILTER_IGNORE
var fill := ColorRect.new()
fill.name = "Fill"
fill.position = Vector2(1, 1)
fill.size = Vector2(bar.size.x - 2.0, bar.size.y - 2.0)
fill.color = CORAL if enemy else JADE
fill.mouse_filter = Control.MOUSE_FILTER_IGNORE
bar.add_child(fill)
bar.set_meta("fill", fill)
bar.set_meta("max_hp", int(battle.state.enemy_max_hp[actor_id]) if enemy else int(battle.state.member_max_hp[actor_id]))
display_root.add_child(bar)
battle_hp_bars[actor_id] = bar
var name_label := _outlined_label(_battle_actor_name(actor_id), 8, WHITE)
name_label.name = "P97BattleName_%s" % actor_id
name_label.size = Vector2(maxf(86.0, actor_size + 12.0), 26)
name_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
display_root.add_child(name_label)
actor.set_meta("name_label", name_label)
func _update_battle_visuals() -> void:
if not battle_active:
return
for actor_id in battle_actors:
var actor: TextureRect = battle_actors[actor_id]
var enemy: bool = actor_id in battle.state.enemy_ids
var hp := int(battle.state.enemy_hp[actor_id]) if enemy else int(battle.state.member_hp[actor_id])
var position_value := int(battle.state.enemy_positions[actor_id]) if enemy else int(battle.state.member_positions[actor_id])
var x := _battle_x(position_value)
var y := size.y * (0.44 if enemy else 0.51) + (_enemy_visual_y_offset(actor_id) if enemy else 0.0)
actor.position = Vector2(x - actor.size.x * 0.5, y)
var action: Dictionary = battle_actions.get(actor_id, {})
var state_name := String(action.get("state", "idle" if battle.state.terminal else "advance"))
if hp <= 0:
state_name = "death" if enemy else "idle"
actor.texture = animation.texture("Riftblade" if enemy else _recruit_rig(actor_id), state_name, world_clock)
actor.modulate.a = 0.28 if hp <= 0 else 1.0
actor.visible = hp > 0 or not battle.state.terminal
var bar: ColorRect = battle_hp_bars[actor_id]
bar.position = Vector2(actor.position.x + (actor.size.x - bar.size.x) * 0.5, actor.position.y - 8)
var fill: ColorRect = bar.get_meta("fill")
var ratio := clampf(float(hp) / maxf(1.0, float(bar.get_meta("max_hp"))), 0.0, 1.0)
fill.size.x = maxf(0.0, (bar.size.x - 2.0) * ratio)
var name_label: Label = actor.get_meta("name_label")
name_label.position = Vector2(actor.position.x + (actor.size.x - name_label.size.x) * 0.5, actor.position.y + actor.size.y - 6)
bar.visible = hp > 0 and not battle_result_visible
name_label.visible = hp > 0
func _handle_battle_event(event: Dictionary) -> void:
var kind := String(event.get("kind", ""))
presented_event_counts[kind] = int(presented_event_counts.get(kind, 0)) + 1
if kind == "member_action":
var source := String(event.get("source_member_id", ""))
var target := String(event.get("target_enemy_id", event.get("target_member_id", "")))
var healing := String(event.get("action_tag", "")) == "restore"
battle_actions[source] = {"state": "lantern_heal" if healing else "draw_release" if _recruit_rig(source) == "Farstring" else "basic_attack"}
_spawn_number(int(event.get("delta", 0)), target, JADE if healing else CORAL)
if healing:
_spawn_vfx(target, HEAL_VFX)
if not healing and event.has("target_enemy_id"):
_spawn_projectile(source, target)
elif kind == "enemy_action":
var enemy := String(event.get("source_enemy_id", ""))
var target := String(event.get("target_member_id", ""))
battle_actions[enemy] = {"state": "attack"}
_spawn_number(int(event.get("delta", 0)), target, CORAL)
_spawn_vfx(target, HIT_VFX)
elif kind in ["equipment_trigger", "profession_rule", "enemy_rule"]:
var source_id := String(event.get("member_id", event.get("enemy_id", "")))
var word := _battle_event_word(String(event.get("rule", event.get("catalog_id", ""))))
_spawn_word(source_id, word, GOLD_LIGHT if kind != "enemy_rule" else CORAL)
func _render_battle_result() -> void:
var band := ColorRect.new()
band.name = "P97BattleResultBand"
band.position = Vector2(0, size.y - 72)
band.size = Vector2(size.x, 72)
band.color = Color(0.015, 0.035, 0.075, 0.94)
display_root.add_child(band)
var victory: bool = battle.state.outcome == "victory"
var route: Dictionary = content.routes[battle.state.route_id]
var result := _outlined_label(_tr("胜利 ◆ +%d" % int(route.reward), "VICTORY ◆ +%d" % int(route.reward)) if victory else _tr("队伍战败 · 无奖励", "COMPANY DEFEATED · NO REWARD"), 22, GOLD_LIGHT if victory else CORAL)
result.position = Vector2(18, size.y - 66)
result.size = Vector2(size.x * 0.62, 56)
display_root.add_child(result)
var return_button := _action_button("P97SettleButton", _tr("返回集市", "RETURN TO MARKET"), settle_and_return)
return_button.position = Vector2(size.x - 230, size.y - 62)
return_button.size = Vector2(212, 52)
display_root.add_child(return_button)
func _update_battle_control_labels() -> void:
var pause := find_child("P97PauseButton", true, false) as Button
var speed := find_child("P97SpeedButton", true, false) as Button
if pause != null:
pause.text = "▶" if battle_paused else "Ⅱ"
if speed != null:
speed.text = "%dx" % battle_speed
func _spawn_number(delta_value: int, actor_id: String, color: Color) -> void:
var actor: TextureRect = battle_actors.get(actor_id)
if actor == null or delta_value == 0:
return
var label := _outlined_label(("+" if delta_value > 0 else "") + str(delta_value), 21, color)
label.name = "P97FloatValue"
label.position = actor.position + Vector2(actor.size.x * 0.30, -18)
label.size = Vector2(90, 36)
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
label.z_index = 30
display_root.add_child(label)
var tween := label.create_tween()
tween.set_parallel(true)
tween.tween_property(label, "position:y", label.position.y - (18 if reduced_motion else 42), 0.55)
tween.tween_property(label, "modulate:a", 0.0, 0.55).set_delay(0.12)
tween.chain().tween_callback(label.queue_free)
func _spawn_projectile(source_id: String, target_id: String) -> void:
var source: TextureRect = battle_actors.get(source_id)
var target: TextureRect = battle_actors.get(target_id)
if source == null or target == null:
return
var projectile := _outlined_label("➤", 24, CYAN)
projectile.name = "P97Projectile"
projectile.position = source.position + source.size * 0.45
projectile.size = Vector2(42, 32)
projectile.z_index = 25
display_root.add_child(projectile)
var tween := projectile.create_tween()
tween.tween_property(projectile, "position", target.position + target.size * 0.42, 0.16 if reduced_motion else 0.38)
tween.tween_callback(projectile.queue_free)
func _spawn_vfx(actor_id: String, texture: Texture2D) -> void:
var actor: TextureRect = battle_actors.get(actor_id)
if actor == null:
return
var vfx := TextureRect.new()
vfx.name = "P97HitVFX"
vfx.texture = texture
vfx.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
vfx.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
vfx.custom_minimum_size = Vector2.ZERO
vfx.position = actor.position + actor.size * 0.32
vfx.size = actor.size * 0.36
vfx.z_index = 24
display_root.add_child(vfx)
var tween := vfx.create_tween()
tween.tween_property(vfx, "modulate:a", 0.0, 0.30)
tween.tween_callback(vfx.queue_free)
func _spawn_word(actor_id: String, value: String, color: Color) -> void:
var actor: TextureRect = battle_actors.get(actor_id)
if actor == null or value.is_empty():
return
var label := _outlined_label(value, 11, color)
label.name = "P97EventWord"
label.position = actor.position + Vector2(-25, -32)
label.size = Vector2(actor.size.x + 50, 30)
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
label.z_index = 29
display_root.add_child(label)
var tween := label.create_tween()
tween.tween_property(label, "modulate:a", 0.0, 0.62).set_delay(0.18)
tween.tween_callback(label.queue_free)
func _render_rotate_notice() -> void:
var background := ColorRect.new()
background.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
background.color = INK
display_root.add_child(background)
var message := _outlined_label(_tr("请将手机旋转为横屏\n队伍会在同一条战线上行动", "ROTATE TO LANDSCAPE\nThe Company fights on one continuous line"), 20, WHITE)
message.name = "P97RotateNotice"
message.position = Vector2(24, size.y * 0.36)
message.size = Vector2(size.x - 48, 130)
message.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
display_root.add_child(message)
func _render_missing_entry() -> void:
_add_background(HALL_BACKGROUND)
var label := _outlined_label(_tr("需要已完成的裂潮存档", "A SETTLED RIFT TIDE SAVE IS REQUIRED"), 18, WHITE)
label.position = Vector2(40, size.y * 0.42)
label.size = Vector2(size.x - 80, 72)
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
display_root.add_child(label)
func _add_background(texture: Texture2D) -> void:
var background := TextureRect.new()
background.name = "P97WorldBackground"
background.texture = texture
background.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
background.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
background.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_COVERED
background.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS
background.mouse_filter = Control.MOUSE_FILTER_IGNORE
display_root.add_child(background)
func _add_actor(rig: String, at: Vector2, actor_height: float, node_name: String, ambient: bool, forced_state := "idle") -> TextureRect:
var actor := TextureRect.new()
actor.name = node_name
actor.texture = animation.texture(rig, forced_state, world_clock)
actor.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
actor.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
actor.custom_minimum_size = Vector2.ZERO
actor.position = at
actor.size = Vector2(actor_height, actor_height)
actor.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS
actor.mouse_filter = Control.MOUSE_FILTER_IGNORE
actor.set_meta("rig", rig)
actor.set_meta("animation_state", forced_state)
display_root.add_child(actor)
if ambient:
ambient_actors[node_name] = actor
return actor
func _update_ambient_actors() -> void:
for actor_name in ambient_actors:
var actor: TextureRect = ambient_actors[actor_name]
if is_instance_valid(actor):
actor.texture = animation.texture(String(actor.get_meta("rig", "Hearthguard")), String(actor.get_meta("animation_state", "idle")), world_clock)
func _add_ground_ring(center: Vector2, diameter: float, color: Color, node_name: String) -> void:
var marker := ColorRect.new()
marker.name = node_name
marker.position = center - Vector2(diameter * 0.5, 5)
marker.size = Vector2(diameter, 3)
marker.color = color
marker.mouse_filter = Control.MOUSE_FILTER_IGNORE
display_root.add_child(marker)
func _actor_target(node_name: String, at: Vector2, target_size: Vector2, callback: Callable) -> Button:
var button := Button.new()
button.name = node_name
button.position = at
button.size = Vector2(maxf(48.0, target_size.x), maxf(48.0, target_size.y))
button.flat = true
button.focus_mode = Control.FOCUS_ALL
button.add_theme_stylebox_override("normal", StyleBoxEmpty.new())
button.add_theme_stylebox_override("hover", _flat_style(Color(1, 1, 1, 0.04), WHITE, 2, 4))
button.add_theme_stylebox_override("pressed", _flat_style(Color(GOLD, 0.08), GOLD, 2, 4))
button.add_theme_stylebox_override("focus", _flat_style(Color(1, 1, 1, 0.02), GOLD_LIGHT, 3, 4))
button.pressed.connect(callback)
return button
func _action_button(node_name: String, value: String, callback: Callable) -> Button:
var button := Button.new()
button.name = node_name
button.text = value
button.focus_mode = Control.FOCUS_ALL
button.add_theme_font_size_override("font_size", _scaled(14))
button.add_theme_color_override("font_color", INK)
button.add_theme_stylebox_override("normal", _flat_style(GOLD, GOLD_LIGHT, 2, 4))
button.add_theme_stylebox_override("hover", _flat_style(GOLD_LIGHT, WHITE, 2, 4))
button.add_theme_stylebox_override("pressed", _flat_style(Color("#d9a947"), GOLD_LIGHT, 2, 4))
button.add_theme_stylebox_override("focus", _flat_style(GOLD, WHITE, 3, 4))
button.pressed.connect(callback)
return button
func _secondary_button(node_name: String, value: String, callback: Callable) -> Button:
var button := Button.new()
button.name = node_name
button.text = value
button.focus_mode = Control.FOCUS_ALL
button.add_theme_font_size_override("font_size", _scaled(13))
button.add_theme_color_override("font_color", WHITE)
button.add_theme_stylebox_override("normal", _flat_style(Color(0.03, 0.08, 0.15, 0.88), CYAN, 2, 4))
button.add_theme_stylebox_override("hover", _flat_style(Color(0.06, 0.15, 0.23, 0.96), WHITE, 2, 4))
button.add_theme_stylebox_override("pressed", _flat_style(Color(0.02, 0.05, 0.10, 0.96), GOLD, 2, 4))
button.add_theme_stylebox_override("focus", _flat_style(Color(0.03, 0.08, 0.15, 0.88), GOLD_LIGHT, 3, 4))
button.pressed.connect(callback)
return button
func _outlined_label(value: String, font_size: int, color: Color) -> Label:
var label := Label.new()
label.text = value
label.add_theme_font_size_override("font_size", _scaled(font_size))
label.add_theme_color_override("font_color", color)
label.add_theme_color_override("font_outline_color", Color(0.015, 0.035, 0.075, 0.96))
label.add_theme_constant_override("outline_size", 4)
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
return label
func _flat_style(fill: Color, border: Color, border_width: int, radius: int) -> StyleBoxFlat:
var style := StyleBoxFlat.new()
style.bg_color = fill
style.border_color = border
style.set_border_width_all(border_width)
style.set_corner_radius_all(radius)
style.content_margin_left = 7
style.content_margin_right = 7
style.content_margin_top = 4
style.content_margin_bottom = 4
return style
func _battle_x(position_value: int) -> float:
return clampf(64.0 + float(position_value + 60) * ((size.x - 128.0) / 190.0), 54.0, size.x - 54.0)
func _recruit_rig(recruit_id: String) -> String:
var profession_id := String(_recruit_record(recruit_id).get("profession_id", "PF-B01"))
if content.professions.has(profession_id):
return String(content.professions[profession_id].visual_rig)
return {"PF-B01": "Hearthguard", "PF-B04": "Wayrunner", "PF-B05": "Farstring", "PF-B07": "Kindhand"}.get(profession_id, "Hearthguard")
func _profession_record(profession_id: String) -> Dictionary:
if content.professions.has(profession_id):
return content.professions[profession_id]
return {"name_zh": {"PF-B01": "守卫", "PF-B04": "游侠", "PF-B05": "弓手", "PF-B07": "治疗"}.get(profession_id, profession_id), "name_en": {"PF-B01": "Hearthguard", "PF-B04": "Wayrunner", "PF-B05": "Farstring", "PF-B07": "Kindhand"}.get(profession_id, profession_id), "strength_zh": "稳定的既有职责", "strength_en": "Established reliable role", "limit_zh": "需要队友配合", "limit_en": "Needs team support"}
func _profession_name(recruit_id: String) -> String:
var profession_id := String(_recruit_record(recruit_id).get("profession_id", ""))
var row := _profession_record(profession_id)
return _tr(String(row.name_zh), String(row.name_en))
func _recruit_name(recruit_id: String) -> String:
var record: Dictionary = _recruit_record(recruit_id)
return _tr(String(record.get("name_zh", recruit_id)), String(record.get("name_en", recruit_id)))
func _recruit_record(recruit_id: String) -> Dictionary:
if market != null and market.state.recruit_records.has(recruit_id):
return market.state.recruit_records[recruit_id]
return content.recruits.get(recruit_id, {})
func _equipment_name(catalog_id: String) -> String:
if content.equipment.has(catalog_id):
return _tr(String(content.equipment[catalog_id].name_zh), String(content.equipment[catalog_id].name_en))
return {"EQ-002": _tr("定步护胫", "Frontpin Greaves"), "EQ-012": _tr("开线绳结", "Open-Line Cord")}.get(catalog_id, catalog_id)
func _equipment_texture(catalog_id: String) -> Texture2D:
return OPEN_LINE_TEXTURE if catalog_id in ["EQ-012", "EQ-023", "EQ-036"] else FRONTPIN_TEXTURE
func _equipment_color(catalog_id: String) -> Color:
return {"EQ-021": CYAN, "EQ-023": GOLD_LIGHT, "EQ-028": CORAL, "EQ-036": VIOLET}.get(catalog_id, WHITE)
func _recruit_color(recruit_id: String) -> Color:
var profession_id := String(_recruit_record(recruit_id).get("profession_id", ""))
return {"PF-B02": CORAL, "PF-B03": JADE, "PF-B06": VIOLET, "PF-B08": Color("#ef9cbc"), "PF-B09": Color("#78d9d0"), "PF-B11": GOLD_LIGHT}.get(profession_id, WHITE)
func _enemy_color(enemy_id: String) -> Color:
return {"EN-R1-01": Color("#d2ecf3"), "EN-R1-02": CYAN, "EN-R1-03": Color("#e7b779"), "EN-R1-05": Color("#a9c0db"), "EN-R1-08": JADE, "EN-R1-11": VIOLET}.get(enemy_id, WHITE)
func _enemy_visual_scale(enemy_id: String) -> float:
return {"EN-R1-01": 0.90, "EN-R1-02": 0.82, "EN-R1-03": 1.08, "EN-R1-05": 1.02, "EN-R1-08": 0.88, "EN-R1-11": 1.10}.get(enemy_id, 1.0)
func _enemy_visual_y_offset(enemy_id: String) -> float:
return {"EN-R1-02": 9.0, "EN-R1-03": -4.0, "EN-R1-08": 7.0, "EN-R1-11": -5.0}.get(enemy_id, 0.0)
func _risk_color(risk: int, focused: bool) -> Color:
var base: Color = [Color(0.15, 0.46, 0.34, 0.88), Color(0.56, 0.38, 0.12, 0.90), Color(0.48, 0.16, 0.24, 0.92)][risk]
return base.lightened(0.22) if focused else base
func _battle_actor_name(actor_id: String) -> String:
if content.enemies.has(actor_id):
return _tr(String(content.enemies[actor_id].name_zh), String(content.enemies[actor_id].name_en))
return _profession_name(actor_id)
func _battle_event_word(event_id: String) -> String:
var words := {
"first_projectile_reflect": [_tr("镜反", "REFLECT")],
"front_pushed": [_tr("推位", "PUSH")],
"growth": [_tr("生长", "GROW")],
"command_interrupt": [_tr("打断", "INTERRUPT")],
"needle_post_fired": [_tr("针塔", "NEEDLE")],
"windup_delayed": [_tr("迟滞", "DELAY")],
"EQ-021": [_tr("识破蓄力", "WIND-UP READ")],
"EQ-023": [_tr("后排锁定", "REAR LOCK")],
"EQ-028": [_tr("破护", "GUARD BREAK")],
"EQ-036": [_tr("双目标", "TWIN MARK")],
}
if words.has(event_id):
return String(words[event_id][0])
return event_id.replace("_", " ").to_upper().left(14)
func _tr(zh: String, en: String) -> String:
return en if locale == "en" else zh
func _scaled(value: int) -> int:
return maxi(9, int(round(float(value) * text_scale)))