1062 lines
48 KiB
GDScript
1062 lines
48 KiB
GDScript
class_name P93CoreLoopController
|
|
extends Control
|
|
|
|
const Service = preload("res://p9_3/core_loop_service.gd")
|
|
const State = preload("res://p9_3/core_loop_state.gd")
|
|
const Rules = preload("res://domain/p9_2/battle_rules.gd")
|
|
const AnimationAdapter = preload("res://presentation/animation_adapter.gd")
|
|
|
|
const HALL_BACKGROUND = preload("res://assets/backgrounds/p9_3_guild_hall.png")
|
|
const FIELD_BACKGROUND = preload("res://assets/backgrounds/first_expedition_field.png")
|
|
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")
|
|
|
|
var service := Service.new()
|
|
var animation := AnimationAdapter.new()
|
|
var display_root: Control
|
|
var ambient_actors: Dictionary = {}
|
|
var battle_actors: Dictionary = {}
|
|
var battle_hp_bars: Dictionary = {}
|
|
var battle_visual_x: Dictionary = {}
|
|
var battle_actions: Dictionary = {}
|
|
var battle_tick_accumulator := 0.0
|
|
var battle_visual_active := false
|
|
var battle_result_delay := 0.0
|
|
var battle_result_shown := false
|
|
var world_clock := 0.0
|
|
var shake_clock := 0.0
|
|
var locale := "zh_CN"
|
|
var text_scale := 1.0
|
|
var reduced_motion := false
|
|
var muted := true
|
|
var low_power := false
|
|
var speed_multiplier := 1
|
|
var paused := false
|
|
var returned_from_battle := false
|
|
var render_queued := false
|
|
|
|
|
|
func _ready() -> void:
|
|
set_meta("visible_world_ratio", 0.88)
|
|
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 = "P93DisplayRoot"
|
|
display_root.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
|
add_child(display_root)
|
|
resized.connect(_queue_render_current)
|
|
_render_current()
|
|
|
|
|
|
func _process(delta: float) -> void:
|
|
world_clock += maxf(0.0, delta)
|
|
_update_ambient_actors()
|
|
if battle_visual_active:
|
|
_process_battle(delta)
|
|
if shake_clock > 0.0:
|
|
shake_clock = maxf(0.0, shake_clock - delta)
|
|
var strength := shake_clock / 0.16
|
|
display_root.position = Vector2(sin(world_clock * 90.0) * 4.0 * strength, cos(world_clock * 76.0) * 2.0 * strength)
|
|
elif display_root.position != Vector2.ZERO:
|
|
display_root.position = Vector2.ZERO
|
|
|
|
|
|
func enter_hall() -> void:
|
|
if service.enter_hall().ok:
|
|
_queue_render_current()
|
|
|
|
|
|
func tap_offer(member_id: String) -> void:
|
|
if service.state.phase != State.Phase.HALL:
|
|
return
|
|
if service.state.focused_offer == member_id:
|
|
var result: Dictionary = service.hire(member_id)
|
|
if result.ok:
|
|
_queue_render_current()
|
|
return
|
|
if service.focus_offer(member_id).ok:
|
|
_queue_render_current()
|
|
|
|
|
|
func tap_equipment(item_id: String) -> void:
|
|
if service.focus_equipment(item_id).ok:
|
|
_queue_render_current()
|
|
|
|
|
|
func tap_equipment_wearer(member_id: String) -> void:
|
|
if service.equip_focused(member_id).ok:
|
|
_queue_render_current()
|
|
|
|
|
|
func tap_encounter(encounter_id: String) -> void:
|
|
if service.state.phase != State.Phase.ENCOUNTER:
|
|
return
|
|
if service.state.selected_encounter == encounter_id:
|
|
start_selected_battle()
|
|
return
|
|
if service.select_encounter(encounter_id).ok:
|
|
_queue_render_current()
|
|
|
|
|
|
func start_selected_battle() -> void:
|
|
var result: Dictionary = service.start_battle()
|
|
if not result.ok:
|
|
return
|
|
battle_tick_accumulator = 0.0
|
|
battle_result_delay = 0.0
|
|
battle_result_shown = false
|
|
battle_visual_active = true
|
|
paused = false
|
|
speed_multiplier = 1
|
|
_queue_render_current()
|
|
|
|
|
|
func toggle_pause() -> void:
|
|
if service.state.phase != State.Phase.BATTLE:
|
|
return
|
|
paused = not paused
|
|
service.battle.set_paused(paused)
|
|
_update_battle_control_labels()
|
|
|
|
|
|
func toggle_speed() -> void:
|
|
if service.state.phase != State.Phase.BATTLE:
|
|
return
|
|
speed_multiplier = 2 if speed_multiplier == 1 else 1
|
|
_update_battle_control_labels()
|
|
|
|
|
|
func claim_and_return() -> void:
|
|
var result: Dictionary = service.claim_and_return()
|
|
if not result.ok:
|
|
return
|
|
battle_visual_active = false
|
|
returned_from_battle = true
|
|
_queue_render_current()
|
|
|
|
|
|
func reset_loop() -> void:
|
|
service.state.reset()
|
|
service.battle.state.reset()
|
|
battle_visual_active = false
|
|
returned_from_battle = false
|
|
_queue_render_current()
|
|
|
|
|
|
func _queue_render_current() -> void:
|
|
if render_queued:
|
|
return
|
|
render_queued = true
|
|
call_deferred("_deferred_render_current")
|
|
|
|
|
|
func _deferred_render_current() -> 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()
|
|
return
|
|
match service.state.phase:
|
|
State.Phase.TITLE:
|
|
_render_title()
|
|
State.Phase.HALL:
|
|
_render_hall()
|
|
State.Phase.ENCOUNTER:
|
|
_render_encounters()
|
|
State.Phase.BATTLE, State.Phase.RESULT:
|
|
_render_battle()
|
|
|
|
|
|
func _clear_display() -> void:
|
|
ambient_actors.clear()
|
|
battle_actors.clear()
|
|
battle_hp_bars.clear()
|
|
battle_visual_x.clear()
|
|
for child in display_root.get_children():
|
|
child.free()
|
|
|
|
|
|
func _render_title() -> void:
|
|
_add_background(HALL_BACKGROUND)
|
|
var wash := ColorRect.new()
|
|
wash.name = "P93TitleWash"
|
|
wash.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
|
wash.color = Color(0.01, 0.04, 0.09, 0.34)
|
|
wash.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
display_root.add_child(wash)
|
|
|
|
_add_actor("Hearthguard", Vector2(size.x * 0.63, size.y * 0.49), size.y * 0.48, "P93TitleHearthguard", true)
|
|
_add_actor("Farstring", Vector2(size.x * 0.73, size.y * 0.53), size.y * 0.43, "P93TitleFarstring", true)
|
|
var title := _label(_tr("裂脉远征团", "AETHERBOUND GUILD"), 38, WHITE)
|
|
title.name = "P93Title"
|
|
title.position = Vector2(size.x * 0.055, size.y * 0.24)
|
|
title.size = Vector2(size.x * 0.52, 58)
|
|
display_root.add_child(title)
|
|
var subtitle := _label(_tr("招募队员 · 排列队伍 · 观看他们战斗", "RECRUIT · ORDER · WATCH THEM FIGHT"), 14, CYAN)
|
|
subtitle.position = Vector2(size.x * 0.058, size.y * 0.39)
|
|
subtitle.size = Vector2(size.x * 0.48, 34)
|
|
display_root.add_child(subtitle)
|
|
var start := _action_button("P93EnterHallButton", _tr("进入公会", "ENTER THE GUILD"), enter_hall)
|
|
start.position = Vector2(size.x * 0.058, size.y * 0.62)
|
|
start.size = Vector2(minf(270.0, size.x * 0.34), 56)
|
|
display_root.add_child(start)
|
|
|
|
|
|
func _render_hall() -> void:
|
|
if service.equipment_choice_available():
|
|
_render_equipment_hall()
|
|
return
|
|
if service.state.reward_receipts.size() >= 3 and service.state.equipment_receipts.size() == 1:
|
|
_render_growth_return_hall()
|
|
return
|
|
_add_background(HALL_BACKGROUND)
|
|
_add_compact_hud(_tr("公会大厅", "GUILD HALL"), service.state.coin, service.state.company.size())
|
|
var company_caption := _label(_tr("你的队伍", "YOUR COMPANY"), 13, GOLD_LIGHT)
|
|
company_caption.position = Vector2(28, size.y - 62)
|
|
company_caption.size = Vector2(size.x * 0.30, 30)
|
|
display_root.add_child(company_caption)
|
|
|
|
var actor_height := clampf(size.y * 0.36, 118.0, 146.0)
|
|
for index in range(service.state.company.size()):
|
|
var member_id: String = service.state.company[index]
|
|
_add_actor(member_id, Vector2(52 + index * actor_height * 0.62, size.y - actor_height - 18), actor_height, "P93Company_%s" % member_id, true)
|
|
|
|
if returned_from_battle and service.state.focused_offer.is_empty():
|
|
var returned := _outlined_label(_tr("远征归来 +%d 金币" % service.state.last_reward, "RETURNED +%d COIN" % service.state.last_reward), 17, GOLD_LIGHT)
|
|
returned.name = "P93ReturnReceipt"
|
|
returned.position = Vector2(size.x * 0.34, 72)
|
|
returned.size = Vector2(size.x * 0.34, 42)
|
|
display_root.add_child(returned)
|
|
|
|
var offers: Array[String] = service.state.available_offers
|
|
if offers.is_empty():
|
|
return
|
|
var offer_start := size.x * 0.48
|
|
var offer_spacing := size.x * 0.16
|
|
for index in range(offers.size()):
|
|
var member_id: String = offers[index]
|
|
var focused := service.state.focused_offer == member_id
|
|
var actor_pos := Vector2(offer_start + index * offer_spacing, size.y - actor_height - (32 if focused else 18))
|
|
_add_ground_ring(actor_pos + Vector2(actor_height * 0.13, actor_height * 0.84), actor_height * 0.76, GOLD if focused else Color(0.2, 0.75, 0.82, 0.62), "P93OfferRing_%s" % member_id)
|
|
_add_actor(member_id, actor_pos, actor_height, "P93OfferActor_%s" % member_id, true)
|
|
var offer: Dictionary = service.offer(member_id)
|
|
var caption := _outlined_label("%s ◆%d" % [_offer_name(member_id), int(offer.cost)], 11, GOLD_LIGHT if focused else WHITE)
|
|
caption.name = "P93OfferCaption_%s" % member_id
|
|
caption.position = Vector2(actor_pos.x - 8, actor_pos.y - 28)
|
|
caption.size = Vector2(actor_height + 16, 28)
|
|
caption.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
display_root.add_child(caption)
|
|
var target := _actor_target("P93OfferTarget_%s" % member_id, actor_pos, Vector2(actor_height, actor_height), tap_offer.bind(member_id))
|
|
target.tooltip_text = _tr("点击查看;再次点击招募", "Tap to inspect; tap again to recruit")
|
|
display_root.add_child(target)
|
|
|
|
var incoming_crosswind := _incoming_crosswind_visible()
|
|
if incoming_crosswind:
|
|
_render_incoming_crosswind()
|
|
|
|
if service.state.focused_offer.is_empty():
|
|
if not incoming_crosswind:
|
|
var hint := _outlined_label(_tr("点击一名候选人", "TAP A RECRUIT"), 17, WHITE)
|
|
hint.name = "P93HallHint"
|
|
hint.position = Vector2(size.x * 0.43, 72)
|
|
hint.size = Vector2(size.x * 0.50, 40)
|
|
hint.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
display_root.add_child(hint)
|
|
else:
|
|
var focused_id := service.state.focused_offer
|
|
var data: Dictionary = service.offer(focused_id)
|
|
var detail := _outlined_label(
|
|
"%s ◆ %d\n%s · %s\n%s" % [
|
|
_offer_name(focused_id), int(data.cost),
|
|
_tr(String(data.strength_zh), String(data.strength_en)),
|
|
_tr(String(data.limit_zh), String(data.limit_en)),
|
|
_tr("再次点击本人即可招募", "TAP THIS PERSON AGAIN TO HIRE"),
|
|
],
|
|
12,
|
|
GOLD_LIGHT
|
|
)
|
|
detail.name = "P93OfferDetail"
|
|
detail.position = Vector2(size.x * (0.28 if incoming_crosswind else 0.36), 62)
|
|
detail.size = Vector2(size.x * (0.46 if incoming_crosswind else 0.61), 74)
|
|
detail.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
display_root.add_child(detail)
|
|
|
|
|
|
func _incoming_crosswind_visible() -> bool:
|
|
return service.state.phase == State.Phase.HALL and service.state.reward_receipts.size() == 1 and service.state.company.size() == 2
|
|
|
|
|
|
func _render_incoming_crosswind() -> void:
|
|
var threat_label := _outlined_label(_tr("侧风隘口逼近\n近战 + 远程 ◆ +12", "CROSSWIND APPROACHES\nCONTACT + RANGED ◆ +12"), 11, CORAL)
|
|
threat_label.name = "P94IncomingThreat"
|
|
threat_label.position = Vector2(size.x - 210, 72)
|
|
threat_label.size = Vector2(190, 56)
|
|
threat_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
display_root.add_child(threat_label)
|
|
|
|
var ground_center := Vector2(size.x - 100, size.y - 48)
|
|
_add_ground_ring(ground_center, 112, CORAL, "P94IncomingThreatMark")
|
|
var lead := _add_actor("Riftblade", Vector2(size.x - 168, size.y - 126), 72, "P94IncomingEnemy_0", true)
|
|
lead.modulate = Color(1.0, 0.82, 0.88, 1.0)
|
|
var rear := _add_actor("Riftblade", Vector2(size.x - 108, size.y - 112), 60, "P94IncomingEnemy_1", true)
|
|
rear.modulate = Color(0.78, 0.86, 1.0, 1.0)
|
|
|
|
|
|
func _render_equipment_hall() -> void:
|
|
_add_background(HALL_BACKGROUND)
|
|
_add_compact_hud(_tr("钟门战前整备", "BEFORE BELL BREACH"), service.state.coin, service.state.company.size())
|
|
var actor_height := clampf(size.y * 0.27, 100.0, 112.0)
|
|
var actor_positions: Dictionary = {}
|
|
for index in range(service.state.company.size()):
|
|
var member_id: String = service.state.company[index]
|
|
var actor_position := Vector2(16 + index * 88, size.y - actor_height - 18)
|
|
actor_positions[member_id] = actor_position
|
|
_add_actor(member_id, actor_position, actor_height, "P93Company_%s" % member_id, true)
|
|
_add_member_status(member_id, actor_position + Vector2(-4, -42), Vector2(actor_height + 8, 42), "P95MemberStatus_%s" % member_id)
|
|
|
|
var threat := _outlined_label(_tr("钟门裂口逼近\n第一击会推开前排", "BELL BREACH APPROACHES\nFIRST CONTACT PUSHES THE LEAD"), 11, CORAL)
|
|
threat.name = "P95BellThreat"
|
|
threat.position = Vector2(size.x - 228, 66)
|
|
threat.size = Vector2(210, 58)
|
|
threat.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
display_root.add_child(threat)
|
|
_add_ground_ring(Vector2(size.x - 104, size.y - 48), 118, CORAL, "P95BellThreatMark")
|
|
var bell_lead := _add_actor("Riftblade", Vector2(size.x - 176, size.y - 128), 76, "P95BellEnemy_0", true)
|
|
bell_lead.modulate = Color(1.0, 0.75, 0.82, 1.0)
|
|
var bell_rear := _add_actor("Riftblade", Vector2(size.x - 112, size.y - 114), 62, "P95BellEnemy_1", true)
|
|
bell_rear.modulate = Color(0.78, 0.86, 1.0, 1.0)
|
|
|
|
var item_ids := ["EQ-002", "EQ-012"]
|
|
for index in range(item_ids.size()):
|
|
var item_id: String = item_ids[index]
|
|
var focused := service.state.focused_equipment == item_id
|
|
var center := Vector2(size.x * 0.42 + index * 126, size.y * 0.66)
|
|
_add_ground_ring(center + Vector2(0, 24), 88 if focused else 72, GOLD if focused else CYAN, "P95EquipmentRing_%s" % item_id.replace("-", "_"))
|
|
var icon := TextureRect.new()
|
|
icon.name = "P95EquipmentIcon_%s" % item_id.replace("-", "_")
|
|
icon.custom_minimum_size = Vector2.ZERO
|
|
icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
|
icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
|
icon.texture = _equipment_texture(item_id)
|
|
icon.position = center - Vector2(48, 70 if focused else 62)
|
|
icon.size = Vector2(96, 96)
|
|
icon.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
|
|
icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
display_root.add_child(icon)
|
|
var data: Dictionary = service.equipment(item_id)
|
|
var caption := _outlined_label("%s\n%s" % [_tr(String(data.name_zh), String(data.name_en)), _tr("稳住队列 · 较慢", "HOLD LINE · SLOWER") if item_id == "EQ-002" else _tr("借推抢射 · 弓手暴露", "PUSH SHOT · EXPOSE ARCHER")], 10, GOLD_LIGHT if focused else WHITE)
|
|
caption.name = "P95EquipmentCaption_%s" % item_id.replace("-", "_")
|
|
caption.position = center + Vector2(-65, 20)
|
|
caption.size = Vector2(130, 48)
|
|
caption.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
display_root.add_child(caption)
|
|
var target := _actor_target("P95EquipmentTarget_%s" % item_id.replace("-", "_"), center + Vector2(-54, -72), Vector2(108, 142), tap_equipment.bind(item_id))
|
|
target.tooltip_text = _tr("点击查看这件装备", "Tap to inspect this item")
|
|
display_root.add_child(target)
|
|
|
|
if service.state.focused_equipment.is_empty():
|
|
var hint := _outlined_label(_tr("点击一件战利品", "TAP ONE SALVAGED ITEM"), 15, GOLD_LIGHT)
|
|
hint.name = "P95EquipmentHint"
|
|
hint.position = Vector2(size.x * 0.36, 70)
|
|
hint.size = Vector2(size.x * 0.30, 40)
|
|
hint.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
display_root.add_child(hint)
|
|
else:
|
|
var item_id := service.state.focused_equipment
|
|
var data: Dictionary = service.equipment(item_id)
|
|
var wearer := String(data.wearer)
|
|
var wearer_position: Vector2 = actor_positions[wearer]
|
|
_add_ground_ring(wearer_position + Vector2(actor_height * 0.5, actor_height * 0.90), actor_height * 0.82, GOLD, "P95CompatibleWearerMark")
|
|
var wearer_target := _actor_target("P95WearerTarget_%s" % wearer, wearer_position, Vector2(actor_height, actor_height), tap_equipment_wearer.bind(wearer))
|
|
wearer_target.tooltip_text = _tr("点击发光的队员穿戴", "Tap the marked Recruit to equip")
|
|
display_root.add_child(wearer_target)
|
|
var detail := _outlined_label("%s\n%s · %s\n%s" % [_tr(String(data.name_zh), String(data.name_en)), _tr(String(data.benefit_zh), String(data.benefit_en)), _tr(String(data.cost_zh), String(data.cost_en)), _tr("点击发光的队员穿戴", "TAP THE MARKED RECRUIT")], 10, GOLD_LIGHT)
|
|
detail.name = "P95EquipmentDetail"
|
|
detail.position = Vector2(size.x * 0.35, 58)
|
|
detail.size = Vector2(size.x * 0.35, 68)
|
|
detail.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
display_root.add_child(detail)
|
|
|
|
|
|
func _render_growth_return_hall() -> void:
|
|
_add_background(HALL_BACKGROUND)
|
|
_add_compact_hud(_tr("钟门凯旋", "BELL BREACH CLEARED"), service.state.coin, service.state.company.size())
|
|
var headline := _outlined_label(_tr("全员升级 · 第三战奖励 +%d 金币" % service.state.last_reward, "COMPANY LEVEL UP · THIRD REWARD +%d COIN" % service.state.last_reward), 18, GOLD_LIGHT)
|
|
headline.name = "P95GrowthHeadline"
|
|
headline.position = Vector2(size.x * 0.20, 64)
|
|
headline.size = Vector2(size.x * 0.60, 46)
|
|
headline.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
display_root.add_child(headline)
|
|
var actor_height := clampf(size.y * 0.36, 122.0, 146.0)
|
|
var spacing := size.x * 0.25
|
|
var start_x := size.x * 0.16
|
|
for index in range(service.state.company.size()):
|
|
var member_id: String = service.state.company[index]
|
|
var actor_position := Vector2(start_x + index * spacing, size.y - actor_height - 18)
|
|
_add_ground_ring(actor_position + Vector2(actor_height * 0.5, actor_height * 0.90), actor_height * 0.80, GOLD if not String(service.state.equipped_items[member_id]).is_empty() else CYAN, "P95GrowthRing_%s" % member_id)
|
|
_add_actor(member_id, actor_position, actor_height, "P93Company_%s" % member_id, true, "victory" if member_id == "Hearthguard" else "idle")
|
|
_add_member_status(member_id, actor_position + Vector2(-10, -54), Vector2(actor_height + 20, 52), "P95GrowthStatus_%s" % member_id)
|
|
var item_id := String(service.state.equipped_items[member_id])
|
|
if not item_id.is_empty():
|
|
var icon := TextureRect.new()
|
|
icon.name = "P95EquippedIcon_%s" % member_id
|
|
icon.custom_minimum_size = Vector2.ZERO
|
|
icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
|
icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
|
icon.texture = _equipment_texture(item_id)
|
|
icon.position = actor_position + Vector2(actor_height - 20, actor_height * 0.34)
|
|
icon.size = Vector2(54, 54)
|
|
icon.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
|
|
icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
display_root.add_child(icon)
|
|
|
|
|
|
func _render_encounters() -> void:
|
|
_add_background(HALL_BACKGROUND)
|
|
var p95_bell_only := service.state.reward_receipts.size() == 2 and service.state.equipment_receipts.size() == 1
|
|
_add_compact_hud(_tr("检验新装备", "TEST THE NEW EQUIPMENT") if p95_bell_only else _tr("选择下一场战斗", "CHOOSE THE NEXT FIGHT"), service.state.coin, service.state.company.size())
|
|
var actor_height := 100.0 if p95_bell_only else clampf(size.y * 0.30, 100.0, 126.0)
|
|
for index in range(service.state.company.size()):
|
|
var member_id: String = service.state.company[index]
|
|
var actor_position := Vector2(16 + index * 100, size.y - actor_height - 16) if p95_bell_only else Vector2(36 + index * actor_height * 0.57, size.y - actor_height - 16)
|
|
_add_actor(member_id, actor_position, actor_height, "P93Ready_%s" % member_id, true, "advance")
|
|
if p95_bell_only:
|
|
_add_member_status(member_id, actor_position + Vector2(0, -40), Vector2(96, 40), "P95ReadyStatus_%s" % member_id, true)
|
|
|
|
var encounter_ids := ["bell_breach"] if p95_bell_only else ["open_road", "crosswind", "bell_breach"]
|
|
var start_x := size.x * (0.72 if p95_bell_only else 0.43)
|
|
var spacing := size.x * 0.18
|
|
for index in range(encounter_ids.size()):
|
|
var encounter_id: String = encounter_ids[index]
|
|
var selected := service.state.selected_encounter == encounter_id
|
|
var data: Dictionary = service.encounter(encounter_id)
|
|
var center := Vector2(start_x + index * spacing, size.y * 0.67)
|
|
_add_ground_ring(center, 92.0 if selected else 78.0, CORAL if encounter_id == "bell_breach" else (GOLD if selected else Color(0.35, 0.82, 0.92, 0.68)), "P93EncounterRing_%s" % encounter_id)
|
|
var enemy_count := 1 if encounter_id == "open_road" else 2
|
|
for enemy_index in range(enemy_count):
|
|
var enemy_size := actor_height * (0.58 if enemy_index == 0 else 0.48)
|
|
var enemy_pos := Vector2(center.x - enemy_size * 0.58 + enemy_index * 34, size.y - enemy_size - 58 - enemy_index * 10)
|
|
var enemy := _add_actor("Riftblade", enemy_pos, enemy_size, "P93EncounterEnemy_%s_%d" % [encounter_id, enemy_index], true)
|
|
enemy.modulate = Color(1.0, 0.75, 0.82, 1.0) if encounter_id == "bell_breach" else Color.WHITE
|
|
var caption := _outlined_label("%s\n%s ◆ +%d" % [_encounter_name(encounter_id), _tr(String(data.pressure_zh), String(data.pressure_en)), int(data.reward)], 10, GOLD_LIGHT if selected else WHITE)
|
|
caption.name = "P93EncounterCaption_%s" % encounter_id
|
|
caption.position = Vector2(center.x - 70, 66)
|
|
caption.size = Vector2(140, 58)
|
|
caption.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
display_root.add_child(caption)
|
|
var target := _actor_target("P93EncounterTarget_%s" % encounter_id, Vector2(center.x - 64, 58), Vector2(128, size.y - 82), tap_encounter.bind(encounter_id))
|
|
target.tooltip_text = _tr("点击预览;再次点击出战", "Tap to preview; tap again to fight")
|
|
display_root.add_child(target)
|
|
|
|
var instruction_text := _tr("点击一个敌群", "TAP AN ENEMY GROUP") if service.state.selected_encounter.is_empty() else _tr("再次点击同一敌群,立即出战", "TAP THE SAME GROUP AGAIN TO FIGHT")
|
|
var instruction := _outlined_label(instruction_text, 15, GOLD_LIGHT)
|
|
instruction.name = "P93EncounterHint"
|
|
instruction.position = Vector2(size.x * 0.36, size.y - 47)
|
|
instruction.size = Vector2(size.x * 0.60, 34)
|
|
instruction.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
display_root.add_child(instruction)
|
|
if p95_bell_only:
|
|
var item_id := String(service.state.equipment_receipts[0].item_id)
|
|
var item: Dictionary = service.equipment(item_id)
|
|
var equipment_read := _outlined_label("%s → %s\n%s" % [_tr(String(item.name_zh), String(item.name_en)), _member_name(String(item.wearer)), _tr(String(item.benefit_zh), String(item.benefit_en))], 11, GOLD_LIGHT)
|
|
equipment_read.name = "P95CommittedEquipment"
|
|
equipment_read.position = Vector2(size.x * 0.40, 70)
|
|
equipment_read.size = Vector2(size.x * 0.28, 62)
|
|
equipment_read.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
display_root.add_child(equipment_read)
|
|
|
|
|
|
func _render_battle() -> void:
|
|
_add_background(FIELD_BACKGROUND)
|
|
_add_battle_controls()
|
|
battle_visual_active = true
|
|
var actor_height := clampf(size.y * 0.34, 112.0, 158.0)
|
|
for member_id in service.battle.state.member_ids:
|
|
var actor := _add_actor(member_id, Vector2.ZERO, actor_height, "P93BattleMember_%s" % member_id, false)
|
|
battle_actors[member_id] = actor
|
|
battle_visual_x[member_id] = _battle_x(int(service.battle.state.member_positions.get(member_id, 0)))
|
|
battle_hp_bars[member_id] = _add_actor_health(member_id, JADE)
|
|
for enemy_id in service.battle.state.enemy_ids:
|
|
if int(service.battle.state.enemy_hp.get(enemy_id, 0)) <= 0:
|
|
continue
|
|
var actor := _add_actor("Riftblade", Vector2.ZERO, actor_height * (0.90 if enemy_id == "RiftBulwark" else 0.76), "P93BattleEnemy_%s" % enemy_id, false)
|
|
actor.modulate = Color.WHITE if enemy_id == "RiftBulwark" else Color(0.78, 0.86, 1.0, 1.0)
|
|
battle_actors[enemy_id] = actor
|
|
battle_visual_x[enemy_id] = _battle_x(int(service.battle.state.enemy_positions.get(enemy_id, 100)))
|
|
battle_hp_bars[enemy_id] = _add_actor_health(enemy_id, CORAL)
|
|
_update_battle_visuals(0.0)
|
|
if service.state.phase == State.Phase.RESULT:
|
|
_show_battle_result()
|
|
|
|
|
|
func _process_battle(delta: float) -> void:
|
|
_update_battle_visuals(delta)
|
|
if service.state.phase == State.Phase.RESULT:
|
|
if not battle_result_shown:
|
|
battle_result_delay += delta
|
|
if battle_result_delay >= (0.18 if reduced_motion else 0.72):
|
|
_show_battle_result()
|
|
return
|
|
if paused:
|
|
return
|
|
battle_tick_accumulator += maxf(0.0, delta) * float(speed_multiplier)
|
|
while battle_tick_accumulator >= Rules.TICK_SECONDS and service.state.phase == State.Phase.BATTLE:
|
|
battle_tick_accumulator -= Rules.TICK_SECONDS
|
|
var result: Dictionary = service.advance_battle_ticks(1)
|
|
for event in result.get("events", []):
|
|
_handle_battle_event(event)
|
|
|
|
|
|
func _handle_battle_event(event: Dictionary) -> void:
|
|
match String(event.get("kind", "")):
|
|
"contact":
|
|
_spawn_word(_tr("接敌!", "CONTACT!"), Vector2(size.x * 0.49, size.y * 0.46), GOLD_LIGHT)
|
|
"equipment_trigger":
|
|
if String(event.get("qualifier_en", "")) == "push resisted":
|
|
_set_action("Hearthguard", "guard", 0.64)
|
|
_spawn_word(_tr("稳住队列!", "LINE HELD!"), Vector2(size.x * 0.31, size.y * 0.43), GOLD_LIGHT, "P95BellCue_Frontpin")
|
|
_spawn_vfx("Hearthguard", HEAL_VFX)
|
|
"line_push":
|
|
_set_action("Hearthguard", "guard", 0.42)
|
|
_set_action("Farstring", "advance", 0.48)
|
|
_spawn_word(_tr("前排被推开!", "LEAD PUSHED!"), Vector2(size.x * 0.39, size.y * 0.43), CORAL, "P95BellCue_Push")
|
|
"member_action":
|
|
var source := String(event.get("source_member_id", ""))
|
|
_set_action(source, _member_action_state(source), 0.52)
|
|
if source == "Wayrunner":
|
|
_spawn_role_word(source, _tr("反击!", "COUNTER!"), GOLD_LIGHT)
|
|
if event.has("target_enemy_id"):
|
|
var target_enemy := String(event.target_enemy_id)
|
|
_set_action(target_enemy, "hit", 0.30)
|
|
_spawn_number(int(event.delta), target_enemy, CORAL)
|
|
if source == "Farstring":
|
|
_spawn_projectile(source, target_enemy)
|
|
if String(event.get("qualifier_en", "")) == "extended shot":
|
|
_spawn_word(_tr("开线抢射!", "OPEN-LINE SHOT!"), Vector2(size.x * 0.48, size.y * 0.34), GOLD_LIGHT, "P95BellCue_OpenLine")
|
|
else:
|
|
_spawn_vfx(target_enemy, HIT_VFX)
|
|
else:
|
|
var target_member := String(event.get("target_member_id", source))
|
|
if source == "Kindhand":
|
|
_spawn_role_word(source, _tr("治疗!", "HEAL!"), JADE)
|
|
_spawn_number(int(event.delta), target_member, JADE)
|
|
_spawn_vfx(target_member, HEAL_VFX)
|
|
"enemy_action":
|
|
var source_enemy := String(event.get("source_enemy_id", ""))
|
|
var target_member := String(event.get("target_member_id", ""))
|
|
_set_action(source_enemy, "attack", 0.48)
|
|
_set_action(target_member, "guard" if target_member == "Hearthguard" else "idle", 0.34)
|
|
_spawn_number(int(event.delta), target_member, CORAL)
|
|
_spawn_vfx(target_member, HIT_VFX)
|
|
if not reduced_motion:
|
|
shake_clock = 0.16
|
|
"outcome_locked":
|
|
battle_result_delay = 0.0
|
|
|
|
|
|
func _update_battle_visuals(delta: float) -> void:
|
|
if battle_actors.is_empty():
|
|
return
|
|
for actor_key in battle_actors:
|
|
var actor_id: String = String(actor_key)
|
|
var actor: TextureRect = battle_actors[actor_id]
|
|
var is_enemy: bool = actor_id in service.battle.state.enemy_ids
|
|
var position_value: int = int(service.battle.state.enemy_positions.get(actor_id, 100)) if is_enemy else int(service.battle.state.member_positions.get(actor_id, 0))
|
|
var desired_x: float = _battle_x(position_value)
|
|
var current_x: float = float(battle_visual_x.get(actor_id, desired_x))
|
|
current_x = desired_x if reduced_motion else lerpf(current_x, desired_x, minf(1.0, delta * 8.0))
|
|
battle_visual_x[actor_id] = current_x
|
|
var actor_height: float = actor.size.y
|
|
actor.position = Vector2(current_x - actor.size.x * 0.5, size.y - actor_height - 18)
|
|
var animation_id: String = "Riftblade" if is_enemy else actor_id
|
|
var state_name: String = _battle_actor_state(actor_id, is_enemy)
|
|
var action_clock: float = world_clock
|
|
if battle_actions.has(actor_id):
|
|
var action: Dictionary = battle_actions[actor_id]
|
|
action.clock = float(action.get("clock", 0.0)) + delta
|
|
action.remaining = maxf(0.0, float(action.get("remaining", 0.0)) - delta)
|
|
action_clock = float(action.clock)
|
|
if float(action.remaining) <= 0.0:
|
|
battle_actions.erase(actor_id)
|
|
actor.texture = animation.texture(animation_id, state_name, action_clock)
|
|
var hp_bar: ProgressBar = battle_hp_bars.get(actor_id)
|
|
if hp_bar != null:
|
|
var hp: int = int(service.battle.state.enemy_hp.get(actor_id, 0)) if is_enemy else int(service.battle.state.member_hp.get(actor_id, 0))
|
|
hp_bar.value = hp
|
|
hp_bar.position = Vector2(actor.position.x + actor.size.x * 0.5 - hp_bar.size.x * 0.5, actor.position.y - 9)
|
|
hp_bar.visible = hp > 0 and service.state.phase == State.Phase.BATTLE
|
|
|
|
|
|
func _battle_actor_state(actor_id: String, is_enemy: bool) -> String:
|
|
var hp := int(service.battle.state.enemy_hp.get(actor_id, 0)) if is_enemy else int(service.battle.state.member_hp.get(actor_id, 0))
|
|
if hp <= 0:
|
|
return "death" if is_enemy else "idle"
|
|
if battle_actions.has(actor_id):
|
|
return String(battle_actions[actor_id].state)
|
|
if service.state.phase == State.Phase.RESULT and service.state.last_outcome == "victory" and not is_enemy:
|
|
return "victory" if actor_id == "Hearthguard" else "idle"
|
|
if not _contact_reached():
|
|
return "advance"
|
|
return "idle"
|
|
|
|
|
|
func _member_action_state(member_id: String) -> String:
|
|
return {
|
|
"Hearthguard": "basic_attack",
|
|
"Wayrunner": "evade_counter",
|
|
"Farstring": "draw_release",
|
|
"Kindhand": "lantern_heal",
|
|
}.get(member_id, "idle")
|
|
|
|
|
|
func _set_action(actor_id: String, state_name: String, duration: float) -> void:
|
|
battle_actions[actor_id] = {"state": state_name, "remaining": duration, "clock": 0.0}
|
|
|
|
|
|
func _contact_reached() -> bool:
|
|
var members: Array[String] = service.battle.state.living_member_ids()
|
|
var enemies: Array[String] = service.battle.state.living_enemy_ids()
|
|
if members.is_empty() or enemies.is_empty():
|
|
return true
|
|
return int(service.battle.state.enemy_positions.get(enemies[0], 100)) - int(service.battle.state.member_positions.get(members[0], 0)) <= Rules.CONTACT_RANGE
|
|
|
|
|
|
func _show_battle_result() -> void:
|
|
if battle_result_shown:
|
|
return
|
|
battle_result_shown = true
|
|
var victory := service.state.last_outcome == "victory"
|
|
var reward := int(service.encounter(service.state.selected_encounter).get("reward", 0)) if victory else 0
|
|
var result := _outlined_label(_tr("胜利 ◆ +%d 金币" % reward, "VICTORY ◆ +%d COIN" % reward) if victory else _tr("队伍战败", "COMPANY DEFEATED"), 28, GOLD_LIGHT if victory else CORAL)
|
|
result.name = "P93BattleResult"
|
|
result.position = Vector2(size.x * 0.25, size.y * 0.24)
|
|
result.size = Vector2(size.x * 0.50, 70)
|
|
result.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
display_root.add_child(result)
|
|
if service.state.selected_encounter == "bell_breach" and service.state.equipment_receipts.size() == 1:
|
|
var item_id := String(service.state.equipment_receipts[0].item_id)
|
|
var farstring_hp := int(service.battle.state.member_hp.get("Farstring", 0))
|
|
var result_detail := _outlined_label(
|
|
_tr("钉阵胫甲:队列未乱 · 弓手 %d HP · %d tick" % [farstring_hp, service.battle.state.tick], "FRONTPIN: LINE HELD · ARCHER %d HP · %d TICKS" % [farstring_hp, service.battle.state.tick]) if item_id == "EQ-002" else _tr("开线绳结:弓手抢射后排 · 剩余 %d HP · %d tick" % [farstring_hp, service.battle.state.tick], "OPEN-LINE: REAR SHOT · ARCHER %d HP · %d TICKS" % [farstring_hp, service.battle.state.tick]),
|
|
13,
|
|
GOLD_LIGHT if item_id == "EQ-002" else CORAL
|
|
)
|
|
result_detail.name = "P95BellResultDetail"
|
|
result_detail.position = Vector2(size.x * 0.18, size.y * 0.41)
|
|
result_detail.size = Vector2(size.x * 0.64, 52)
|
|
result_detail.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
display_root.add_child(result_detail)
|
|
for hp_bar in battle_hp_bars.values():
|
|
if is_instance_valid(hp_bar):
|
|
hp_bar.hide()
|
|
var return_button := _action_button("P93ReturnHallButton", _tr("带着结果返回大厅", "RETURN TO THE HALL"), claim_and_return)
|
|
return_button.position = Vector2(size.x * 0.35, size.y - 68)
|
|
return_button.size = Vector2(size.x * 0.30, 52)
|
|
display_root.add_child(return_button)
|
|
|
|
|
|
func _add_compact_hud(context: String, coin: int, company_size: int) -> void:
|
|
var shade := ColorRect.new()
|
|
shade.name = "P93TopShade"
|
|
shade.position = Vector2.ZERO
|
|
shade.size = Vector2(size.x, 54)
|
|
shade.color = Color(0.02, 0.06, 0.12, 0.72)
|
|
shade.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
display_root.add_child(shade)
|
|
var context_label := _label(context, 15, WHITE)
|
|
context_label.position = Vector2(18, 8)
|
|
context_label.size = Vector2(size.x * 0.44, 38)
|
|
context_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
|
display_root.add_child(context_label)
|
|
var resources := _outlined_label("◆ %d ⚔ %d/10" % [coin, company_size], 15, GOLD_LIGHT)
|
|
resources.name = "P93Resources"
|
|
resources.position = Vector2(size.x * 0.54, 8)
|
|
resources.size = Vector2(size.x * 0.28, 38)
|
|
resources.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
|
resources.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
|
display_root.add_child(resources)
|
|
var pause_button := _icon_button("P93MenuButton", "↻", reset_loop)
|
|
pause_button.position = Vector2(size.x - 54, 5)
|
|
pause_button.size = Vector2(46, 44)
|
|
pause_button.tooltip_text = _tr("重新开始本次体验", "Restart this preview")
|
|
display_root.add_child(pause_button)
|
|
|
|
|
|
func _add_battle_controls() -> void:
|
|
var context := _outlined_label(_encounter_name(service.state.selected_encounter), 14, WHITE)
|
|
context.name = "P93BattleContext"
|
|
context.position = Vector2(14, 8)
|
|
context.size = Vector2(size.x * 0.44, 42)
|
|
context.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
|
display_root.add_child(context)
|
|
var pause_button := _icon_button("P93PauseButton", "▶" if paused else "Ⅱ", toggle_pause)
|
|
pause_button.position = Vector2(size.x - 106, 8)
|
|
pause_button.size = Vector2(44, 44)
|
|
display_root.add_child(pause_button)
|
|
var speed_button := _icon_button("P93SpeedButton", "%dx" % speed_multiplier, toggle_speed)
|
|
speed_button.position = Vector2(size.x - 56, 8)
|
|
speed_button.size = Vector2(48, 44)
|
|
display_root.add_child(speed_button)
|
|
|
|
|
|
func _update_battle_control_labels() -> void:
|
|
var pause_button := find_child("P93PauseButton", true, false) as Button
|
|
var speed_button := find_child("P93SpeedButton", true, false) as Button
|
|
if pause_button != null:
|
|
pause_button.text = "▶" if paused else "Ⅱ"
|
|
if speed_button != null:
|
|
speed_button.text = "%dx" % speed_multiplier
|
|
|
|
|
|
func _add_actor_health(actor_id: String, color: Color) -> ProgressBar:
|
|
var is_enemy: bool = actor_id in service.battle.state.enemy_ids
|
|
var maximum: int = int(Rules.enemy_definition(actor_id).get("max_hp", 1)) if is_enemy else int(Rules.member_definition(actor_id).get("max_hp", 1))
|
|
var bar := ProgressBar.new()
|
|
bar.name = "P93Health_%s" % actor_id
|
|
bar.custom_minimum_size = Vector2.ZERO
|
|
bar.size = Vector2(48, 5)
|
|
bar.max_value = maximum
|
|
bar.value = maximum
|
|
bar.show_percentage = false
|
|
bar.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
bar.add_theme_stylebox_override("background", _flat_style(Color(0.02, 0.04, 0.07, 0.90), Color(0.85, 0.91, 0.96, 0.42), 1, 3))
|
|
bar.add_theme_stylebox_override("fill", _flat_style(color, color.lightened(0.18), 0, 3))
|
|
display_root.add_child(bar)
|
|
return bar
|
|
|
|
|
|
func _spawn_number(delta_value: int, actor_id: String, color: Color) -> void:
|
|
var actor: TextureRect = battle_actors.get(actor_id)
|
|
if actor == null:
|
|
return
|
|
var number := _outlined_label(("+" if delta_value > 0 else "") + str(delta_value), 22, color)
|
|
number.name = "P93Float_%s_%d" % [actor_id, Time.get_ticks_msec()]
|
|
number.position = actor.position + Vector2(actor.size.x * 0.36, -6)
|
|
number.size = Vector2(92, 38)
|
|
number.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
display_root.add_child(number)
|
|
var tween := number.create_tween()
|
|
tween.set_parallel(true)
|
|
tween.tween_property(number, "position:y", number.position.y - (18 if reduced_motion else 42), 0.64).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
|
|
tween.tween_property(number, "modulate:a", 0.0, 0.64).set_delay(0.16)
|
|
tween.chain().tween_callback(number.queue_free)
|
|
|
|
|
|
func _spawn_role_word(actor_id: String, value: String, color: Color) -> void:
|
|
var actor: TextureRect = battle_actors.get(actor_id)
|
|
if actor == null:
|
|
return
|
|
_spawn_word(value, actor.position + Vector2(actor.size.x * 0.5 - 75, -20), color, "P94RoleCue_%s" % actor_id)
|
|
|
|
|
|
func _spawn_word(value: String, at: Vector2, color: Color, node_name := "") -> void:
|
|
var word := _outlined_label(value, 17, color)
|
|
if not node_name.is_empty():
|
|
word.name = node_name
|
|
word.position = at
|
|
word.size = Vector2(150, 36)
|
|
word.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
display_root.add_child(word)
|
|
var tween := word.create_tween()
|
|
tween.tween_property(word, "modulate:a", 0.0, 0.72).set_delay(0.28)
|
|
tween.tween_callback(word.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("➤", 28, GOLD_LIGHT)
|
|
projectile.name = "P93Projectile"
|
|
projectile.position = source.position + Vector2(source.size.x * 0.72, source.size.y * 0.38)
|
|
projectile.size = Vector2(52, 36)
|
|
projectile.z_index = 20
|
|
display_root.add_child(projectile)
|
|
var target_position := target.position + Vector2(target.size.x * 0.28, target.size.y * 0.40)
|
|
var tween := projectile.create_tween()
|
|
tween.tween_property(projectile, "position", target_position, 0.46 if not reduced_motion else 0.16).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN)
|
|
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 existing := find_child("P93VFX_%s" % actor_id, true, false)
|
|
if existing != null:
|
|
existing.queue_free()
|
|
var vfx := TextureRect.new()
|
|
vfx.name = "P93VFX_%s" % actor_id
|
|
vfx.custom_minimum_size = Vector2.ZERO
|
|
vfx.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
|
vfx.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
|
vfx.texture = texture
|
|
vfx.position = actor.position + Vector2(actor.size.x * 0.36, actor.size.y * 0.34)
|
|
vfx.size = actor.size * 0.28
|
|
vfx.z_index = 18
|
|
vfx.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
display_root.add_child(vfx)
|
|
var tween := vfx.create_tween()
|
|
tween.set_parallel(true)
|
|
tween.tween_property(vfx, "modulate:a", 0.0, 0.32)
|
|
tween.tween_property(vfx, "scale", Vector2(1.18, 1.18), 0.32).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
|
|
tween.chain().tween_callback(vfx.queue_free)
|
|
|
|
|
|
func _add_background(texture: Texture2D) -> void:
|
|
var background := TextureRect.new()
|
|
background.name = "P93WorldBackground"
|
|
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(character_id: String, at: Vector2, actor_height: float, node_name: String, ambient: bool, forced_state := "idle") -> TextureRect:
|
|
var actor := TextureRect.new()
|
|
actor.name = node_name
|
|
actor.custom_minimum_size = Vector2.ZERO
|
|
actor.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
|
actor.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
|
actor.texture = animation.texture(character_id, forced_state, world_clock)
|
|
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("character_id", character_id)
|
|
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 not is_instance_valid(actor):
|
|
continue
|
|
var character_id := String(actor.get_meta("character_id", "Hearthguard"))
|
|
var state_name := String(actor.get_meta("animation_state", "idle"))
|
|
actor.texture = animation.texture(character_id, state_name, world_clock)
|
|
|
|
|
|
func _add_ground_ring(center: Vector2, diameter: float, color: Color, node_name: String) -> void:
|
|
var marker := Control.new()
|
|
marker.name = node_name
|
|
marker.position = center - Vector2(diameter * 0.5, 8)
|
|
marker.size = Vector2(diameter, 24)
|
|
marker.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
display_root.add_child(marker)
|
|
var line := ColorRect.new()
|
|
line.position = Vector2(0, 11)
|
|
line.size = Vector2(diameter, 3)
|
|
line.color = color
|
|
line.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
marker.add_child(line)
|
|
var diamond := _outlined_label("◆", 15, color)
|
|
diamond.position = Vector2(diameter * 0.5 - 18, -3)
|
|
diamond.size = Vector2(36, 28)
|
|
diamond.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
marker.add_child(diamond)
|
|
|
|
|
|
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 = target_size
|
|
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.05), Color(1, 1, 1, 0.35), 2, 8))
|
|
button.add_theme_stylebox_override("pressed", _flat_style(Color(GOLD, 0.08), GOLD, 2, 8))
|
|
button.add_theme_stylebox_override("focus", _flat_style(Color(1, 1, 1, 0.02), GOLD_LIGHT, 3, 8))
|
|
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(15))
|
|
button.add_theme_color_override("font_color", INK)
|
|
button.add_theme_color_override("font_hover_color", INK)
|
|
button.add_theme_color_override("font_pressed_color", INK)
|
|
button.add_theme_stylebox_override("normal", _flat_style(GOLD, Color("#fff0b0"), 2, 4))
|
|
button.add_theme_stylebox_override("hover", _flat_style(Color("#ffe08a"), 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 _icon_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(15))
|
|
button.add_theme_color_override("font_color", WHITE)
|
|
button.add_theme_stylebox_override("normal", _flat_style(Color(0.03, 0.08, 0.15, 0.80), Color(0.58, 0.78, 0.92, 0.80), 2, 4))
|
|
button.add_theme_stylebox_override("hover", _flat_style(Color(0.05, 0.14, 0.23, 0.92), CYAN, 2, 4))
|
|
button.add_theme_stylebox_override("pressed", _flat_style(Color(0.02, 0.06, 0.12, 0.94), GOLD, 2, 4))
|
|
button.pressed.connect(callback)
|
|
return button
|
|
|
|
|
|
func _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.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
return label
|
|
|
|
|
|
func _outlined_label(value: String, font_size: int, color: Color) -> Label:
|
|
var label := _label(value, font_size, 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", 5)
|
|
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
|
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 = 8
|
|
style.content_margin_right = 8
|
|
style.content_margin_top = 5
|
|
style.content_margin_bottom = 5
|
|
return style
|
|
|
|
|
|
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 = "P93RotateNotice"
|
|
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 _battle_x(position_value: int) -> float:
|
|
return clampf(100.0 + float(position_value + 12) * ((size.x - 200.0) / 124.0), 72.0, size.x - 72.0)
|
|
|
|
|
|
func _add_member_status(member_id: String, at: Vector2, status_size: Vector2, node_name: String, compact := false) -> void:
|
|
var level := int(service.state.member_levels.get(member_id, 1))
|
|
var xp := int(service.state.member_xp.get(member_id, 0))
|
|
var threshold := 20 + 10 * level if level < 10 else 0
|
|
var item_id := String(service.state.equipped_items.get(member_id, ""))
|
|
var item_name := ""
|
|
if not item_id.is_empty():
|
|
var item: Dictionary = service.equipment(item_id)
|
|
item_name = " ◆ %s" % _tr(String(item.name_zh), String(item.name_en))
|
|
var xp_text := _tr("最高" if level >= 10 else "%d/%d XP" % [xp, threshold], "MAX" if level >= 10 else "%d/%d XP" % [xp, threshold])
|
|
var value := "Lv.%d\n%s" % [level, xp_text] if compact else "%s\nLv.%d · %s%s" % [_member_name(member_id), level, xp_text, item_name]
|
|
var status := _outlined_label(value, 9, GOLD_LIGHT)
|
|
status.name = node_name
|
|
status.position = at
|
|
status.size = status_size
|
|
status.clip_text = true
|
|
status.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
display_root.add_child(status)
|
|
|
|
|
|
func _equipment_texture(item_id: String) -> Texture2D:
|
|
return FRONTPIN_TEXTURE if item_id == "EQ-002" else OPEN_LINE_TEXTURE
|
|
|
|
|
|
func _member_name(member_id: String) -> String:
|
|
return {
|
|
"Hearthguard": _tr("艾拉 · 守卫", "Ayla · Hearthguard"),
|
|
"Wayrunner": _tr("托马斯 · 游侠", "Tomas · Wayrunner"),
|
|
"Farstring": _tr("妮娅 · 弓手", "Nia · Farstring"),
|
|
"Kindhand": _tr("赛布尔 · 治疗", "Sable · Kindhand"),
|
|
}.get(member_id, member_id)
|
|
|
|
|
|
func _offer_name(member_id: String) -> String:
|
|
var data: Dictionary = service.offer(member_id)
|
|
return _tr(String(data.get("name_zh", _member_name(member_id))), String(data.get("name_en", _member_name(member_id))))
|
|
|
|
|
|
func _encounter_name(encounter_id: String) -> String:
|
|
var data: Dictionary = service.encounter(encounter_id)
|
|
return _tr(String(data.get("name_zh", encounter_id)), String(data.get("name_en", encounter_id)))
|
|
|
|
|
|
func _tr(zh: String, en: String) -> String:
|
|
return en if locale == "en" else zh
|
|
|
|
|
|
func _scaled(value: int) -> int:
|
|
return maxi(10, int(round(float(value) * text_scale)))
|