1100 lines
47 KiB
GDScript
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)))
|