feat(runtime): render real Guild formation
This commit is contained in:
@@ -468,6 +468,91 @@ func added_rear_animation_state() -> String:
|
||||
return "idle"
|
||||
|
||||
|
||||
func visible_member_ids() -> Array[String]:
|
||||
var members := session_state.current_member_ids()
|
||||
if members.is_empty():
|
||||
for member_id in [front_choice, rear_choice, added_rear]:
|
||||
if not member_id.is_empty() and member_id not in members:
|
||||
members.append(member_id)
|
||||
return members
|
||||
|
||||
|
||||
func visible_member_id(index: int) -> String:
|
||||
var members := visible_member_ids()
|
||||
return members[index] if index >= 0 and index < members.size() else ""
|
||||
|
||||
|
||||
func visible_member_slot(member_id: String) -> String:
|
||||
var index := visible_member_ids().find(member_id)
|
||||
return ["front", "rear", "added"][index] if index >= 0 and index < 3 else ""
|
||||
|
||||
|
||||
func support_member_ids() -> Array[String]:
|
||||
var members := visible_member_ids()
|
||||
return members.slice(1) if members.size() > 1 else []
|
||||
|
||||
|
||||
func profession_action_state(member_id: String) -> String:
|
||||
match member_id:
|
||||
"Hearthguard": return "basic_attack"
|
||||
"Wayrunner": return "evade_counter"
|
||||
"Farstring": return "draw_release"
|
||||
"Kindhand": return "lantern_heal"
|
||||
return "idle"
|
||||
|
||||
|
||||
func profession_defense_state(member_id: String) -> String:
|
||||
if member_id == "Hearthguard":
|
||||
return "guard"
|
||||
if member_id == "Wayrunner":
|
||||
return "evade_counter"
|
||||
return "idle"
|
||||
|
||||
|
||||
func later_battle_animation_state(member_id: String) -> String:
|
||||
var lead_id := visible_member_id(0)
|
||||
var supports := support_member_ids()
|
||||
if battle_phase == 0:
|
||||
return "advance"
|
||||
if page == AppPage.THIRD_BATTLE:
|
||||
if battle_phase == 1 and retained_item == "EQ-061" and member_id == lead_id:
|
||||
return profession_action_state(member_id)
|
||||
if battle_phase == 2 and member_id == lead_id:
|
||||
return profession_defense_state(member_id)
|
||||
if battle_phase == 3 and not supports.is_empty() and member_id == supports[0]:
|
||||
return profession_action_state(member_id)
|
||||
if battle_phase == 4 and supports.size() > 1 and member_id == supports[1]:
|
||||
return profession_action_state(member_id)
|
||||
if battle_phase == 5 and member_id == lead_id:
|
||||
return profession_action_state(member_id)
|
||||
if page == AppPage.FOURTH_BATTLE:
|
||||
if battle_phase == 1 and selected_build_profile() == "tempo" and route_choice == "R6-02" and member_id == lead_id:
|
||||
return profession_action_state(member_id)
|
||||
if battle_phase == 2 and member_id == (session_state.marked_member_id() if route_choice == "R6-03" else lead_id):
|
||||
return profession_defense_state(member_id)
|
||||
if battle_phase == 3 and not supports.is_empty() and member_id == supports[0]:
|
||||
return profession_action_state(member_id)
|
||||
if battle_phase == 4 and supports.size() > 1 and member_id == supports[1]:
|
||||
return profession_action_state(member_id)
|
||||
if battle_phase == 5 and member_id == lead_id:
|
||||
return profession_action_state(member_id)
|
||||
if page == AppPage.FIFTH_BATTLE:
|
||||
var pending_supports := supports.duplicate()
|
||||
if trained_profession() == "Farstring":
|
||||
pending_supports.erase(trained_profession())
|
||||
if battle_phase == 1 and trained_profession() == "Farstring" and member_id == trained_profession():
|
||||
return profession_action_state(member_id)
|
||||
if battle_phase == 2 and member_id == lead_id:
|
||||
return profession_defense_state(member_id)
|
||||
if battle_phase == 3 and member_id == session_state.marked_member_id():
|
||||
return profession_defense_state(member_id)
|
||||
if battle_phase == 4 and not pending_supports.is_empty() and member_id == pending_supports[0]:
|
||||
return profession_action_state(member_id)
|
||||
if battle_phase == 5 and pending_supports.size() > 1 and member_id == pending_supports[1]:
|
||||
return profession_action_state(member_id)
|
||||
return "idle"
|
||||
|
||||
|
||||
func enemy_animation_state() -> String:
|
||||
if page == AppPage.FIFTH_BATTLE:
|
||||
match battle_phase:
|
||||
@@ -1192,6 +1277,11 @@ func render_battle() -> void:
|
||||
var third_cycle := page == AppPage.THIRD_BATTLE
|
||||
var fourth_cycle := page == AppPage.FOURTH_BATTLE
|
||||
var fifth_cycle := page == AppPage.FIFTH_BATTLE
|
||||
var later_cycle := third_cycle or fourth_cycle or fifth_cycle
|
||||
var members := visible_member_ids()
|
||||
var visible_front := visible_member_id(0)
|
||||
var visible_rear := visible_member_id(1)
|
||||
var visible_third := visible_member_id(2)
|
||||
var battle_title := tr_text("第五次远征 · 双压关隘", "Fifth Expedition · Twin-Pressure Pass") if fifth_cycle else (tr_text("第四次远征 · %s" % route_name(route_choice), "Fourth Expedition · %s" % route_name(route_choice)) if fourth_cycle else (tr_text("第三次远征 · 裂隙队长", "Third Expedition · Rift Captain") if third_cycle else (tr_text("第二次远征 · 老兵截道", "Second Expedition · Veteran Ambush") if second_cycle else tr_text("第一次远征 · 风环遗迹", "First Expedition · Windring Ruins"))))
|
||||
render_hud(battle_title)
|
||||
var world_bottom := size.y - 58.0
|
||||
@@ -1202,16 +1292,20 @@ func render_battle() -> void:
|
||||
rear_base_position = Vector2(size.x * (0.26 if contact_reached else 0.08), world_bottom - actor_height * 0.92)
|
||||
added_rear_base_position = Vector2(size.x * (0.12 if contact_reached else -0.06), world_bottom - actor_height * 0.86)
|
||||
enemy_base_position = Vector2(size.x * (0.56 if contact_reached else 0.77), world_bottom - actor_height * 0.84)
|
||||
front_sprite = actor_at(animation_texture(front_choice, front_animation_state(), battle_clock), front_base_position, Vector2(actor_width, actor_height), "FrontActor")
|
||||
rear_sprite = actor_at(animation_texture(rear_choice, rear_animation_state(), battle_clock), rear_base_position, Vector2(actor_width * 0.9, actor_height * 0.9), "RearActor")
|
||||
if (third_cycle or fourth_cycle or fifth_cycle) and not added_rear.is_empty():
|
||||
added_rear_sprite = actor_at(animation_texture(added_rear, added_rear_animation_state(), battle_clock), added_rear_base_position, Vector2(actor_width * 0.86, actor_height * 0.86), "AddedRearActor")
|
||||
front_sprite = actor_at(animation_texture(visible_front, later_battle_animation_state(visible_front) if later_cycle else front_animation_state(), battle_clock), front_base_position, Vector2(actor_width, actor_height), "FrontActor")
|
||||
rear_sprite = actor_at(animation_texture(visible_rear, later_battle_animation_state(visible_rear) if later_cycle else rear_animation_state(), battle_clock), rear_base_position, Vector2(actor_width * 0.9, actor_height * 0.9), "RearActor")
|
||||
front_sprite.set_meta("member_id", visible_front)
|
||||
rear_sprite.set_meta("member_id", visible_rear)
|
||||
if later_cycle and members.size() > 2:
|
||||
added_rear_sprite = actor_at(animation_texture(visible_third, later_battle_animation_state(visible_third), battle_clock), added_rear_base_position, Vector2(actor_width * 0.86, actor_height * 0.86), "AddedRearActor")
|
||||
added_rear_sprite.set_meta("member_id", visible_third)
|
||||
enemy_sprite = actor_at(animation_texture("Riftblade", enemy_animation_state(), battle_clock), enemy_base_position, Vector2(actor_width * 0.82, actor_height * 0.82), "EnemyActor")
|
||||
|
||||
add_health_bar("FrontHealth", Vector2(22, 72), Vector2(size.x * 0.29, 14), front_hp, 100, candidate_name(front_choice))
|
||||
add_health_bar("FrontHealth", Vector2(22, 72), Vector2(size.x * 0.29, 14), session_state.member_health(visible_front), 100, candidate_name(visible_front))
|
||||
add_health_bar("EnemyHealth", Vector2(size.x * 0.69 - 22, 72), Vector2(size.x * 0.29, 14), enemy_hp, 140 if fifth_cycle else (130 if third_cycle else (120 if fourth_cycle else 100)), tr_text("双压守卫", "Twin Warden") if fifth_cycle else (tr_text("路线守卫", "Route Warden") if fourth_cycle else (tr_text("裂隙队长", "Rift Captain") if third_cycle else tr_text("裂影刃手", "Riftblade"))))
|
||||
if fifth_cycle or (fourth_cycle and route_choice == "R6-03"):
|
||||
add_health_bar("RearHealth", Vector2(22, 101), Vector2(size.x * 0.20, 12), rear_hp, 100, candidate_name(rear_choice))
|
||||
var marked_id := session_state.marked_member_id()
|
||||
add_health_bar("RearHealth", Vector2(22, 101), Vector2(size.x * 0.20, 12), session_state.member_health(marked_id), 100, candidate_name(marked_id))
|
||||
var event := battle_event_text()
|
||||
var ribbon := PanelContainer.new()
|
||||
ribbon.name = "BattleEvent"
|
||||
@@ -1252,14 +1346,13 @@ func render_battle() -> void:
|
||||
add_signal_label("!", Vector2(size.x * 0.61, size.y * 0.31), COLOR_CRIMSON)
|
||||
elif second_cycle and adaptation_choice == "EQ-061" and battle_phase == 2:
|
||||
add_signal_label("!", Vector2(size.x * 0.61, size.y * 0.31), COLOR_CRIMSON)
|
||||
elif battle_phase == 3 and rear_choice == "Farstring":
|
||||
elif not later_cycle and battle_phase == 3 and rear_choice == "Farstring":
|
||||
projectile = add_signal_label("➤", Vector2(size.x * 0.48, size.y * 0.50), COLOR_GOLD_LIGHT)
|
||||
elif (third_cycle or fourth_cycle) and battle_phase == 4 and added_rear == "Farstring":
|
||||
projectile = add_signal_label("➤", Vector2(size.x * 0.45, size.y * 0.52), COLOR_GOLD_LIGHT)
|
||||
elif fifth_cycle and battle_phase == 4 and rear_choice == "Farstring" and trained_member != "rear":
|
||||
projectile = add_signal_label("➤", Vector2(size.x * 0.45, size.y * 0.52), COLOR_GOLD_LIGHT)
|
||||
elif fifth_cycle and battle_phase == 5 and added_rear == "Farstring" and trained_member != "added":
|
||||
projectile = add_signal_label("➤", Vector2(size.x * 0.42, size.y * 0.53), COLOR_GOLD_LIGHT)
|
||||
elif later_cycle:
|
||||
for member_id in members:
|
||||
if later_battle_animation_state(member_id) == "draw_release":
|
||||
projectile = add_signal_label("➤", Vector2(size.x * 0.45, size.y * 0.52), COLOR_GOLD_LIGHT)
|
||||
break
|
||||
render_battle_feedback(actor_width, actor_height)
|
||||
update_battle_feedback_visual()
|
||||
|
||||
@@ -1395,11 +1488,18 @@ func render_guild_return() -> void:
|
||||
add_full_wash(Color(0.012, 0.028, 0.06, 0.16))
|
||||
render_hud(tr_text("公会庭院 · 第三次远征准备", "Guild Yard · Prepare the third expedition"))
|
||||
var actor_height: float = clampf(size.y * 0.29, 98.0, 178.0)
|
||||
actor_at(animation_texture(front_choice, "idle", victory_clock), Vector2(size.x * 0.055, size.y - actor_height - 34), Vector2(actor_height, actor_height), "GuildFront")
|
||||
actor_at(animation_texture(rear_choice, "idle", victory_clock), Vector2(size.x * 0.19, size.y - actor_height * 0.9 - 34), Vector2(actor_height * 0.9, actor_height * 0.9), "GuildRear")
|
||||
if guild_choice == "recruit_rear":
|
||||
var preview_rear := missing_rear_profession()
|
||||
actor_at(animation_texture(preview_rear, "idle", victory_clock), Vector2(size.x * 0.315, size.y - actor_height * 0.82 - 36), Vector2(actor_height * 0.82, actor_height * 0.82), "GuildRecruitPreview")
|
||||
var preview_order := guild_preview_order()
|
||||
var preview_names := ["GuildFront", "GuildRear", "GuildRecruitPreview"]
|
||||
for index in range(preview_order.size()):
|
||||
var member_id: String = preview_order[index]
|
||||
var actor_scale: float = [1.0, 0.9, 0.82][index]
|
||||
var actor := actor_at(
|
||||
animation_texture(member_id, "idle", victory_clock),
|
||||
Vector2(size.x * (0.055 + 0.135 * index), size.y - actor_height * actor_scale - 34),
|
||||
Vector2(actor_height * actor_scale, actor_height * actor_scale),
|
||||
preview_names[index]
|
||||
)
|
||||
actor.set_meta("member_id", member_id)
|
||||
|
||||
var resources := PanelContainer.new()
|
||||
resources.name = "GuildResources"
|
||||
@@ -1441,7 +1541,9 @@ func guild_choice_button(id: String) -> Button:
|
||||
if id == "retain_item":
|
||||
button.text = tr_text("▣ 保留%s · 12 金币\n小队保持 2 人|剩余 8 金币\n继续发挥已验证的装备效果" % adaptation_name(adaptation_choice), "▣ RETAIN %s · 12 Coin\nCompany stays 2 | 8 Coin left\nKeep the proven item effect" % adaptation_name(adaptation_choice))
|
||||
else:
|
||||
button.text = tr_text("+ 招募%s · 14 金币\n小队扩至 3 人|剩余 6 金币\n归还借用装备,补齐弓手与治疗" % candidate_name(missing_rear_profession()), "+ RECRUIT %s · 14 Coin\nCompany grows to 3 | 6 Coin left\nReturn item; gain Archer and Healer" % candidate_name(missing_rear_profession()))
|
||||
var recruit_id := session_service.mapped_guild_recruit()
|
||||
var position := guild_recruit_position(recruit_id)
|
||||
button.text = tr_text("+ 招募%s · 14 金币\n小队扩至 3 人|剩余 6 金币\n加入%s,系统按职责重新编队" % [candidate_name(recruit_id), position], "+ RECRUIT %s · 14 Coin\nCompany grows to 3 | 6 Coin left\nJoins as %s; system reforms by role" % [candidate_name(recruit_id), position])
|
||||
button.focus_mode = Control.FOCUS_ALL
|
||||
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
button.custom_minimum_size = Vector2(215, 112)
|
||||
@@ -1458,10 +1560,7 @@ func render_session_result() -> void:
|
||||
add_full_wash(Color(0.02, 0.035, 0.075, 0.18))
|
||||
render_hud(tr_text("首轮远征完成", "First Expedition Cycle Complete"))
|
||||
var actor_height: float = clampf(size.y * 0.29, 98.0, 180.0)
|
||||
actor_at(animation_texture(front_choice, "victory" if front_choice == "Hearthguard" else "idle", victory_clock), Vector2(size.x * 0.05, size.y - actor_height - 38), Vector2(actor_height, actor_height), "SessionFront")
|
||||
actor_at(animation_texture(rear_choice, "idle", victory_clock), Vector2(size.x * 0.18, size.y - actor_height * 0.9 - 38), Vector2(actor_height * 0.9, actor_height * 0.9), "SessionRear")
|
||||
if not added_rear.is_empty():
|
||||
actor_at(animation_texture(added_rear, "idle", victory_clock), Vector2(size.x * 0.30, size.y - actor_height * 0.84 - 38), Vector2(actor_height * 0.84, actor_height * 0.84), "SessionAddedRear")
|
||||
render_visible_party("Session", Vector2(size.x * 0.05, size.y - actor_height - 38), actor_height)
|
||||
actor_at(animation_texture("Riftblade", "death", 99.0), Vector2(size.x * 0.45, size.y - actor_height * 0.52 - 36), Vector2(actor_height * 0.52, actor_height * 0.52), "SessionEnemy")
|
||||
var result := PanelContainer.new()
|
||||
result.name = "SessionResultPanel"
|
||||
@@ -1479,7 +1578,8 @@ func render_session_result() -> void:
|
||||
explanation.name = "SessionResultExplanation"
|
||||
box.add_child(explanation)
|
||||
var company_size := 3 if not added_rear.is_empty() else 2
|
||||
var facts := centered_label(tr_text("金币 %d | 小队 %d 人\n前排生命 %d / 100 | 用时 %.2f 秒" % [coin, company_size, front_hp, third_result_time], "Coin %d | Company %d\nFront HP %d / 100 | Time %.2f s" % [coin, company_size, front_hp, third_result_time]), 12, Color("#6d5523"))
|
||||
var lead_id := visible_member_id(0)
|
||||
var facts := centered_label(tr_text("金币 %d | 小队 %d 人\n先行者%s %d / 100 | 用时 %.2f 秒" % [coin, company_size, candidate_name(lead_id), session_state.member_health(lead_id), third_result_time], "Coin %d | Company %d\nLead %s %d / 100 | Time %.2f s" % [coin, company_size, candidate_name(lead_id), session_state.member_health(lead_id), third_result_time]), 12, Color("#6d5523"))
|
||||
facts.name = "SessionResultFacts"
|
||||
box.add_child(facts)
|
||||
box.add_child(game_button("OpenRouteBoardButton", tr_text("查看下一轮三条路线", "Compare the next three routes"), open_route_board, true))
|
||||
@@ -1490,10 +1590,7 @@ func render_route_board() -> void:
|
||||
add_full_wash(Color(0.015, 0.03, 0.07, 0.25))
|
||||
render_hud(tr_text("第二轮 · 选择下一条路线", "Second Cycle · Choose the next route"))
|
||||
var actor_height: float = clampf(size.y * 0.22, 82.0, 138.0)
|
||||
actor_at(animation_texture(front_choice, "idle", victory_clock), Vector2(20, size.y - actor_height - 24), Vector2(actor_height, actor_height), "RouteFront")
|
||||
actor_at(animation_texture(rear_choice, "idle", victory_clock), Vector2(100, size.y - actor_height * 0.90 - 24), Vector2(actor_height * 0.90, actor_height * 0.90), "RouteRear")
|
||||
if not added_rear.is_empty():
|
||||
actor_at(animation_texture(added_rear, "idle", victory_clock), Vector2(176, size.y - actor_height * 0.84 - 24), Vector2(actor_height * 0.84, actor_height * 0.84), "RouteAddedRear")
|
||||
render_visible_party("Route", Vector2(20, size.y - actor_height - 24), actor_height, 80.0)
|
||||
var build_panel := PanelContainer.new()
|
||||
build_panel.name = "RouteBuildSummary"
|
||||
build_panel.position = Vector2(16, 72)
|
||||
@@ -1601,10 +1698,7 @@ func render_second_cycle_result() -> void:
|
||||
add_full_wash(Color(0.02, 0.035, 0.075, 0.18))
|
||||
render_hud(tr_text("第二轮路线完成", "Second-Cycle Route Complete"))
|
||||
var actor_height: float = clampf(size.y * 0.28, 96.0, 176.0)
|
||||
actor_at(animation_texture(front_choice, "victory" if front_choice == "Hearthguard" else "idle", victory_clock), Vector2(size.x * 0.05, size.y - actor_height - 36), Vector2(actor_height, actor_height), "CycleResultFront")
|
||||
actor_at(animation_texture(rear_choice, "idle", victory_clock), Vector2(size.x * 0.18, size.y - actor_height * 0.90 - 36), Vector2(actor_height * 0.90, actor_height * 0.90), "CycleResultRear")
|
||||
if not added_rear.is_empty():
|
||||
actor_at(animation_texture(added_rear, "idle", victory_clock), Vector2(size.x * 0.30, size.y - actor_height * 0.84 - 36), Vector2(actor_height * 0.84, actor_height * 0.84), "CycleResultAddedRear")
|
||||
render_visible_party("CycleResult", Vector2(size.x * 0.05, size.y - actor_height - 36), actor_height)
|
||||
actor_at(animation_texture("Riftblade", "death", 99.0), Vector2(size.x * 0.45, size.y - actor_height * 0.50 - 34), Vector2(actor_height * 0.50, actor_height * 0.50), "CycleResultEnemy")
|
||||
var result := PanelContainer.new()
|
||||
result.name = "SecondCycleResultPanel"
|
||||
@@ -1621,7 +1715,9 @@ func render_second_cycle_result() -> void:
|
||||
explanation.name = "SecondCycleExplanation"
|
||||
box.add_child(explanation)
|
||||
var company_size := 3 if not added_rear.is_empty() else 2
|
||||
var facts := centered_label(tr_text("奖励 +%d 金币 | 当前金币 %d\n小队 %d 人 | 前排 %d 后排 %d | %.2f 秒" % [route_reward(route_choice), coin, company_size, front_hp, rear_hp, fourth_result_time], "Reward +%d Coin | Current %d\nCompany %d | Front %d Rear %d | %.2f s" % [route_reward(route_choice), coin, company_size, front_hp, rear_hp, fourth_result_time]), 11, Color("#6d5523"))
|
||||
var lead_id := visible_member_id(0)
|
||||
var marked_id := session_state.marked_member_id()
|
||||
var facts := centered_label(tr_text("奖励 +%d 金币 | 当前金币 %d\n先行%s %d 标记%s %d | %.2f 秒" % [route_reward(route_choice), coin, candidate_name(lead_id), session_state.member_health(lead_id), candidate_name(marked_id), session_state.member_health(marked_id), fourth_result_time], "Reward +%d Coin | Current %d\nLead %s %d Marked %s %d | %.2f s" % [route_reward(route_choice), coin, candidate_name(lead_id), session_state.member_health(lead_id), candidate_name(marked_id), session_state.member_health(marked_id), fourth_result_time]), 11, Color("#6d5523"))
|
||||
facts.name = "SecondCycleFacts"
|
||||
box.add_child(facts)
|
||||
box.add_child(game_button("OpenTrainingButton", tr_text("使用历练印记培养一名成员", "Train one member with the Veteran Mark"), open_training, true))
|
||||
@@ -1639,7 +1735,8 @@ func render_training() -> void:
|
||||
var slot: String = slots[index]
|
||||
var profession := member_profession(slot)
|
||||
var actor_x := 18.0 + slot_width * float(index) + (slot_width - actor_height) * 0.5
|
||||
actor_at(animation_texture(profession, "idle", victory_clock), Vector2(actor_x, 82), Vector2(actor_height, actor_height), "TrainingActor_%s" % slot)
|
||||
var actor := actor_at(animation_texture(profession, "idle", victory_clock), Vector2(actor_x, 82), Vector2(actor_height, actor_height), "TrainingActor_%s" % slot)
|
||||
actor.set_meta("member_id", profession)
|
||||
var choice := training_button(slot)
|
||||
choice.position = Vector2(20.0 + slot_width * float(index), 205)
|
||||
choice.size = Vector2(slot_width - 8.0, 118)
|
||||
@@ -1671,6 +1768,7 @@ func training_button(slot: String) -> Button:
|
||||
var selected := training_choice == slot
|
||||
var button := Button.new()
|
||||
button.name = "Training_%s" % slot.capitalize()
|
||||
button.set_meta("member_id", profession)
|
||||
button.text = "%s · %s\nLv.1 → Lv.2\n%s" % [member_slot_label(slot), candidate_name(profession), training_effect_short(slot)]
|
||||
button.focus_mode = Control.FOCUS_ALL
|
||||
button.custom_minimum_size = Vector2(146, 108)
|
||||
@@ -1718,10 +1816,7 @@ func render_third_cycle_result() -> void:
|
||||
add_full_wash(Color(0.02, 0.035, 0.075, 0.18))
|
||||
render_hud(tr_text("第三轮成长完成", "Third-Cycle Growth Complete"))
|
||||
var actor_height: float = clampf(size.y * 0.28, 96.0, 176.0)
|
||||
actor_at(animation_texture(front_choice, "victory" if front_choice == "Hearthguard" else "idle", victory_clock), Vector2(size.x * 0.05, size.y - actor_height - 36), Vector2(actor_height, actor_height), "GrowthResultFront")
|
||||
actor_at(animation_texture(rear_choice, "idle", victory_clock), Vector2(size.x * 0.18, size.y - actor_height * 0.90 - 36), Vector2(actor_height * 0.90, actor_height * 0.90), "GrowthResultRear")
|
||||
if not added_rear.is_empty():
|
||||
actor_at(animation_texture(added_rear, "idle", victory_clock), Vector2(size.x * 0.30, size.y - actor_height * 0.84 - 36), Vector2(actor_height * 0.84, actor_height * 0.84), "GrowthResultAddedRear")
|
||||
render_visible_party("GrowthResult", Vector2(size.x * 0.05, size.y - actor_height - 36), actor_height)
|
||||
actor_at(animation_texture("Riftblade", "death", 99.0), Vector2(size.x * 0.45, size.y - actor_height * 0.50 - 34), Vector2(actor_height * 0.50, actor_height * 0.50), "GrowthResultEnemy")
|
||||
var result := PanelContainer.new()
|
||||
result.name = "ThirdCycleResultPanel"
|
||||
@@ -1738,7 +1833,9 @@ func render_third_cycle_result() -> void:
|
||||
explanation.name = "ThirdCycleExplanation"
|
||||
box.add_child(explanation)
|
||||
var company_size := member_slots().size()
|
||||
var facts := centered_label(tr_text("奖励 +%d 金币 | 当前金币 %d\n小队 %d 人 | 前排 %d 后排 %d | %.2f 秒" % [FIFTH_BATTLE_REWARD, coin, company_size, front_hp, rear_hp, fifth_result_time], "Reward +%d Coin | Current %d\nCompany %d | Front %d Rear %d | %.2f s" % [FIFTH_BATTLE_REWARD, coin, company_size, front_hp, rear_hp, fifth_result_time]), 11, Color("#6d5523"))
|
||||
var lead_id := visible_member_id(0)
|
||||
var marked_id := session_state.marked_member_id()
|
||||
var facts := centered_label(tr_text("奖励 +%d 金币 | 当前金币 %d\n先行%s %d 标记%s %d | %.2f 秒" % [FIFTH_BATTLE_REWARD, coin, candidate_name(lead_id), session_state.member_health(lead_id), candidate_name(marked_id), session_state.member_health(marked_id), fifth_result_time], "Reward +%d Coin | Current %d\nLead %s %d Marked %s %d | %.2f s" % [FIFTH_BATTLE_REWARD, coin, candidate_name(lead_id), session_state.member_health(lead_id), candidate_name(marked_id), session_state.member_health(marked_id), fifth_result_time]), 11, Color("#6d5523"))
|
||||
facts.name = "ThirdCycleFacts"
|
||||
box.add_child(facts)
|
||||
var levels := centered_label(member_levels_summary(), 10, Color("#6d5523"))
|
||||
@@ -2129,13 +2226,8 @@ func commit_phase_impact(phase: int, refresh_runtime := true) -> void:
|
||||
func legacy_feedback_target(member_id: String) -> String:
|
||||
if member_id in ["enemy", "front", "rear", "added"]:
|
||||
return member_id
|
||||
if member_id == session_state.lead_id():
|
||||
return "front"
|
||||
if member_id == session_state.marked_member_id():
|
||||
return "rear"
|
||||
if member_id == added_rear:
|
||||
return "added"
|
||||
return member_id
|
||||
var slot := visible_member_slot(member_id)
|
||||
return slot if not slot.is_empty() else member_id
|
||||
|
||||
|
||||
func commit_second_phase_impact(phase: int, refresh_runtime := true) -> void:
|
||||
@@ -2219,14 +2311,20 @@ func _process(delta: float) -> void:
|
||||
if added_rear_sprite != null and is_instance_valid(added_rear_sprite):
|
||||
added_rear_sprite.position.x = lerp(-added_rear_sprite.size.x, size.x * 0.10, progress)
|
||||
enemy_sprite.position.x = lerp(size.x * 0.82, size.x * 0.57, progress)
|
||||
front_sprite.texture = animation_texture(front_choice, front_animation_state(), battle_clock)
|
||||
rear_sprite.texture = animation_texture(rear_choice, rear_animation_state(), battle_clock)
|
||||
var later_cycle := page in [AppPage.THIRD_BATTLE, AppPage.FOURTH_BATTLE, AppPage.FIFTH_BATTLE]
|
||||
var visible_front := visible_member_id(0)
|
||||
var visible_rear := visible_member_id(1)
|
||||
var visible_third := visible_member_id(2)
|
||||
front_sprite.texture = animation_texture(visible_front, later_battle_animation_state(visible_front) if later_cycle else front_animation_state(), battle_clock)
|
||||
rear_sprite.texture = animation_texture(visible_rear, later_battle_animation_state(visible_rear) if later_cycle else rear_animation_state(), battle_clock)
|
||||
if added_rear_sprite != null and is_instance_valid(added_rear_sprite):
|
||||
added_rear_sprite.texture = animation_texture(added_rear, added_rear_animation_state(), battle_clock)
|
||||
added_rear_sprite.texture = animation_texture(visible_third, later_battle_animation_state(visible_third), battle_clock)
|
||||
enemy_sprite.texture = animation_texture("Riftblade", enemy_animation_state(), battle_clock)
|
||||
var projectile_actor_is_active := (battle_phase == 3 and rear_choice == "Farstring") or (page in [AppPage.THIRD_BATTLE, AppPage.FOURTH_BATTLE] and battle_phase == 4 and added_rear == "Farstring")
|
||||
if page == AppPage.FIFTH_BATTLE:
|
||||
projectile_actor_is_active = (battle_phase == 1 and trained_profession() == "Farstring") or (battle_phase == 4 and rear_choice == "Farstring" and trained_member != "rear") or (battle_phase == 5 and added_rear == "Farstring" and trained_member != "added")
|
||||
var projectile_actor_is_active := false
|
||||
for member_id in visible_member_ids():
|
||||
if (later_battle_animation_state(member_id) if later_cycle else (rear_animation_state() if member_id == visible_rear else front_animation_state())) == "draw_release":
|
||||
projectile_actor_is_active = true
|
||||
break
|
||||
if projectile != null and is_instance_valid(projectile) and projectile_actor_is_active and not reduced_motion:
|
||||
var projectile_start := 0.08
|
||||
var projectile_end := phase_impact_time(battle_phase) + 0.10
|
||||
@@ -2530,18 +2628,21 @@ func second_battle_event_text() -> String:
|
||||
|
||||
|
||||
func third_battle_event_text() -> String:
|
||||
var lead_id := visible_member_id(0)
|
||||
var second_id := visible_member_id(1)
|
||||
var third_id := visible_member_id(2)
|
||||
match battle_phase:
|
||||
0:
|
||||
return tr_text("小队向裂隙队长推进…", "The squad advances on the Rift Captain…")
|
||||
1:
|
||||
return tr_text("晨钟先手:前排抢在队长之前行动", "Early Bell: the front acts before the captain") if retained_item == "EQ-061" else tr_text("队长蓄力,准备撕开前排", "The captain gathers force against the front")
|
||||
return tr_text("晨钟先手:%s抢在队长之前行动" % candidate_name(lead_id), "Early Bell: %s acts before the captain" % candidate_name(lead_id)) if retained_item == "EQ-061" else tr_text("队长蓄力,准备攻击先行者%s" % candidate_name(lead_id), "The captain gathers force against lead %s" % candidate_name(lead_id))
|
||||
2:
|
||||
return tr_text("钉阵胫甲守住阵线,重击只造成 -10", "Frontpin holds the line; the strike deals only -10") if retained_item == "EQ-002" else tr_text("队长重击前排;现在需要后排支援", "The captain hits the front; rear support is needed")
|
||||
return tr_text("钉阵胫甲让%s守住阵线,重击只造成 -10" % candidate_name(lead_id), "Frontpin lets %s hold; the strike deals only -10" % candidate_name(lead_id)) if retained_item == "EQ-002" else tr_text("队长重击%s;现在需要队友支援" % candidate_name(lead_id), "The captain hits %s; support is needed" % candidate_name(lead_id))
|
||||
3:
|
||||
return tr_text("原后排弓手命中队长", "The original Archer hits the captain") if rear_choice == "Farstring" else tr_text("原后排治疗为前排恢复 +12", "The original Healer restores +12 to the front")
|
||||
return support_action_text(second_id, lead_id, "第二位", "second position")
|
||||
4:
|
||||
if not added_rear.is_empty():
|
||||
return tr_text("新弓手补上远程伤害", "The new Archer adds ranged damage") if added_rear == "Farstring" else tr_text("新治疗补上 +12 恢复", "The new Healer adds +12 recovery")
|
||||
if not third_id.is_empty():
|
||||
return support_action_text(third_id, lead_id, "第三位", "third position")
|
||||
return tr_text("保留装备继续放大两人连携", "The retained item amplifies the two-person follow-up")
|
||||
5:
|
||||
return tr_text("三人阵线合力压制队长", "The three-person line overwhelms the captain") if not added_rear.is_empty() else tr_text("两人阵线守住装备优势", "The two-person line keeps its item advantage")
|
||||
@@ -2551,23 +2652,27 @@ func third_battle_event_text() -> String:
|
||||
|
||||
|
||||
func fourth_battle_event_text() -> String:
|
||||
var lead_id := visible_member_id(0)
|
||||
var marked_id := session_state.marked_member_id()
|
||||
var second_id := visible_member_id(1)
|
||||
var third_id := visible_member_id(2)
|
||||
match battle_phase:
|
||||
0:
|
||||
return tr_text("小队进入%s…" % route_name(route_choice), "The Company enters %s…" % route_name(route_choice))
|
||||
1:
|
||||
if route_choice == "R6-01":
|
||||
return tr_text("路线守卫蓄力:正面粉碎即将命中前排", "The Warden charges a front crush")
|
||||
return tr_text("路线守卫蓄力:正面粉碎即将命中%s" % candidate_name(lead_id), "The Warden charges a front crush against %s" % candidate_name(lead_id))
|
||||
if route_choice == "R6-02":
|
||||
return tr_text("晨钟抢到先手" if selected_build_profile() == "tempo" else "敌方速度爆发抢先行动", "Early Bell seizes initiative" if selected_build_profile() == "tempo" else "Enemy tempo bursts first")
|
||||
return tr_text("双辉标记锁定了原后排", "Twinflare marks the original rear")
|
||||
return tr_text("双辉标记锁定了%s" % candidate_name(marked_id), "Twinflare marks %s" % candidate_name(marked_id))
|
||||
2:
|
||||
if route_choice == "R6-03":
|
||||
return tr_text("分裂打击越过前排,命中后排 -35", "The split strike bypasses the front and hits rear -35")
|
||||
return tr_text("钉阵胫甲把粉碎降为 -10" if route_choice == "R6-01" and selected_build_profile() == "guarded" else "路线压力命中前排", "Frontpin reduces the crush to -10" if route_choice == "R6-01" and selected_build_profile() == "guarded" else "Route pressure hits the front")
|
||||
return tr_text("分裂打击越过%s,命中%s -35" % [candidate_name(lead_id), candidate_name(marked_id)], "The split strike bypasses %s and hits %s for -35" % [candidate_name(lead_id), candidate_name(marked_id)])
|
||||
return tr_text("钉阵胫甲把%s受到的粉碎降为 -10" % candidate_name(lead_id) if route_choice == "R6-01" and selected_build_profile() == "guarded" else "路线压力命中%s" % candidate_name(lead_id), "Frontpin reduces %s's crush to -10" % candidate_name(lead_id) if route_choice == "R6-01" and selected_build_profile() == "guarded" else "Route pressure hits %s" % candidate_name(lead_id))
|
||||
3:
|
||||
return tr_text("原弓手从后排反击", "The original Archer counters") if rear_choice == "Farstring" else tr_text("原治疗修复受损阵线", "The original Healer restores the damaged line")
|
||||
return support_action_text(second_id, marked_id if route_choice == "R6-03" else lead_id, "第二位", "second position")
|
||||
4:
|
||||
return tr_text("第三名成员补上第二种后排能力", "The third member adds the missing rear response") if not added_rear.is_empty() else tr_text("保留构筑发动第二次反击", "The retained build follows up")
|
||||
return support_action_text(third_id, marked_id if route_choice == "R6-03" else lead_id, "第三位", "third position") if not third_id.is_empty() else tr_text("保留构筑发动第二次反击", "The retained build follows up")
|
||||
5:
|
||||
return tr_text("小队突破路线守卫的防线", "The Company breaks the route defense")
|
||||
6:
|
||||
@@ -2576,30 +2681,47 @@ func fourth_battle_event_text() -> String:
|
||||
|
||||
|
||||
func fifth_battle_event_text() -> String:
|
||||
var lead_id := visible_member_id(0)
|
||||
var marked_id := session_state.marked_member_id()
|
||||
var supports := support_member_ids()
|
||||
var first_support := supports[0] if not supports.is_empty() else ""
|
||||
var second_support := supports[1] if supports.size() > 1 else ""
|
||||
match battle_phase:
|
||||
0:
|
||||
return tr_text("Lv.2 成员带队进入双压关隘…", "The Lv.2 member leads into Twin-Pressure Pass…")
|
||||
1:
|
||||
return tr_text("历练弓手提前射击,但本场仍只行动一次", "The trained Archer fires early, but still acts only once") if trained_profession() == "Farstring" else tr_text("双压守卫同时锁定前排与原后排", "The Twin Warden marks the front and original rear")
|
||||
return tr_text("历练弓手%s提前射击,但本场仍只行动一次" % candidate_name(trained_profession()), "Trained Archer %s fires early, but still acts only once" % candidate_name(trained_profession())) if trained_profession() == "Farstring" else tr_text("双压守卫同时锁定%s与%s" % [candidate_name(lead_id), candidate_name(marked_id)], "The Twin Warden marks %s and %s" % [candidate_name(lead_id), candidate_name(marked_id)])
|
||||
2:
|
||||
return tr_text("前排历练额外减少 20 伤害", "Front training removes 20 more damage") if trained_member == "front" else tr_text("前排承受第一道正面重压", "The front absorbs the first pressure")
|
||||
return tr_text("%s的历练额外减少 20 伤害" % candidate_name(lead_id), "%s's training removes 20 more damage" % candidate_name(lead_id)) if trained_profession() == lead_id else tr_text("%s承受第一道正面重压" % candidate_name(lead_id), "%s absorbs the first pressure" % candidate_name(lead_id))
|
||||
3:
|
||||
return tr_text("分裂压力越过阵线,原后排受到 -40", "Split pressure bypasses the line; original rear takes -40")
|
||||
return tr_text("分裂压力越过阵线,%s受到 -40" % candidate_name(marked_id), "Split pressure bypasses the line; %s takes -40" % candidate_name(marked_id))
|
||||
4:
|
||||
if trained_member == "rear" and rear_choice == "Farstring":
|
||||
if trained_profession() == first_support and first_support == "Farstring":
|
||||
return tr_text("历练弓手已在第一步行动,本步不重复攻击", "The trained Archer already acted; no duplicate attack")
|
||||
return tr_text("原后排用 +25 强疗恢复", "The original rear restores +25 with trained healing") if rear_choice == "Kindhand" and trained_member == "rear" else (tr_text("原后排恢复 +12", "The original rear restores +12") if rear_choice == "Kindhand" else tr_text("原弓手完成本场唯一射击", "The original Archer takes its one shot"))
|
||||
return support_action_text(first_support, marked_id, "第二位", "second position", trained_profession() == first_support)
|
||||
5:
|
||||
if not added_rear.is_empty():
|
||||
if trained_member == "added" and added_rear == "Farstring":
|
||||
if not second_support.is_empty():
|
||||
if trained_profession() == second_support and second_support == "Farstring":
|
||||
return tr_text("历练弓手已提前行动,不会重复射击", "The trained Archer acted early and does not fire twice")
|
||||
return tr_text("增援治疗用 +25 强疗恢复", "The added Healer restores +25") if added_rear == "Kindhand" and trained_member == "added" else (tr_text("增援治疗恢复 +12", "The added Healer restores +12") if added_rear == "Kindhand" else tr_text("增援弓手完成本场唯一射击", "The added Archer takes its one shot"))
|
||||
return support_action_text(second_support, marked_id, "第三位", "third position", trained_profession() == second_support)
|
||||
return tr_text("两人小队发动保留构筑追击", "The two-person line triggers its retained-build follow-up")
|
||||
6:
|
||||
return tr_text("双压守卫被击败;成长结果已验证", "Twin Warden defeated; growth result verified")
|
||||
return ""
|
||||
|
||||
|
||||
func support_action_text(member_id: String, target_id: String, position_zh: String, position_en: String, trained := false) -> String:
|
||||
match member_id:
|
||||
"Wayrunner":
|
||||
return tr_text("%s%s发动侧翼反击" % [position_zh, candidate_name(member_id)], "%s in %s launches a flank counter" % [candidate_name(member_id), position_en])
|
||||
"Farstring":
|
||||
return tr_text("%s%s完成远程射击" % [position_zh, candidate_name(member_id)], "%s in %s fires a ranged shot" % [candidate_name(member_id), position_en])
|
||||
"Kindhand":
|
||||
var amount := 25 if trained else 12
|
||||
return tr_text("%s%s为%s恢复 +%d" % [position_zh, candidate_name(member_id), candidate_name(target_id), amount], "%s in %s restores +%d to %s" % [candidate_name(member_id), position_en, amount, candidate_name(target_id)])
|
||||
return tr_text("%s%s完成职责行动" % [position_zh, candidate_name(member_id)], "%s in %s completes the role action" % [candidate_name(member_id), position_en])
|
||||
|
||||
|
||||
func selected_build_profile() -> String:
|
||||
if not added_rear.is_empty():
|
||||
return "full_line"
|
||||
@@ -2607,42 +2729,47 @@ func selected_build_profile() -> String:
|
||||
|
||||
|
||||
func member_slots() -> Array[String]:
|
||||
var slots: Array[String] = ["front", "rear"]
|
||||
if not added_rear.is_empty():
|
||||
slots.append("added")
|
||||
var slots: Array[String] = []
|
||||
for index in range(visible_member_ids().size()):
|
||||
slots.append(["front", "rear", "added"][index])
|
||||
return slots
|
||||
|
||||
|
||||
func member_profession(slot: String) -> String:
|
||||
match slot:
|
||||
"front":
|
||||
return front_choice
|
||||
return visible_member_id(0)
|
||||
"rear":
|
||||
return rear_choice
|
||||
return visible_member_id(1)
|
||||
"added":
|
||||
return added_rear
|
||||
return visible_member_id(2)
|
||||
return ""
|
||||
|
||||
|
||||
func member_slot_label(slot: String) -> String:
|
||||
var member_id := member_profession(slot)
|
||||
var origin := tr_text("公会加入", "Guild recruit") if member_id == added_rear and not added_rear.is_empty() else tr_text("初始成员", "Starting member")
|
||||
match slot:
|
||||
"front":
|
||||
return tr_text("前排", "Front")
|
||||
return tr_text("先行位 · %s" % origin, "Lead · %s" % origin)
|
||||
"rear":
|
||||
return tr_text("原后排", "Original rear")
|
||||
return tr_text("第二位 · %s" % origin, "Second · %s" % origin)
|
||||
"added":
|
||||
return tr_text("增援后排", "Added rear")
|
||||
return tr_text("第三位 · %s" % origin, "Third · %s" % origin)
|
||||
return ""
|
||||
|
||||
|
||||
func member_level(slot: String) -> int:
|
||||
if slot not in member_slots():
|
||||
return 0
|
||||
return 2 if training_committed and slot == trained_member else 1
|
||||
var member_id := member_profession(slot)
|
||||
return int(session_state.member_levels.get(member_id, 2 if training_committed and slot == trained_member else 1))
|
||||
|
||||
|
||||
func trained_profession() -> String:
|
||||
return member_profession(trained_member) if training_committed else ""
|
||||
if not training_committed:
|
||||
return ""
|
||||
return session_state.trained_member_id if not session_state.trained_member_id.is_empty() else member_profession(trained_member)
|
||||
|
||||
|
||||
func training_color(slot: String) -> Color:
|
||||
@@ -2842,8 +2969,17 @@ func later_preview_supports_pair() -> bool:
|
||||
return formation_commit_count > 0 or (front_choice in ["Hearthguard", "Wayrunner"] and rear_choice in ["Farstring", "Kindhand"])
|
||||
|
||||
|
||||
func missing_rear_profession() -> String:
|
||||
return "Kindhand" if rear_choice == "Farstring" else "Farstring"
|
||||
func guild_preview_order() -> Array[String]:
|
||||
if guild_choice != "recruit_rear":
|
||||
return visible_member_ids()
|
||||
return session_service.ordered_with_addition(session_service.mapped_guild_recruit())
|
||||
|
||||
|
||||
func guild_recruit_position(recruit_id: String) -> String:
|
||||
var index := session_service.ordered_with_addition(recruit_id).find(recruit_id)
|
||||
var labels_zh := ["先行位", "第二位", "第三位"]
|
||||
var labels_en := ["lead", "second", "third"]
|
||||
return (labels_zh if locale == "zh_CN" else labels_en)[index] if index >= 0 and index < 3 else ""
|
||||
|
||||
|
||||
func guild_choice_guidance() -> String:
|
||||
@@ -2851,7 +2987,11 @@ func guild_choice_guidance() -> String:
|
||||
return tr_text("没有预选。保留已验证的装备,或用更多金币补齐后排。", "Nothing is preselected. Keep the proven item, or spend more to complete the rear line.")
|
||||
if guild_choice == "retain_item":
|
||||
return tr_text("已选择保留装备;剩余 8 金币、小队 2 人。出发前仍可切换。", "Retain selected: 8 Coin, Company 2. You can still switch before departure.")
|
||||
return tr_text("已选择招募%s;剩余 6 金币、小队 3 人。出发前仍可切换。" % candidate_name(missing_rear_profession()), "Recruit %s selected: 6 Coin, Company 3. You can still switch." % candidate_name(missing_rear_profession()))
|
||||
var recruit_id := session_service.mapped_guild_recruit()
|
||||
var order_names: Array[String] = []
|
||||
for member_id in guild_preview_order():
|
||||
order_names.append(candidate_name(member_id))
|
||||
return tr_text("已选择招募%s;她成为%s。新顺序:%s;原两人相对顺序不变。" % [candidate_name(recruit_id), guild_recruit_position(recruit_id), " → ".join(order_names)], "Recruit %s selected as %s. New order: %s; the original pair keeps its relative order." % [candidate_name(recruit_id), guild_recruit_position(recruit_id), " -> ".join(order_names)])
|
||||
|
||||
|
||||
func session_result_explanation() -> String:
|
||||
@@ -2919,6 +3059,25 @@ func actor_at(texture: Texture2D, actor_position: Vector2, actor_size: Vector2,
|
||||
return actor
|
||||
|
||||
|
||||
func render_visible_party(prefix: String, start_position: Vector2, actor_height: float, spacing := -1.0) -> Array[TextureRect]:
|
||||
var actors: Array[TextureRect] = []
|
||||
var resolved_spacing := spacing if spacing >= 0.0 else actor_height * 0.88
|
||||
var node_suffixes := ["Front", "Rear", "AddedRear"]
|
||||
for index in range(visible_member_ids().size()):
|
||||
var member_id := visible_member_id(index)
|
||||
var scale: float = [1.0, 0.90, 0.84][index]
|
||||
var actor_size: float = actor_height * scale
|
||||
var actor := actor_at(
|
||||
animation_texture(member_id, "victory" if index == 0 and member_id == "Hearthguard" else "idle", victory_clock),
|
||||
start_position + Vector2(resolved_spacing * index, actor_height - actor_size),
|
||||
Vector2(actor_size, actor_size),
|
||||
prefix + node_suffixes[index]
|
||||
)
|
||||
actor.set_meta("member_id", member_id)
|
||||
actors.append(actor)
|
||||
return actors
|
||||
|
||||
|
||||
func add_vfx(texture: Texture2D, vfx_position: Vector2, diameter: float) -> TextureRect:
|
||||
var image := TextureRect.new()
|
||||
image.name = "BattleVFX"
|
||||
@@ -2937,8 +3096,8 @@ func render_battle_feedback(actor_width: float, actor_height: float) -> void:
|
||||
return
|
||||
var target: String = active_battle_feedback["target"]
|
||||
var delta: int = int(active_battle_feedback["delta"])
|
||||
var target_position := front_base_position if target == "front" else (rear_base_position if target == "rear" else enemy_base_position)
|
||||
var target_scale := 1.0 if target == "front" else (0.90 if target == "rear" else 0.82)
|
||||
var target_position := front_base_position if target == "front" else (rear_base_position if target == "rear" else (added_rear_base_position if target == "added" else enemy_base_position))
|
||||
var target_scale := 1.0 if target == "front" else (0.90 if target == "rear" else (0.86 if target == "added" else 0.82))
|
||||
var feedback_width := clampf(size.x * 0.20, 170.0, 260.0)
|
||||
battle_feedback_origin = Vector2(
|
||||
clampf(target_position.x + actor_width * target_scale * 0.5 - feedback_width * 0.5, 12.0, size.x - feedback_width - 12.0),
|
||||
|
||||
@@ -194,7 +194,7 @@ func commit_guild_response() -> Dictionary:
|
||||
var cost := 12 if state.guild_response == "retain_loan" else 14
|
||||
if state.coin < cost:
|
||||
return _reject(Contracts.INSUFFICIENT_COIN)
|
||||
var mapped_recruit := _mapped_guild_recruit() if state.guild_response == "add_recruit" else ""
|
||||
var mapped_recruit := mapped_guild_recruit() if state.guild_response == "add_recruit" else ""
|
||||
if state.guild_response == "add_recruit" and (mapped_recruit.is_empty() or mapped_recruit in state.field_order):
|
||||
return _reject(Contracts.INVALID_ID)
|
||||
state.guild_committed = true
|
||||
@@ -427,7 +427,7 @@ func _domain_context(compat_context: Dictionary = {}) -> Dictionary:
|
||||
"trained_slot": compat_context.get("trained_slot", state.trained_member),
|
||||
}
|
||||
|
||||
func _mapped_guild_recruit() -> String:
|
||||
func mapped_guild_recruit() -> String:
|
||||
var key := "%s:%s" % [state.front_choice + ":" + state.rear_choice, state.adaptation_choice]
|
||||
return {
|
||||
"Hearthguard:Wayrunner:EQ-002": "Farstring",
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
extends SceneTree
|
||||
|
||||
const MainScene = preload("res://main.tscn")
|
||||
const VIEWPORT_SIZE := Vector2i(844, 390)
|
||||
|
||||
var failures: Array[String] = []
|
||||
var capture_root := ""
|
||||
var viewport: SubViewport
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("run_test")
|
||||
|
||||
|
||||
func run_test() -> void:
|
||||
capture_root = OS.get_environment("ABG_P9_1_FORMATION_CAPTURE_ROOT")
|
||||
if not capture_root.is_empty():
|
||||
DirAccess.make_dir_recursive_absolute(capture_root)
|
||||
viewport = SubViewport.new()
|
||||
viewport.size = VIEWPORT_SIZE
|
||||
viewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS
|
||||
root.add_child(viewport)
|
||||
var game := MainScene.instantiate()
|
||||
game.auto_boot = false
|
||||
game.audio_enabled = false
|
||||
viewport.add_child(game)
|
||||
await settle(10)
|
||||
game.locale = "zh_CN"
|
||||
game.muted = true
|
||||
game.boot_verified = true
|
||||
game.page = game.AppPage.MAIN_MENU
|
||||
game.render()
|
||||
|
||||
start_pair(game, "Farstring", "Kindhand")
|
||||
resolve_battle(game, 6)
|
||||
expect(game.page == game.AppPage.VICTORY, "exceptional pair wins battle one")
|
||||
game.open_adaptation()
|
||||
game.choose_adaptation("EQ-002")
|
||||
game.begin_second_battle()
|
||||
resolve_battle(game, 6)
|
||||
expect(game.page == game.AppPage.SECOND_RESULT, "exceptional pair reaches the Guild handoff")
|
||||
|
||||
game.open_guild_return()
|
||||
expect(game.facade.session_service.mapped_guild_recruit() == "Hearthguard", "Guild domain maps Hearthguard for Farstring and Kindhand")
|
||||
var recruit_button := find_control(game, "GuildChoice_recruit_rear") as Button
|
||||
expect(recruit_button != null and recruit_button.text.contains(game.candidate_name("Hearthguard")), "unselected Guild option names Hearthguard")
|
||||
expect(recruit_button != null and recruit_button.text.contains("先行位"), "unselected Guild option previews Hearthguard as lead")
|
||||
game.choose_guild_option("recruit_rear")
|
||||
expect(game.facade.guild_preview_order() == ["Hearthguard", "Farstring", "Kindhand"], "selected Guild preview uses post-recruit order")
|
||||
expect(game.facade.guild_choice_guidance().contains("艾连") and game.facade.guild_choice_guidance().contains("艾连 → 妮娅 → 赛布尔"), "Guild guidance names recruit and complete order")
|
||||
expect(member_meta(game, "GuildFront") == "Hearthguard", "Guild preview puts the recruit at lead")
|
||||
expect(member_meta(game, "GuildRear") == "Farstring", "Guild preview moves Farstring to second")
|
||||
expect(member_meta(game, "GuildRecruitPreview") == "Kindhand", "Guild preview keeps Kindhand after Farstring")
|
||||
await capture_state("01-guild-reformation")
|
||||
|
||||
game.begin_third_battle()
|
||||
expect(game.facade.session_state.field_order == ["Hearthguard", "Farstring", "Kindhand"], "Guild commit produces the previewed order")
|
||||
expect(game.added_rear == "Hearthguard", "stable origin records Hearthguard as the Guild recruit")
|
||||
expect(member_meta(game, "FrontActor") == "Hearthguard", "battle three lead actor is Hearthguard")
|
||||
expect(member_meta(game, "RearActor") == "Farstring", "battle three second actor is Farstring")
|
||||
expect(member_meta(game, "AddedRearActor") == "Kindhand", "battle three third actor is Kindhand")
|
||||
expect(control_text(game, "FrontHealth").contains("艾连"), "lead health bar names Hearthguard")
|
||||
game.advance_battle_phase()
|
||||
await capture_state("02-battle-three")
|
||||
|
||||
game.advance_battle_phase()
|
||||
game.commit_phase_impact(2)
|
||||
expect(game.active_battle_feedback.get("target", "") == "front", "Hearthguard damage feedback lands on the current lead")
|
||||
resolve_remaining_battle(game, 2, 7)
|
||||
expect(game.page == game.AppPage.SESSION_RESULT, "re-formed line reaches battle-three result")
|
||||
|
||||
game.open_route_board()
|
||||
expect(member_meta(game, "RouteFront") == "Hearthguard", "route board keeps Hearthguard at lead")
|
||||
expect(member_meta(game, "RouteRear") == "Farstring", "route board keeps Farstring second")
|
||||
expect(member_meta(game, "RouteAddedRear") == "Kindhand", "route board keeps Kindhand third")
|
||||
await capture_state("03-route-board")
|
||||
game.choose_route("R6-03")
|
||||
game.open_route_review()
|
||||
game.commit_route()
|
||||
game.advance_battle_phase()
|
||||
game.advance_battle_phase()
|
||||
game.commit_phase_impact(2)
|
||||
expect(game.facade.session_state.marked_member_id() == "Kindhand", "route pressure retains Kindhand's stable identity")
|
||||
expect(game.active_battle_feedback.get("target", "") == "added", "Kindhand feedback lands on her current third position")
|
||||
resolve_remaining_battle(game, 2, 7)
|
||||
expect(game.page == game.AppPage.SECOND_CYCLE_RESULT, "re-formed line reaches battle-four result")
|
||||
|
||||
game.open_training()
|
||||
expect(training_member(game, "Training_Front") == "Hearthguard", "lead training control maps Hearthguard")
|
||||
expect(training_member(game, "Training_Rear") == "Farstring", "second training control maps Farstring")
|
||||
expect(training_member(game, "Training_Added") == "Kindhand", "third training control maps Kindhand")
|
||||
expect(member_meta(game, "TrainingActor_front") == "Hearthguard", "lead training actor matches its control")
|
||||
expect(member_meta(game, "TrainingActor_rear") == "Farstring", "second training actor matches its control")
|
||||
expect(member_meta(game, "TrainingActor_added") == "Kindhand", "third training actor matches its control")
|
||||
await capture_state("04-training")
|
||||
game.choose_training("front")
|
||||
game.open_training_review()
|
||||
game.commit_training()
|
||||
resolve_battle(game, 7)
|
||||
expect(game.page == game.AppPage.THIRD_CYCLE_RESULT, "re-formed line reaches the final result")
|
||||
expect(member_meta(game, "GrowthResultFront") == "Hearthguard", "final result keeps Hearthguard at lead")
|
||||
expect(member_meta(game, "GrowthResultRear") == "Farstring", "final result keeps Farstring second")
|
||||
expect(member_meta(game, "GrowthResultAddedRear") == "Kindhand", "final result keeps Kindhand third")
|
||||
await capture_state("05-final-result")
|
||||
|
||||
game.clear_experience()
|
||||
game.page = game.AppPage.MAIN_MENU
|
||||
game.render()
|
||||
start_pair(game, "Hearthguard", "Farstring")
|
||||
expect(game.facade.visible_member_ids() == ["Hearthguard", "Farstring"], "standard pair remains a two-person line")
|
||||
expect(member_meta(game, "FrontActor") == "Hearthguard" and member_meta(game, "RearActor") == "Farstring", "standard pair actor mapping is unchanged")
|
||||
expect(find_control(game, "AddedRearActor") == null, "standard pair has no third actor")
|
||||
|
||||
game.queue_free()
|
||||
viewport.queue_free()
|
||||
await settle(90)
|
||||
if failures.is_empty():
|
||||
print("ABG_P9_1_REAL_FORMATION_UI_OK preview=HG,FS,KH battle=HG,FS,KH feedback=lead,third training=HG,FS,KH standard=HG,FS")
|
||||
quit(0)
|
||||
else:
|
||||
for failure in failures:
|
||||
push_error(failure)
|
||||
quit(1)
|
||||
|
||||
|
||||
func start_pair(game: Control, first: String, second: String) -> void:
|
||||
game.start_new_game()
|
||||
game.open_recruit_selection()
|
||||
game.toggle_recruit(first)
|
||||
game.toggle_recruit(second)
|
||||
game.review_automatic_formation()
|
||||
game.commit_formation()
|
||||
|
||||
|
||||
func resolve_battle(game: Control, phase_count: int) -> void:
|
||||
for phase in range(phase_count):
|
||||
game.advance_battle_phase()
|
||||
|
||||
|
||||
func resolve_remaining_battle(game: Control, current_phase: int, phase_count: int) -> void:
|
||||
for phase in range(current_phase, phase_count):
|
||||
game.advance_battle_phase()
|
||||
|
||||
|
||||
func training_member(game: Control, node_name: String) -> String:
|
||||
var control := find_control(game, node_name)
|
||||
return String(control.get_meta("member_id", "")) if control != null else ""
|
||||
|
||||
|
||||
func capture_state(state_name: String) -> void:
|
||||
if capture_root.is_empty():
|
||||
return
|
||||
await settle(7)
|
||||
await RenderingServer.frame_post_draw
|
||||
var image := viewport.get_texture().get_image()
|
||||
var error := image.save_png(capture_root.path_join(state_name + ".png"))
|
||||
if error != OK:
|
||||
failures.append("P9.1 real formation UI: capture %s failed with %s" % [state_name, error])
|
||||
|
||||
|
||||
func member_meta(game: Control, node_name: String) -> String:
|
||||
var control := find_control(game, node_name)
|
||||
return String(control.get_meta("member_id", "")) if control != null else ""
|
||||
|
||||
|
||||
func control_text(game: Control, node_name: String) -> String:
|
||||
var control := find_control(game, node_name)
|
||||
return collect_text(control) if control != null else ""
|
||||
|
||||
|
||||
func collect_text(node: Node) -> String:
|
||||
var parts: Array[String] = []
|
||||
if node is Label or node is Button:
|
||||
parts.append(node.text)
|
||||
for child in node.get_children():
|
||||
parts.append(collect_text(child))
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
func find_control(node: Node, node_name: String) -> Control:
|
||||
if node.name == node_name and node is Control and not node.is_queued_for_deletion():
|
||||
return node
|
||||
for child in node.get_children():
|
||||
var found := find_control(child, node_name)
|
||||
if found != null:
|
||||
return found
|
||||
return null
|
||||
|
||||
|
||||
func settle(frames: int) -> void:
|
||||
for frame in range(frames):
|
||||
await process_frame
|
||||
|
||||
|
||||
func expect(condition: bool, message: String) -> void:
|
||||
if not condition:
|
||||
failures.append("P9.1 real formation UI: %s" % message)
|
||||
Reference in New Issue
Block a user