1288 lines
57 KiB
GDScript
1288 lines
57 KiB
GDScript
class_name P101RuntimeController
|
|
extends "res://p10_0/runtime_controller.gd"
|
|
|
|
const BaseContent = preload("res://p10_0/content_registry.gd")
|
|
const RegionContent = preload("res://p10_1/content_registry.gd")
|
|
const RegionState = preload("res://p10_1/region_run_state.gd")
|
|
const RegionMarketService = preload("res://p10_1/region_market_service.gd")
|
|
const RegionBattleService = preload("res://p10_1/first_crossing_battle_service.gd")
|
|
const RegionResolutionService = preload("res://p10_1/first_crossing_resolution_service.gd")
|
|
const RegionSaveService = preload("res://p10_1/save_v7_service.gd")
|
|
const MotionPresenter = preload("res://p10_1/motion_presenter.gd")
|
|
const DAMAGE_VFX = preload("res://assets/vfx/damage_heavy.png")
|
|
const PROTECT_GAIN_VFX = preload("res://assets/vfx/protect_gain.png")
|
|
const PROTECT_ABSORB_VFX = preload("res://assets/vfx/protect_absorb.png")
|
|
|
|
const REGION_SAVE_PATH := "user://aetherbound_guild_p10_1.json"
|
|
const REGION_TICK_SECONDS := 0.10
|
|
const REGION_SCREEN_MARKET := "siltwake_market"
|
|
const REGION_SCREEN_PLAN := "siltwake_plan"
|
|
const REGION_SCREEN_BATTLE := "siltwake_battle"
|
|
const REGION_SCREEN_RECOVERY := "siltwake_recovery"
|
|
const REGION_SCREEN_MARKET_2 := "siltwake_market_2"
|
|
|
|
var region_mode := false
|
|
var region_market
|
|
var region_save = RegionSaveService.new()
|
|
var region_catalog: Dictionary = RegionContent.snapshot()
|
|
var region_equipment_rows: Dictionary = RegionContent.rows_by_id(region_catalog, "equipment")
|
|
var region_enemy_rows: Dictionary = RegionContent.rows_by_id(region_catalog, "enemies")
|
|
var region_route_rows: Dictionary = RegionContent.rows_by_id(region_catalog, "routes")
|
|
var region_focused_recruit := ""
|
|
var region_focused_equipment := ""
|
|
var region_focused_route := ""
|
|
var region_focused_company := ""
|
|
var region_selected_instance := ""
|
|
var region_plan_id := ""
|
|
var region_battle_snapshot: Dictionary = {}
|
|
var region_battle_tick := 0
|
|
var region_battle_terminal_tick := 0
|
|
var region_battle_accumulator := 0.0
|
|
var region_battle_complete := false
|
|
var region_battle_paused := false
|
|
var region_battle_speed := 1
|
|
var region_event_cursor := 0
|
|
var region_recent_events: Array[Dictionary] = []
|
|
var region_event_counts: Dictionary = {}
|
|
var region_actor_order: Array = []
|
|
var region_display_hp: Dictionary = {}
|
|
var region_safe_index := 0
|
|
var region_save_sequence := 0
|
|
var region_save_commit_hash := ""
|
|
var region_motion = MotionPresenter.new()
|
|
var region_motion_clock := 0.0
|
|
|
|
|
|
func _bootstrap_market() -> void:
|
|
region_mode = false
|
|
if _try_restore_region_entry():
|
|
_prepare_base_content()
|
|
_queue_render()
|
|
return
|
|
super._bootstrap_market()
|
|
|
|
|
|
func _try_restore_region_entry() -> bool:
|
|
var snapshot: Dictionary = {}
|
|
if String(entry_snapshot.get("state_kind", "")) == RegionState.STATE_KIND:
|
|
snapshot = entry_snapshot.duplicate(true)
|
|
elif persistence_enabled:
|
|
var loaded: Dictionary = region_save.load_slot(ProjectSettings.globalize_path(REGION_SAVE_PATH))
|
|
if loaded.ok:
|
|
snapshot = Dictionary(loaded.snapshot).duplicate(true)
|
|
region_save_sequence = int(loaded.sequence)
|
|
region_save_commit_hash = String(loaded.commit_hash)
|
|
if snapshot.is_empty():
|
|
return false
|
|
region_market = RegionMarketService.new(snapshot)
|
|
if not region_market.valid:
|
|
region_market = null
|
|
return false
|
|
region_mode = true
|
|
last_code = "p101_resumed"
|
|
_restore_region_screen()
|
|
_reset_region_focus()
|
|
return true
|
|
|
|
|
|
func _prepare_base_content() -> void:
|
|
content = BaseContent.snapshot()
|
|
profession_rows = BaseContent.rows_by_id(content, "professions")
|
|
trait_rows = BaseContent.rows_by_id(content, "traits")
|
|
equipment_rows = BaseContent.rows_by_id(content, "equipment")
|
|
enemy_rows = BaseContent.rows_by_id(content, "enemies")
|
|
boss_rows = BaseContent.rows_by_id(content, "bosses")
|
|
contract_rows = BaseContent.rows_by_id(content, "contracts")
|
|
battle_active = false
|
|
battle_result_visible = false
|
|
pending_line.clear()
|
|
if region_market != null and not region_market.state.current_run.is_empty():
|
|
pending_line.assign(region_market.state.current_run.deployed_recruit_ids)
|
|
|
|
|
|
func _restore_region_screen() -> void:
|
|
if region_market == null or region_market.state.current_run.is_empty():
|
|
screen = REGION_SCREEN_MARKET
|
|
return
|
|
match String(region_market.state.current_run.phase):
|
|
"prepared":
|
|
screen = REGION_SCREEN_PLAN
|
|
"recovery":
|
|
screen = REGION_SCREEN_RECOVERY
|
|
"market_2":
|
|
screen = REGION_SCREEN_MARKET_2
|
|
_:
|
|
screen = REGION_SCREEN_MARKET
|
|
|
|
|
|
func _process(delta: float) -> void:
|
|
if not region_mode:
|
|
super._process(delta)
|
|
return
|
|
if screen == REGION_SCREEN_BATTLE and not region_battle_paused:
|
|
region_motion_clock += maxf(0.0, delta)
|
|
if not low_power:
|
|
world_clock += maxf(0.0, delta)
|
|
_update_ambient_actors()
|
|
_animate_region_battle_actors()
|
|
if screen != REGION_SCREEN_BATTLE or region_battle_complete or region_battle_paused:
|
|
return
|
|
if region_motion.playback_held(region_motion_clock, reduced_motion):
|
|
return
|
|
region_battle_accumulator += maxf(0.0, delta) * float(region_battle_speed)
|
|
var ticks := int(floor(region_battle_accumulator / REGION_TICK_SECONDS))
|
|
if ticks <= 0:
|
|
return
|
|
region_battle_accumulator -= float(ticks) * REGION_TICK_SECONDS
|
|
advance_region_battle_ticks(ticks)
|
|
|
|
|
|
func _render_current() -> void:
|
|
if not region_mode:
|
|
super._render_current()
|
|
return
|
|
if display_root == null:
|
|
return
|
|
_clear_display()
|
|
line_touch_rects.clear()
|
|
set_meta("visible_world_ratio", 0.86)
|
|
if size.y > size.x:
|
|
_render_rotate_notice()
|
|
else:
|
|
match screen:
|
|
REGION_SCREEN_PLAN:
|
|
_render_region_plan()
|
|
REGION_SCREEN_BATTLE:
|
|
_render_region_battle()
|
|
REGION_SCREEN_RECOVERY:
|
|
_render_region_recovery()
|
|
REGION_SCREEN_MARKET_2:
|
|
_render_region_market_2()
|
|
_:
|
|
_render_region_market()
|
|
_add_settings_button()
|
|
if settings_open:
|
|
_render_settings_band()
|
|
|
|
|
|
func _render_guild_result() -> void:
|
|
super._render_guild_result()
|
|
if not _region_gate_ready():
|
|
return
|
|
var water := ColorRect.new()
|
|
water.name = "P101SiltwakeGateWater"
|
|
water.position = Vector2(size.x * 0.52, size.y * 0.52)
|
|
water.size = Vector2(size.x * 0.43, 5)
|
|
water.color = Color("#63c8c4")
|
|
water.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
display_root.add_child(water)
|
|
var enter := _action_button("P101EnterRegionButton", _tr("踏入淤潮地", "ENTER SILTWAKE"), enter_siltwake)
|
|
enter.position = Vector2(size.x - 238, size.y - 66)
|
|
enter.size = Vector2(218, 52)
|
|
display_root.add_child(enter)
|
|
|
|
|
|
func _region_gate_ready() -> bool:
|
|
return (
|
|
market != null
|
|
and not market.state.guild_profile.is_empty()
|
|
and "RG-02" in market.state.guild_profile.unlocked_region_ids
|
|
and market.state.current_run.is_empty()
|
|
and market.state.run_creation_draft.is_empty()
|
|
)
|
|
|
|
|
|
func enter_siltwake() -> void:
|
|
if region_mode or not _region_gate_ready():
|
|
return
|
|
var migrated := RegionState.new()
|
|
if not migrated.initialize_from_p100(market.state.snapshot()):
|
|
last_code = "p101_migration_rejected"
|
|
_queue_render()
|
|
return
|
|
var service = RegionMarketService.new(migrated.snapshot())
|
|
var result: Dictionary = service.begin_region_run("p101-ui-enter-rg02", service.state.revision, selected_difficulty)
|
|
last_code = String(result.code)
|
|
if not result.ok:
|
|
_queue_render()
|
|
return
|
|
region_market = service
|
|
region_mode = true
|
|
screen = REGION_SCREEN_MARKET
|
|
_prepare_base_content()
|
|
_reset_region_focus()
|
|
_save_region()
|
|
_queue_render()
|
|
|
|
|
|
func tap_region_recruit(offer_id: String) -> void:
|
|
if not _region_market_ready():
|
|
return
|
|
if region_focused_recruit != offer_id:
|
|
region_focused_recruit = offer_id
|
|
region_focused_equipment = ""
|
|
region_focused_route = ""
|
|
last_code = "recruit_inspected"
|
|
_queue_render()
|
|
return
|
|
var offer := _region_offer("recruit", offer_id)
|
|
if offer.is_empty():
|
|
return
|
|
var result: Dictionary = region_market.hire(
|
|
_region_operation("hire", offer_id),
|
|
region_market.state.revision,
|
|
offer_id,
|
|
int(offer.list_price)
|
|
)
|
|
_accept_region_result(result)
|
|
if result.ok:
|
|
region_focused_recruit = ""
|
|
pending_line.assign(region_market.state.current_run.deployed_recruit_ids)
|
|
|
|
|
|
func tap_region_equipment(offer_id: String) -> void:
|
|
if not _region_market_ready():
|
|
return
|
|
if region_focused_equipment != offer_id:
|
|
region_focused_equipment = offer_id
|
|
region_focused_recruit = ""
|
|
region_focused_route = ""
|
|
last_code = "equipment_inspected"
|
|
_queue_render()
|
|
return
|
|
var offer := _region_offer("equipment", offer_id)
|
|
if offer.is_empty():
|
|
return
|
|
var result: Dictionary = region_market.buy_equipment(
|
|
_region_operation("buy", offer_id),
|
|
region_market.state.revision,
|
|
offer_id,
|
|
int(offer.list_price)
|
|
)
|
|
_accept_region_result(result)
|
|
if result.ok:
|
|
region_selected_instance = String(result.receipt.instance_id)
|
|
region_focused_equipment = ""
|
|
|
|
|
|
func tap_region_company(recruit_id: String) -> void:
|
|
if not _region_market_ready() or recruit_id not in region_market.state.current_run.company:
|
|
return
|
|
if not region_selected_instance.is_empty():
|
|
var result: Dictionary = region_market.equip(
|
|
_region_operation("equip", recruit_id),
|
|
region_market.state.revision,
|
|
recruit_id,
|
|
region_selected_instance
|
|
)
|
|
_accept_region_result(result)
|
|
if result.ok:
|
|
region_selected_instance = ""
|
|
region_focused_company = recruit_id
|
|
return
|
|
region_focused_company = recruit_id
|
|
last_code = "company_inspected"
|
|
_queue_render()
|
|
|
|
|
|
func tap_region_route(offer_id: String) -> void:
|
|
if not _region_market_ready():
|
|
return
|
|
region_focused_route = offer_id
|
|
region_focused_recruit = ""
|
|
region_focused_equipment = ""
|
|
last_code = "route_inspected"
|
|
_queue_render()
|
|
|
|
|
|
func commit_region_route() -> void:
|
|
if not _region_market_ready() or region_focused_route.is_empty():
|
|
return
|
|
var result: Dictionary = region_market.commit_route(
|
|
_region_operation("route", region_focused_route),
|
|
region_market.state.revision,
|
|
region_focused_route,
|
|
String(region_market.state.current_run.market_board.board_hash)
|
|
)
|
|
_accept_region_result(result)
|
|
if result.ok:
|
|
screen = REGION_SCREEN_PLAN
|
|
_save_region()
|
|
_queue_render()
|
|
|
|
|
|
func choose_region_plan(plan_id: String) -> void:
|
|
if region_market == null or String(region_market.state.current_run.get("phase", "")) != "prepared":
|
|
return
|
|
var result: Dictionary = RegionBattleService.new(region_market.state.snapshot()).run_to_terminal(plan_id)
|
|
last_code = String(result.code)
|
|
if not result.ok:
|
|
_queue_render()
|
|
return
|
|
region_plan_id = plan_id
|
|
region_battle_snapshot = Dictionary(result.snapshot).duplicate(true)
|
|
region_battle_tick = 0
|
|
region_battle_terminal_tick = int(region_battle_snapshot.tick)
|
|
region_battle_accumulator = 0.0
|
|
region_battle_complete = false
|
|
region_battle_paused = false
|
|
region_battle_speed = 1
|
|
region_event_cursor = 0
|
|
region_recent_events.clear()
|
|
region_event_counts.clear()
|
|
region_actor_order = Array(region_market.state.current_run.deployed_recruit_ids).duplicate()
|
|
region_display_hp.clear()
|
|
for recruit_id in region_actor_order:
|
|
region_display_hp[recruit_id] = int(region_market.state.current_run.recruit_records[recruit_id].current_hp)
|
|
region_safe_index = int(region_market.state.current_run.pending_encounter.tide_disclosure.current_safe_index)
|
|
region_motion_clock = 0.0
|
|
region_motion.reset(region_actor_order, region_safe_index)
|
|
screen = REGION_SCREEN_BATTLE
|
|
_queue_render()
|
|
|
|
|
|
func advance_region_battle_ticks(ticks: int) -> Dictionary:
|
|
if screen != REGION_SCREEN_BATTLE or region_battle_complete or ticks <= 0:
|
|
return {"ok": false, "code": "battle_inactive", "events": []}
|
|
var previous_tick := region_battle_tick
|
|
region_battle_tick = mini(region_battle_terminal_tick, region_battle_tick + ticks)
|
|
var emitted: Array[Dictionary] = []
|
|
var events: Array = region_battle_snapshot.get("event_log", [])
|
|
while region_event_cursor < events.size() and int(events[region_event_cursor].tick) <= region_battle_tick:
|
|
var event: Dictionary = Dictionary(events[region_event_cursor]).duplicate(true)
|
|
region_event_cursor += 1
|
|
emitted.append(event)
|
|
_consume_region_event(event)
|
|
if region_battle_tick >= region_battle_terminal_tick:
|
|
region_battle_complete = true
|
|
last_code = "first_crossing_playback_terminal"
|
|
elif previous_tick != region_battle_tick:
|
|
last_code = "first_crossing_playing"
|
|
_queue_render()
|
|
return {"ok": true, "code": last_code, "events": emitted, "tick": region_battle_tick}
|
|
|
|
|
|
func toggle_region_battle_pause() -> void:
|
|
if screen != REGION_SCREEN_BATTLE or region_battle_complete:
|
|
return
|
|
region_battle_paused = not region_battle_paused
|
|
_queue_render()
|
|
|
|
|
|
func toggle_region_battle_speed() -> void:
|
|
if screen != REGION_SCREEN_BATTLE or region_battle_complete:
|
|
return
|
|
region_battle_speed = 2 if region_battle_speed == 1 else 1
|
|
_queue_render()
|
|
|
|
|
|
func settle_region_battle() -> void:
|
|
if not region_battle_complete or region_battle_snapshot.is_empty():
|
|
return
|
|
var resolver = RegionResolutionService.new(region_market.state.snapshot())
|
|
var result: Dictionary = resolver.settle(
|
|
_region_operation("settle", String(region_battle_snapshot.snapshot_hash).left(8)),
|
|
resolver.state.revision,
|
|
region_battle_snapshot
|
|
)
|
|
last_code = String(result.code)
|
|
if not result.ok:
|
|
_queue_render()
|
|
return
|
|
region_market = RegionMarketService.new(result.snapshot)
|
|
_save_region()
|
|
screen = REGION_SCREEN_MARKET_2 if String(region_market.state.current_run.phase) == "market_2" else REGION_SCREEN_RECOVERY
|
|
_queue_render()
|
|
|
|
|
|
func recover_region_company() -> void:
|
|
if region_market == null or String(region_market.state.current_run.get("phase", "")) != "recovery":
|
|
return
|
|
var report: Dictionary = region_market.state.current_run.last_battle_report
|
|
var cost := int(region_market.state.current_run.recovery_cost)
|
|
var resolver = RegionResolutionService.new(region_market.state.snapshot())
|
|
var result: Dictionary = resolver.recover(
|
|
_region_operation("recover", String(report.get("battle_hash", "")).left(8)),
|
|
resolver.state.revision,
|
|
String(report.get("battle_hash", "")),
|
|
cost
|
|
)
|
|
last_code = String(result.code)
|
|
if not result.ok:
|
|
_queue_render()
|
|
return
|
|
region_market = RegionMarketService.new(result.snapshot)
|
|
pending_line.assign(region_market.state.current_run.deployed_recruit_ids)
|
|
_reset_region_focus()
|
|
_save_region()
|
|
screen = REGION_SCREEN_MARKET
|
|
_queue_render()
|
|
|
|
|
|
func _commit_line_drag(source_id: String, target_id: String) -> void:
|
|
if not region_mode:
|
|
super._commit_line_drag(source_id, target_id)
|
|
return
|
|
if not _region_market_ready() or source_id.is_empty() or target_id.is_empty() or source_id == target_id:
|
|
return
|
|
var order: Array = Array(region_market.state.current_run.deployed_recruit_ids).duplicate()
|
|
if source_id not in order or target_id not in order:
|
|
return
|
|
order.erase(source_id)
|
|
order.insert(order.find(target_id), source_id)
|
|
var result: Dictionary = region_market.commit_order(
|
|
_region_operation("order", "drag"),
|
|
region_market.state.revision,
|
|
order
|
|
)
|
|
_accept_region_result(result)
|
|
if result.ok:
|
|
pending_line.assign(region_market.state.current_run.deployed_recruit_ids)
|
|
|
|
|
|
func reset_progress() -> void:
|
|
if not region_mode:
|
|
super.reset_progress()
|
|
return
|
|
if not reset_confirmation:
|
|
reset_confirmation = true
|
|
last_code = "reset_confirmation_required"
|
|
_queue_render()
|
|
return
|
|
for suffix in ["", ".bak", ".candidate"]:
|
|
var path := ProjectSettings.globalize_path(REGION_SAVE_PATH + suffix)
|
|
if FileAccess.file_exists(path):
|
|
DirAccess.remove_absolute(path)
|
|
region_mode = false
|
|
region_market = null
|
|
entry_snapshot = {}
|
|
region_save_sequence = 0
|
|
region_save_commit_hash = ""
|
|
super.reset_progress()
|
|
|
|
|
|
func _accept_region_result(result: Dictionary) -> void:
|
|
last_code = String(result.get("code", ""))
|
|
if result.ok:
|
|
_save_region()
|
|
_queue_render()
|
|
|
|
|
|
func _save_region() -> bool:
|
|
if not persistence_enabled or region_market == null:
|
|
return true
|
|
var next_sequence := region_save_sequence + 1
|
|
var result: Dictionary = region_save.write_slot(
|
|
ProjectSettings.globalize_path(REGION_SAVE_PATH),
|
|
region_market.state.snapshot(),
|
|
next_sequence,
|
|
region_save_commit_hash
|
|
)
|
|
if not result.ok:
|
|
last_code = "p101_%s" % String(result.code)
|
|
return false
|
|
region_save_sequence = int(result.sequence)
|
|
region_save_commit_hash = String(result.commit_hash)
|
|
return true
|
|
|
|
|
|
func _region_market_ready() -> bool:
|
|
return (
|
|
region_mode
|
|
and region_market != null
|
|
and not region_market.state.current_run.is_empty()
|
|
and String(region_market.state.current_run.phase) == "market"
|
|
)
|
|
|
|
|
|
func _region_operation(kind: String, target: String) -> String:
|
|
operation_index += 1
|
|
var revision := int(region_market.state.revision) if region_market != null else -1
|
|
return "p101-ui-%s-%d-%d-%s" % [kind, revision, operation_index, target]
|
|
|
|
|
|
func _reset_region_focus() -> void:
|
|
region_focused_recruit = ""
|
|
region_focused_equipment = ""
|
|
region_focused_route = ""
|
|
region_focused_company = ""
|
|
region_selected_instance = ""
|
|
|
|
|
|
func _region_offer(kind: String, offer_id: String) -> Dictionary:
|
|
if region_market == null or region_market.state.current_run.is_empty():
|
|
return {}
|
|
var field := String({"recruit": "recruit_offers", "equipment": "equipment_offers", "route": "battle_offers"}.get(kind, ""))
|
|
var id_field := "source_offer_id" if kind == "recruit" else "offer_id"
|
|
for offer in region_market.state.current_run.market_board.get(field, []):
|
|
if String(offer.get(id_field, "")) == offer_id:
|
|
return Dictionary(offer).duplicate(true)
|
|
return {}
|
|
|
|
|
|
func _consume_region_event(event: Dictionary) -> void:
|
|
var kind := String(event.kind)
|
|
region_motion.consume(event, region_motion_clock)
|
|
region_event_counts[kind] = int(region_event_counts.get(kind, 0)) + 1
|
|
region_recent_events.append(event.duplicate(true))
|
|
if region_recent_events.size() > 4:
|
|
region_recent_events.pop_front()
|
|
if kind == "pull_committed":
|
|
region_actor_order = Array(event.order_after).duplicate()
|
|
elif kind == "tide_followed" or kind == "tide_shifted_while_holding":
|
|
region_safe_index = int(event.safe_index)
|
|
elif kind == "authoritative_damage":
|
|
region_display_hp[String(event.target_id)] = int(event.hp_after)
|
|
elif kind == "recruit_fallen":
|
|
region_display_hp[String(event.recruit_id)] = 0
|
|
|
|
|
|
func _render_region_market() -> void:
|
|
_add_siltwake_world()
|
|
var run: Dictionary = region_market.state.current_run
|
|
var tide: Dictionary = run.market_board.tide_disclosure
|
|
_add_header(
|
|
_tr("淤潮地 · 集市 1", "SILTWAKE · MARKET 1"),
|
|
_tr("◆%d 队伍 %d/4 安全位 %d→%d 5.0秒后换潮" % [run.coin, run.company.size(), tide.current_safe_index, tide.next_safe_index], "◆%d COMPANY %d/4 SAFE %d→%d TIDE 5.0s" % [run.coin, run.company.size(), tide.current_safe_index, tide.next_safe_index])
|
|
)
|
|
_render_tide_notches(tide, 0)
|
|
_render_region_recruit_pier()
|
|
_render_region_equipment_bank()
|
|
_render_region_route_posts()
|
|
_render_region_company_line()
|
|
_render_region_context()
|
|
|
|
|
|
func _render_region_recruit_pier() -> void:
|
|
var run: Dictionary = region_market.state.current_run
|
|
var offers: Array = run.market_board.recruit_offers
|
|
var x0 := 20.0
|
|
var actor_size := clampf(size.y * 0.20, 64.0, 82.0)
|
|
var y := 88.0
|
|
var caption := _outlined_label(_tr("渡口候选 · 点选查看,再点一次雇佣", "FERRY RECRUITS · TAP TO INSPECT, AGAIN TO HIRE"), 9, GOLD_LIGHT)
|
|
caption.position = Vector2(x0, 56)
|
|
caption.size = Vector2(size.x * 0.38, 28)
|
|
display_root.add_child(caption)
|
|
for index in range(offers.size()):
|
|
var offer: Dictionary = offers[index]
|
|
var offer_id := String(offer.source_offer_id)
|
|
var available: bool = offer_id in run.available_recruit_offer_ids
|
|
var x := x0 + index * (actor_size + 8.0)
|
|
_add_ground_ring(Vector2(x + actor_size * 0.5, y + actor_size - 3), actor_size * 0.72, GOLD if region_focused_recruit == offer_id else CYAN, "P101RecruitRing_%s" % offer_id)
|
|
var actor := _add_actor(_p100_recruit_rig(offer), Vector2(x, y), actor_size, "P101RecruitActor_%s" % offer_id, true)
|
|
actor.self_modulate = _p100_profession_color(String(offer.profession_id))
|
|
actor.modulate.a = 1.0 if available else 0.28
|
|
if available:
|
|
display_root.add_child(_actor_target("P101RecruitTarget_%s" % offer_id, Vector2(x - 3, y - 4), Vector2(actor_size + 6, actor_size + 38), tap_region_recruit.bind(offer_id)))
|
|
var label := _outlined_label("%s\n%s ◆%d" % [_generated_name(offer).left(12), _short_profession(String(offer.profession_id)), int(offer.list_price)], 7, GOLD_LIGHT if region_focused_recruit == offer_id else WHITE)
|
|
label.position = Vector2(x - 8, y + actor_size - 2)
|
|
label.size = Vector2(actor_size + 16, 42)
|
|
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
display_root.add_child(label)
|
|
|
|
|
|
func _render_region_equipment_bank() -> void:
|
|
var run: Dictionary = region_market.state.current_run
|
|
var offers: Array = run.market_board.equipment_offers
|
|
var x0 := size.x * 0.39
|
|
var width := size.x * 0.34
|
|
var spacing := width / 4.0
|
|
var caption := _outlined_label(_tr("防潮装备 · 购买后点队员装备", "TIDE GEAR · BUY, THEN TAP A RECRUIT"), 9, GOLD_LIGHT)
|
|
caption.position = Vector2(x0, 56)
|
|
caption.size = Vector2(width, 28)
|
|
display_root.add_child(caption)
|
|
for index in range(offers.size()):
|
|
var offer: Dictionary = offers[index]
|
|
var offer_id := String(offer.offer_id)
|
|
var available: bool = offer_id in run.available_equipment_offer_ids
|
|
var x := x0 + index * spacing + 4
|
|
var post := ColorRect.new()
|
|
post.name = "P101EquipmentPost_%s" % offer_id
|
|
post.position = Vector2(x + spacing * 0.45, 90)
|
|
post.size = Vector2(4, 62)
|
|
post.color = _equipment_color(String(offer.catalog_id))
|
|
post.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
display_root.add_child(post)
|
|
var icon := _outlined_label(_equipment_symbol(String(offer.catalog_id)), 23, _equipment_color(String(offer.catalog_id)))
|
|
icon.position = Vector2(x, 82)
|
|
icon.size = Vector2(spacing - 4, 44)
|
|
icon.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
display_root.add_child(icon)
|
|
if available:
|
|
display_root.add_child(_actor_target("P101EquipmentTarget_%s" % offer_id, Vector2(x, 80), Vector2(spacing - 4, 92), tap_region_equipment.bind(offer_id)))
|
|
var label := _outlined_label("%s\n◆%d" % [_region_name(String(offer.catalog_id)).left(9), int(offer.list_price)], 7, GOLD_LIGHT if region_focused_equipment == offer_id else WHITE)
|
|
label.position = Vector2(x - 4, 128)
|
|
label.size = Vector2(spacing + 4, 40)
|
|
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
label.modulate.a = 1.0 if available else 0.35
|
|
display_root.add_child(label)
|
|
|
|
|
|
func _render_region_route_posts() -> void:
|
|
var run: Dictionary = region_market.state.current_run
|
|
var offers: Array = run.market_board.battle_offers
|
|
var x := size.x - 204.0
|
|
var caption := _outlined_label(_tr("三条水路", "THREE CROSSINGS"), 9, GOLD_LIGHT)
|
|
caption.position = Vector2(x, 56)
|
|
caption.size = Vector2(184, 28)
|
|
display_root.add_child(caption)
|
|
for index in range(offers.size()):
|
|
var offer: Dictionary = offers[index]
|
|
var offer_id := String(offer.offer_id)
|
|
var y := 84.0 + index * 58.0
|
|
var selected := region_focused_route == offer_id
|
|
var line := ColorRect.new()
|
|
line.name = "P101RouteLine_%s" % offer_id
|
|
line.position = Vector2(x, y + 40)
|
|
line.size = Vector2(174, 4)
|
|
line.color = GOLD if selected else [JADE, CYAN, CORAL][int(offer.risk)]
|
|
line.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
display_root.add_child(line)
|
|
var label := _outlined_label(_tr("风险%d %s" % [offer.risk, _region_name(String(offer.route_id))], "RISK %d %s" % [offer.risk, _region_name(String(offer.route_id))]), 8, GOLD_LIGHT if selected else WHITE)
|
|
label.position = Vector2(x, y)
|
|
label.size = Vector2(174, 40)
|
|
label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
|
display_root.add_child(label)
|
|
display_root.add_child(_actor_target("P101RouteTarget_%s" % offer_id, Vector2(x - 4, y), Vector2(182, 48), tap_region_route.bind(offer_id)))
|
|
|
|
|
|
func _render_region_company_line() -> void:
|
|
var run: Dictionary = region_market.state.current_run
|
|
var company: Array = run.company
|
|
var actor_size := clampf(size.y * 0.17, 56.0, 70.0)
|
|
var y := size.y - 132.0
|
|
var x0 := size.x * 0.30
|
|
var spacing := minf(actor_size + 12, (size.x * 0.40) / maxf(1.0, float(company.size())))
|
|
var caption := _outlined_label(_tr("队伍战线 · 拖动调整前后顺序", "COMPANY LINE · DRAG TO REORDER"), 9, GOLD_LIGHT)
|
|
caption.position = Vector2(x0, y - 25)
|
|
caption.size = Vector2(size.x * 0.43, 24)
|
|
display_root.add_child(caption)
|
|
for index in range(company.size()):
|
|
var recruit_id := String(run.deployed_recruit_ids[index])
|
|
var record: Dictionary = run.recruit_records[recruit_id]
|
|
var x := x0 + index * spacing
|
|
_add_ground_ring(Vector2(x + actor_size * 0.5, y + actor_size - 3), actor_size * 0.72, GOLD if region_focused_company == recruit_id else JADE, "P101CompanyRing_%s" % recruit_id)
|
|
var actor := _add_actor(_p100_recruit_rig(record), Vector2(x, y), actor_size, "P101CompanyActor_%s" % recruit_id, true)
|
|
actor.self_modulate = _p100_profession_color(String(record.profession_id))
|
|
var target_rect := Rect2(Vector2(x - 3, y - 4), Vector2(actor_size + 6, actor_size + 35))
|
|
line_touch_rects[recruit_id] = target_rect
|
|
display_root.add_child(_actor_target("P101CompanyTarget_%s" % recruit_id, target_rect.position, target_rect.size, tap_region_company.bind(recruit_id)))
|
|
var item_id := String(run.equipped_by_recruit.get(recruit_id, ""))
|
|
var catalog_id := String(run.inventory.get(item_id, {}).get("catalog_id", ""))
|
|
var label := _outlined_label("%dHP\n%s" % [record.current_hp, _region_name(catalog_id).left(8) if catalog_id.begins_with("EQ-04") else _short_profession(String(record.profession_id))], 7, WHITE)
|
|
label.position = Vector2(x - 6, y + actor_size - 2)
|
|
label.size = Vector2(actor_size + 12, 38)
|
|
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
display_root.add_child(label)
|
|
|
|
|
|
func _render_region_context() -> void:
|
|
var band := ColorRect.new()
|
|
band.name = "P101MarketContextBand"
|
|
band.position = Vector2(0, size.y - 62)
|
|
band.size = Vector2(size.x, 62)
|
|
band.color = Color(0.015, 0.035, 0.075, 0.94)
|
|
band.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
display_root.add_child(band)
|
|
var text := _region_context_text()
|
|
var context := _outlined_label(text, 9, WHITE)
|
|
context.position = Vector2(14, size.y - 58)
|
|
context.size = Vector2(size.x - 250, 52)
|
|
context.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
display_root.add_child(context)
|
|
var depart := _action_button("P101PrepareRouteButton", _tr("准备穿越", "PREPARE CROSSING"), commit_region_route)
|
|
depart.position = Vector2(size.x - 224, size.y - 58)
|
|
depart.size = Vector2(208, 48)
|
|
depart.disabled = region_focused_route.is_empty() or region_market.state.current_run.company.size() < 2
|
|
display_root.add_child(depart)
|
|
|
|
|
|
func _region_context_text() -> String:
|
|
if not region_selected_instance.is_empty():
|
|
var item: Dictionary = region_market.state.current_run.inventory.get(region_selected_instance, {})
|
|
return _tr("已购买 %s:现在点一名队员替换其装备。" % _region_name(String(item.get("catalog_id", ""))), "BOUGHT %s: TAP A RECRUIT TO EQUIP IT." % _region_name(String(item.get("catalog_id", ""))))
|
|
if not region_focused_recruit.is_empty():
|
|
var recruit := _region_offer("recruit", region_focused_recruit)
|
|
return _tr("%s · %s · ◆%d。再点一次确认雇佣。" % [_generated_name(recruit), _short_profession(String(recruit.profession_id)), int(recruit.list_price)], "%s · %s · ◆%d. TAP AGAIN TO HIRE." % [_generated_name(recruit), _short_profession(String(recruit.profession_id)), int(recruit.list_price)])
|
|
if not region_focused_equipment.is_empty():
|
|
var equipment := _region_offer("equipment", region_focused_equipment)
|
|
return _tr("%s · ◆%d。再点一次购买;随后点队员装备。" % [_region_name(String(equipment.catalog_id)), int(equipment.list_price)], "%s · ◆%d. TAP AGAIN TO BUY, THEN TAP A RECRUIT." % [_region_name(String(equipment.catalog_id)), int(equipment.list_price)])
|
|
if not region_focused_route.is_empty():
|
|
var route := _region_offer("route", region_focused_route)
|
|
return _route_summary(int(route.risk))
|
|
if region_market.state.current_run.company.size() < 2:
|
|
return _tr("先在左侧雇佣两名队员。潮水将在战斗第50刻移动安全位置。", "HIRE TWO RECRUITS ON THE LEFT. THE SAFE POSITION SHIFTS AT BATTLE TICK 50.")
|
|
return _tr("选择装备与水路;前后顺序会改变拉拽后的受击关系。", "CHOOSE GEAR AND A CROSSING; LINE ORDER CHANGES WHO IS EXPOSED AFTER A PULL.")
|
|
|
|
|
|
func _render_region_plan() -> void:
|
|
_add_siltwake_world()
|
|
var encounter: Dictionary = region_market.state.current_run.pending_encounter
|
|
var tide: Dictionary = encounter.tide_disclosure
|
|
_add_header(_tr("穿越前战术", "CROSSING PLAN"), _tr("%s · 风险%d · 安全位 %d→%d" % [_region_name(String(encounter.route_id)), encounter.risk, tide.current_safe_index, tide.next_safe_index], "%s · RISK %d · SAFE %d→%d" % [_region_name(String(encounter.route_id)), encounter.risk, tide.current_safe_index, tide.next_safe_index]))
|
|
_render_tide_notches(tide, 0)
|
|
_render_plan_company()
|
|
var center_y := size.y * 0.55
|
|
_render_plan_target("hold_line", Vector2(size.x * 0.08, center_y), Vector2(size.x * 0.36, 92), GOLD)
|
|
_render_plan_target("follow_tide", Vector2(size.x * 0.56, center_y), Vector2(size.x * 0.36, 92), CYAN)
|
|
var hint := _outlined_label(_route_summary(int(encounter.risk)), 10, WHITE)
|
|
hint.position = Vector2(size.x * 0.16, 88)
|
|
hint.size = Vector2(size.x * 0.68, 58)
|
|
hint.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
hint.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
display_root.add_child(hint)
|
|
|
|
|
|
func _render_plan_company() -> void:
|
|
var run: Dictionary = region_market.state.current_run
|
|
var actor_size := clampf(size.y * 0.13, 58.0, 96.0)
|
|
var spacing := actor_size + 8.0
|
|
var total := maxf(actor_size, actor_size + spacing * float(run.deployed_recruit_ids.size() - 1))
|
|
var x0 := (size.x - total) * 0.5
|
|
for index in range(run.deployed_recruit_ids.size()):
|
|
var recruit_id := String(run.deployed_recruit_ids[index])
|
|
var record: Dictionary = run.recruit_records[recruit_id]
|
|
var actor := _add_actor(_p100_recruit_rig(record), Vector2(x0 + index * spacing, size.y * 0.31), actor_size, "P101PlanActor_%s" % recruit_id, true, "advance")
|
|
actor.self_modulate = _p100_profession_color(String(record.profession_id))
|
|
|
|
|
|
func _render_plan_target(plan_id: String, at: Vector2, target_size: Vector2, color: Color) -> void:
|
|
var selected_name := _tr("守住战线", "HOLD LINE") if plan_id == "hold_line" else _tr("顺潮移动", "FOLLOW TIDE")
|
|
var consequence := _tr("保持攻击节奏,依靠站位和抗拉装备。", "KEEP TEMPO; RELY ON ORDER AND PULL RESIST.") if plan_id == "hold_line" else _tr("第50刻随安全位移动,牺牲一次行动节奏。", "MOVE AT TICK 50; GIVE UP ONE ACTION CADENCE.")
|
|
var line := ColorRect.new()
|
|
line.name = "P101PlanLine_%s" % plan_id
|
|
line.position = Vector2(at.x, at.y + target_size.y - 5)
|
|
line.size = Vector2(target_size.x, 5)
|
|
line.color = color
|
|
line.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
display_root.add_child(line)
|
|
var label := _outlined_label("%s\n%s" % [selected_name, consequence], 11, WHITE)
|
|
label.position = at
|
|
label.size = target_size
|
|
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
display_root.add_child(label)
|
|
display_root.add_child(_actor_target("P101PlanTarget_%s" % plan_id, at, target_size, choose_region_plan.bind(plan_id)))
|
|
|
|
|
|
func _render_region_battle() -> void:
|
|
_add_siltwake_world()
|
|
var encounter: Dictionary = region_market.state.current_run.pending_encounter
|
|
var tide: Dictionary = encounter.tide_disclosure
|
|
var countdown := maxf(0.0, float(int(tide.shift_tick) - region_battle_tick) * REGION_TICK_SECONDS)
|
|
_add_header(
|
|
_tr("淤潮穿越 · %s" % _region_name(String(encounter.route_id)), "SILTWAKE CROSSING · %s" % _region_name(String(encounter.route_id))),
|
|
_tr("第%d刻 换潮 %.1f秒 安全位 %d" % [region_battle_tick, countdown, region_safe_index], "TICK %d TIDE %.1fs SAFE %d" % [region_battle_tick, countdown, region_safe_index])
|
|
)
|
|
var battle_status := display_root.get_child(display_root.get_child_count() - 1) as Label
|
|
if battle_status != null:
|
|
battle_status.name = "P101BattleStatus"
|
|
battle_status.size.x = maxf(100.0, size.x * 0.32)
|
|
_render_tide_notches(tide, region_battle_tick)
|
|
_render_region_battle_line()
|
|
_render_region_persistent_feedback()
|
|
_render_region_motion_effects()
|
|
_render_region_event_feedback()
|
|
var pause := _secondary_button("P101PauseButton", "▶" if region_battle_paused else "Ⅱ", toggle_region_battle_pause)
|
|
pause.position = Vector2(size.x - 166, 1)
|
|
pause.size = Vector2(48, 48)
|
|
display_root.add_child(pause)
|
|
var speed := _secondary_button("P101SpeedButton", "%dx" % region_battle_speed, toggle_region_battle_speed)
|
|
speed.position = Vector2(size.x - 112, 1)
|
|
speed.size = Vector2(48, 48)
|
|
display_root.add_child(speed)
|
|
if region_battle_complete:
|
|
_render_region_battle_result()
|
|
|
|
|
|
func _render_region_battle_line() -> void:
|
|
var run: Dictionary = region_market.state.current_run
|
|
var actor_size := clampf(size.y * 0.22, 70.0, 88.0)
|
|
var ally_y := size.y * 0.48
|
|
var lane := ColorRect.new()
|
|
lane.name = "P101BattleLane"
|
|
lane.position = Vector2(0, ally_y - 18)
|
|
lane.size = Vector2(size.x, actor_size + 42)
|
|
lane.color = Color(0.015, 0.04, 0.055, 0.22)
|
|
lane.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
display_root.add_child(lane)
|
|
var ally_spacing := minf(actor_size + 8.0, size.x * 0.30 / maxf(1.0, float(region_actor_order.size() - 1)))
|
|
for index in range(region_actor_order.size()):
|
|
var recruit_id := String(region_actor_order[index])
|
|
var record: Dictionary = run.recruit_records.get(recruit_id, {})
|
|
if record.is_empty():
|
|
continue
|
|
var base := Vector2(size.x * 0.48 - actor_size - index * ally_spacing, ally_y)
|
|
var rig := _p100_recruit_rig(record)
|
|
var visual: Dictionary = region_motion.actor_visual(recruit_id, index, -ally_spacing, region_motion_clock, reduced_motion, low_power)
|
|
_add_ground_ring(base + Vector2(actor_size * 0.5, actor_size - 3), actor_size * 0.62, JADE, "P101BattleGround_%s" % recruit_id)
|
|
var actor := _add_actor(rig, base, actor_size, "P101BattleActor_%s" % recruit_id, false, _motion_actor_animation(rig, String(visual.state)))
|
|
actor.self_modulate = _p100_profession_color(String(record.profession_id))
|
|
actor.set_meta("base_position", base)
|
|
actor.set_meta("region_actor_id", recruit_id)
|
|
actor.set_meta("slot_index", index)
|
|
actor.set_meta("slot_step", -ally_spacing)
|
|
battle_actors[recruit_id] = actor
|
|
_apply_region_actor_visual(actor, visual, rig)
|
|
_add_region_hp_bar(recruit_id, actor, int(region_display_hp.get(recruit_id, record.current_hp)), int(record.max_hp), false)
|
|
var all_enemies: Array = run.pending_encounter.enemy_ids
|
|
var enemies: Array = region_motion.visible_enemy_ids.duplicate()
|
|
var enemy_spacing := minf(actor_size + 8.0, size.x * 0.30 / maxf(1.0, float(all_enemies.size() - 1)))
|
|
for index in range(enemies.size()):
|
|
var enemy_id := String(enemies[index])
|
|
var base := Vector2(size.x * 0.52 + index * enemy_spacing, ally_y - 8)
|
|
var visual: Dictionary = region_motion.enemy_visual(enemy_id, region_motion_clock, reduced_motion, low_power)
|
|
_add_ground_ring(base + Vector2((actor_size + 6) * 0.5, actor_size + 3), actor_size * 0.66, CORAL, "P101BattleEnemyGround_%s" % enemy_id)
|
|
var enemy := _add_actor("Riftblade", base, actor_size + 6, "P101BattleEnemy_%s" % enemy_id, false, String(visual.state))
|
|
enemy.self_modulate = _region_enemy_color(enemy_id)
|
|
enemy.set_meta("base_position", base)
|
|
enemy.set_meta("region_actor_id", enemy_id)
|
|
enemy.set_meta("enemy_actor", true)
|
|
battle_actors[enemy_id] = enemy
|
|
_apply_region_actor_visual(enemy, visual, "Riftblade")
|
|
var name := _outlined_label(_region_name(enemy_id).left(12), 7, CORAL)
|
|
name.name = "P101BattleEnemyName_%s" % enemy_id
|
|
name.position = Vector2(base.x - 12, base.y + actor_size - 2)
|
|
name.size = Vector2(actor_size + 30, 28)
|
|
name.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
name.set_meta("actor_id", enemy_id)
|
|
display_root.add_child(name)
|
|
|
|
|
|
func _render_region_persistent_feedback() -> void:
|
|
if not region_motion.healing_denied_target.is_empty():
|
|
var tether := ColorRect.new()
|
|
tether.name = "P101HealingDenialTether"
|
|
tether.color = Color(0.70, 0.35, 0.88, 0.78)
|
|
tether.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
display_root.add_child(tether)
|
|
var seal := _motion_sprite("P101HealingDenialSeal", PROTECT_ABSORB_VFX, Vector2(32, 32), VIOLET)
|
|
seal.set_meta("target_id", region_motion.healing_denied_target)
|
|
display_root.add_child(seal)
|
|
if region_motion.mud_index >= 0:
|
|
var mud := _motion_sprite("P101IndexedMudMarker", PROTECT_ABSORB_VFX, Vector2(48, 22), Color("#7b8455"))
|
|
mud.set_meta("position_index", region_motion.mud_index)
|
|
display_root.add_child(mud)
|
|
|
|
|
|
func _render_region_motion_effects() -> void:
|
|
var effects: Array[Dictionary] = region_motion.active_effects(region_motion_clock)
|
|
for index in range(effects.size()):
|
|
var effect: Dictionary = effects[index]
|
|
var kind := String(effect.kind)
|
|
var suffix := "%02d_%s" % [index, kind]
|
|
if kind == "allied_basic":
|
|
var source_id := String(effect.get("actor_id", ""))
|
|
var rig := _recruit_rig(source_id)
|
|
var texture := PROTECT_GAIN_VFX if rig in ["Farstring", "Kindhand"] else DAMAGE_VFX
|
|
var projectile := _motion_sprite("P101MotionProjectile_%s" % suffix, texture, Vector2(26, 18), CYAN if rig == "Farstring" else GOLD_LIGHT)
|
|
projectile.set_meta("effect", effect)
|
|
display_root.add_child(projectile)
|
|
elif kind in ["enemy_basic_damage", "authoritative_damage"]:
|
|
var value := _outlined_label("-%d" % int(effect.get("amount", effect.get("damage", 0))), 21, CORAL)
|
|
value.name = "P101MotionDamage_%s" % suffix
|
|
value.size = Vector2(80, 38)
|
|
value.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
value.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
value.z_index = 26
|
|
value.set_meta("effect", effect)
|
|
display_root.add_child(value)
|
|
var impact := _motion_sprite("P101MotionImpact_%s" % suffix, DAMAGE_VFX, Vector2(30, 30), CORAL)
|
|
impact.set_meta("effect", effect)
|
|
display_root.add_child(impact)
|
|
elif kind in ["healing_applied", "healing_converted_to_enemy_guard"]:
|
|
var healing := _motion_sprite("P101MotionHealing_%s" % suffix, PROTECT_GAIN_VFX if kind == "healing_applied" else PROTECT_ABSORB_VFX, Vector2(40, 40), JADE if kind == "healing_applied" else VIOLET)
|
|
healing.set_meta("effect", effect)
|
|
display_root.add_child(healing)
|
|
|
|
|
|
func _motion_sprite(node_name: String, texture: Texture2D, display_size: Vector2, tint: Color) -> Sprite2D:
|
|
var node := Sprite2D.new()
|
|
node.name = node_name
|
|
node.texture = texture
|
|
var source_size := texture.get_size()
|
|
node.scale = Vector2(display_size.x / maxf(1.0, source_size.x), display_size.y / maxf(1.0, source_size.y))
|
|
node.set_meta("base_scale", node.scale)
|
|
node.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
|
|
node.self_modulate = tint
|
|
node.z_index = 24
|
|
return node
|
|
|
|
|
|
func _add_region_hp_bar(actor_id: String, actor: TextureRect, hp: int, max_hp: int, enemy: bool) -> void:
|
|
var bar := ColorRect.new()
|
|
bar.name = "P101Hp_%s" % actor_id
|
|
bar.position = Vector2(actor.position.x + 4, actor.position.y - 8)
|
|
bar.size = Vector2(actor.size.x - 8, 5)
|
|
bar.color = Color(0.02, 0.03, 0.05, 0.88)
|
|
bar.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
display_root.add_child(bar)
|
|
var fill := ColorRect.new()
|
|
fill.name = "Fill"
|
|
fill.position = Vector2(1, 1)
|
|
fill.size = Vector2((bar.size.x - 2) * clampf(float(hp) / maxf(1.0, float(max_hp)), 0.0, 1.0), 3)
|
|
fill.color = CORAL if enemy else JADE
|
|
fill.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
bar.add_child(fill)
|
|
bar.set_meta("actor_id", actor_id)
|
|
battle_hp_bars[actor_id] = bar
|
|
|
|
|
|
func _render_region_event_feedback() -> void:
|
|
var latest: Dictionary = region_recent_events[-1] if not region_recent_events.is_empty() else {}
|
|
var kind := String(latest.get("kind", "battle_started"))
|
|
var feedback := _outlined_label(_region_event_text(latest), 17, _region_event_color(kind))
|
|
feedback.name = "P101BattleFeedback"
|
|
feedback.position = Vector2(size.x * 0.29, 92)
|
|
feedback.size = Vector2(size.x * 0.42, 62)
|
|
feedback.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
feedback.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
display_root.add_child(feedback)
|
|
|
|
|
|
func _render_region_battle_result() -> void:
|
|
var victory := String(region_battle_snapshot.outcome) == "victory"
|
|
var band := ColorRect.new()
|
|
band.name = "P101BattleResultBand"
|
|
band.position = Vector2(0, size.y - 70)
|
|
band.size = Vector2(size.x, 70)
|
|
band.color = Color(0.015, 0.035, 0.075, 0.96)
|
|
band.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
display_root.add_child(band)
|
|
var result := _outlined_label(
|
|
_tr("穿越成功 · ◆+%d" % int(region_battle_snapshot.reward_coin), "CROSSING WON · ◆+%d" % int(region_battle_snapshot.reward_coin)) if victory else _tr("穿越失败 · 永久阵亡 %d" % region_battle_snapshot.fallen_recruit_ids.size(), "CROSSING LOST · PERMANENT FALLEN %d" % region_battle_snapshot.fallen_recruit_ids.size()),
|
|
19,
|
|
GOLD_LIGHT if victory else CORAL
|
|
)
|
|
result.position = Vector2(16, size.y - 66)
|
|
result.size = Vector2(size.x - 250, 56)
|
|
display_root.add_child(result)
|
|
var settle := _action_button("P101SettleButton", _tr("确认结果", "SETTLE RESULT"), settle_region_battle)
|
|
settle.position = Vector2(size.x - 222, size.y - 61)
|
|
settle.size = Vector2(206, 50)
|
|
display_root.add_child(settle)
|
|
|
|
|
|
func _render_region_recovery() -> void:
|
|
_add_siltwake_world()
|
|
var run: Dictionary = region_market.state.current_run
|
|
var report: Dictionary = run.last_battle_report
|
|
_add_header(_tr("幸存者回到渡口", "SURVIVORS RETURN TO THE FERRY"), _tr("◆%d 救援费用 ◆%d" % [run.coin, run.recovery_cost], "◆%d RECOVERY ◆%d" % [run.coin, run.recovery_cost]))
|
|
var title := _outlined_label(_tr("阵亡不会复活", "THE FALLEN DO NOT RETURN"), 26, CORAL)
|
|
title.position = Vector2(size.x * 0.12, 74)
|
|
title.size = Vector2(size.x * 0.76, 52)
|
|
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
display_root.add_child(title)
|
|
var body := _outlined_label(_tr("错误应对让队伍在换潮时失去治疗窗口。支付救援费可让幸存者恢复至40%%,回到同一集市重新整备。", "THE RESPONSE LOST THE HEALING WINDOW AT THE TIDE SHIFT. PAY TO RESTORE SURVIVORS TO 40%% AND REPREPARE THE SAME MARKET."), 12, WHITE)
|
|
body.position = Vector2(size.x * 0.18, 126)
|
|
body.size = Vector2(size.x * 0.64, 76)
|
|
body.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
body.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
display_root.add_child(body)
|
|
_render_survivor_row(Vector2(size.x * 0.30, size.y * 0.54), 70)
|
|
var fallen_count: int = report.get("fallen_recruit_ids", []).size()
|
|
var fallen := _outlined_label(_tr("永久阵亡:%d人" % fallen_count, "PERMANENT FALLEN: %d" % fallen_count), 10, CORAL)
|
|
fallen.position = Vector2(18, size.y - 60)
|
|
fallen.size = Vector2(size.x - 260, 48)
|
|
display_root.add_child(fallen)
|
|
var recover := _action_button("P101RecoverButton", _tr("支付 ◆%d 救援" % run.recovery_cost, "PAY ◆%d TO RECOVER" % run.recovery_cost), recover_region_company)
|
|
recover.position = Vector2(size.x - 232, size.y - 62)
|
|
recover.size = Vector2(216, 52)
|
|
recover.disabled = int(run.coin) < int(run.recovery_cost)
|
|
display_root.add_child(recover)
|
|
|
|
|
|
func _render_region_market_2() -> void:
|
|
_add_siltwake_world()
|
|
var run: Dictionary = region_market.state.current_run
|
|
var handoff: Dictionary = run.market_2_handoff
|
|
_add_header(_tr("淤潮地 · 集市 2", "SILTWAKE · MARKET 2"), _tr("◆%d 已保存" % run.coin, "◆%d SAVED" % run.coin))
|
|
_render_survivor_row(Vector2(size.x * 0.12, size.y * 0.43), 78)
|
|
var title := _outlined_label(_tr("第一道水路已穿越", "FIRST CROSSING COMPLETE"), 27, GOLD_LIGHT)
|
|
title.position = Vector2(size.x * 0.47, size.y * 0.24)
|
|
title.size = Vector2(size.x * 0.46, 62)
|
|
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
display_root.add_child(title)
|
|
var body := _outlined_label(_tr("奖励 ◆%d 已结算一次。队伍、生命、装备与前后顺序已写入 Save v7;后续集市尚未开放。" % int(handoff.get("reward_coin", 0)), "REWARD ◆%d SETTLED ONCE. COMPANY, VITALITY, GEAR AND ORDER ARE SAVED IN V7; LATER MARKETS ARE NOT OPEN YET." % int(handoff.get("reward_coin", 0))), 12, WHITE)
|
|
body.position = Vector2(size.x * 0.49, size.y * 0.40)
|
|
body.size = Vector2(size.x * 0.42, 100)
|
|
body.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
body.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
display_root.add_child(body)
|
|
var seal := _outlined_label(_tr("MARKET 2 · 存档完成", "MARKET 2 · SAVE COMPLETE"), 14, JADE)
|
|
seal.position = Vector2(size.x * 0.55, size.y - 76)
|
|
seal.size = Vector2(size.x * 0.34, 48)
|
|
seal.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
display_root.add_child(seal)
|
|
|
|
|
|
func _render_survivor_row(origin: Vector2, actor_size: float) -> void:
|
|
var run: Dictionary = region_market.state.current_run
|
|
for index in range(run.company.size()):
|
|
var recruit_id := String(run.company[index])
|
|
var record: Dictionary = run.recruit_records[recruit_id]
|
|
var actor := _add_actor(_p100_recruit_rig(record), origin + Vector2(index * (actor_size + 10), 0), actor_size, "P101Survivor_%s" % recruit_id, true, "victory" if screen == REGION_SCREEN_MARKET_2 else "idle")
|
|
actor.self_modulate = _p100_profession_color(String(record.profession_id))
|
|
|
|
|
|
func _add_siltwake_world() -> void:
|
|
_add_background(WINDGLASS_BACKGROUND)
|
|
var wash := ColorRect.new()
|
|
wash.name = "P101WaterLayer"
|
|
wash.position = Vector2(0, 50)
|
|
wash.size = Vector2(size.x, size.y - 50)
|
|
wash.color = Color(0.03, 0.22, 0.24, 0.36)
|
|
wash.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
display_root.add_child(wash)
|
|
for index in range(4):
|
|
var channel := ColorRect.new()
|
|
channel.name = "P101Channel_%d" % (index + 1)
|
|
channel.position = Vector2(size.x * (0.18 + 0.17 * index), 52)
|
|
channel.size = Vector2(3, size.y - 116)
|
|
channel.color = Color(0.55, 0.92, 0.88, 0.24)
|
|
channel.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
display_root.add_child(channel)
|
|
for index in range(7):
|
|
var reed := ColorRect.new()
|
|
reed.name = "P101ReedLayer_%d" % index
|
|
reed.position = Vector2(8 + index * (size.x - 26) / 6.0, size.y - 106 - (index % 3) * 10)
|
|
reed.size = Vector2(3, 42 + (index % 3) * 9)
|
|
reed.color = Color("#99c66b")
|
|
reed.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
display_root.add_child(reed)
|
|
var bank := ColorRect.new()
|
|
bank.name = "P101ForegroundBank"
|
|
bank.position = Vector2(0, size.y - 68)
|
|
bank.size = Vector2(size.x, 68)
|
|
bank.color = Color(0.08, 0.13, 0.10, 0.48)
|
|
bank.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
display_root.add_child(bank)
|
|
|
|
|
|
func _render_tide_notches(tide: Dictionary, tick: int) -> void:
|
|
var current := int(tide.current_safe_index) if tick < int(tide.shift_tick) else int(tide.next_safe_index)
|
|
var next := int(tide.next_safe_index)
|
|
var x0 := size.x * 0.40
|
|
var spacing := minf(54.0, size.x * 0.25 / 4.0)
|
|
for index in range(1, 5):
|
|
var notch := ColorRect.new()
|
|
notch.name = "P101TideNotch_%d" % index
|
|
notch.position = Vector2(x0 + (index - 1) * spacing, 55)
|
|
notch.size = Vector2(spacing - 8, 5)
|
|
notch.color = GOLD if index == current else CYAN if index == next else Color(0.7, 0.8, 0.8, 0.30)
|
|
notch.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
display_root.add_child(notch)
|
|
|
|
|
|
func _animate_region_battle_actors() -> void:
|
|
if screen != REGION_SCREEN_BATTLE:
|
|
return
|
|
for actor_id in battle_actors:
|
|
var actor: TextureRect = battle_actors[actor_id]
|
|
if not is_instance_valid(actor) or not actor.has_meta("base_position"):
|
|
continue
|
|
var rig := String(actor.get_meta("rig", "Hearthguard"))
|
|
var visual: Dictionary
|
|
if bool(actor.get_meta("enemy_actor", false)):
|
|
visual = region_motion.enemy_visual(String(actor_id), region_motion_clock, reduced_motion, low_power)
|
|
else:
|
|
visual = region_motion.actor_visual(
|
|
String(actor_id),
|
|
int(actor.get_meta("slot_index", 0)),
|
|
float(actor.get_meta("slot_step", -68.0)),
|
|
region_motion_clock,
|
|
reduced_motion,
|
|
low_power
|
|
)
|
|
_apply_region_actor_visual(actor, visual, rig)
|
|
if battle_hp_bars.has(actor_id):
|
|
var bar: ColorRect = battle_hp_bars[actor_id]
|
|
bar.position = Vector2(actor.position.x + 4, actor.position.y - 8)
|
|
bar.modulate.a = float(visual.alpha)
|
|
var enemy_name := display_root.find_child("P101BattleEnemyName_%s" % String(actor_id), false, false) as Label
|
|
if enemy_name != null:
|
|
enemy_name.position = Vector2(actor.position.x - 12, actor.position.y + actor.size.y - 2)
|
|
enemy_name.modulate.a = float(visual.alpha)
|
|
_animate_region_persistent_feedback()
|
|
_animate_region_motion_effects()
|
|
|
|
|
|
func _apply_region_actor_visual(actor: TextureRect, visual: Dictionary, rig: String) -> void:
|
|
var camera: Vector2 = region_motion.camera_offset(region_motion_clock, reduced_motion, low_power)
|
|
actor.position = Vector2(actor.get_meta("base_position")) + Vector2(visual.offset) + camera
|
|
actor.pivot_offset = actor.size * 0.5
|
|
actor.scale = Vector2(visual.scale)
|
|
actor.rotation = float(visual.rotation)
|
|
actor.modulate.a = float(visual.alpha)
|
|
var semantic_state := String(visual.state)
|
|
var animation_state := semantic_state if rig == "Riftblade" else _motion_actor_animation(rig, semantic_state)
|
|
actor.set_meta("animation_state", animation_state)
|
|
actor.set_meta("motion_state", semantic_state)
|
|
actor.texture = animation.texture(rig, animation_state, float(visual.animation_time))
|
|
|
|
|
|
func _motion_actor_animation(rig: String, semantic_state: String) -> String:
|
|
match semantic_state:
|
|
"attack":
|
|
return {"Hearthguard": "basic_attack", "Wayrunner": "evade_counter", "Farstring": "draw_release", "Kindhand": "lantern_heal"}.get(rig, "idle")
|
|
"heal", "healed":
|
|
return "lantern_heal" if rig == "Kindhand" else "guard" if rig == "Hearthguard" else "idle"
|
|
"hit", "guard":
|
|
return "guard" if rig == "Hearthguard" else "evade_counter" if rig == "Wayrunner" else "idle"
|
|
"victory":
|
|
return {"Hearthguard": "victory", "Wayrunner": "evade_counter", "Farstring": "draw_release", "Kindhand": "lantern_heal"}.get(rig, "idle")
|
|
"fallen":
|
|
return "idle"
|
|
_:
|
|
return semantic_state
|
|
|
|
|
|
func _animate_region_persistent_feedback() -> void:
|
|
var tether := display_root.find_child("P101HealingDenialTether", false, false) as ColorRect
|
|
if tether != null:
|
|
_set_motion_line(tether, _region_actor_center(region_motion.healing_denial_source), _region_actor_center(region_motion.healing_denied_target), 3.0)
|
|
var pulse := 0.72 if low_power else 0.58 + sin(region_motion_clock * 8.0) * 0.18
|
|
tether.modulate.a = pulse
|
|
var seal := display_root.find_child("P101HealingDenialSeal", false, false) as Sprite2D
|
|
if seal != null:
|
|
seal.position = _region_actor_center(String(seal.get_meta("target_id", "")))
|
|
seal.modulate.a = 0.78 if low_power else 0.68 + sin(region_motion_clock * 7.0) * 0.22
|
|
var mud := display_root.find_child("P101IndexedMudMarker", false, false) as Sprite2D
|
|
if mud != null:
|
|
var index := int(mud.get_meta("position_index", 1))
|
|
mud.position = Vector2(size.x * 0.48 - float(index - 1) * 34.0, size.y * 0.67)
|
|
mud.modulate.a = 0.72
|
|
|
|
|
|
func _animate_region_motion_effects() -> void:
|
|
for child in display_root.get_children():
|
|
if not child.has_meta("effect"):
|
|
continue
|
|
var canvas := child as CanvasItem
|
|
if canvas == null:
|
|
continue
|
|
var effect: Dictionary = child.get_meta("effect")
|
|
var duration := maxf(0.01, float(effect.get("duration", 0.78)))
|
|
var progress := clampf((region_motion_clock - float(effect.get("started", region_motion_clock))) / duration, 0.0, 1.0)
|
|
canvas.visible = progress < 1.0
|
|
if not canvas.visible:
|
|
continue
|
|
if child.name.begins_with("P101MotionProjectile"):
|
|
var projectile := child as Sprite2D
|
|
var source := _region_actor_center(String(effect.get("actor_id", "")))
|
|
var target := _region_actor_center(region_motion.front_visible_enemy())
|
|
var flight := clampf(progress / 0.58, 0.0, 1.0)
|
|
projectile.position = source.lerp(target, flight)
|
|
projectile.position.y -= sin(flight * PI) * (3.0 if reduced_motion else 16.0)
|
|
projectile.rotation = (target - source).angle()
|
|
projectile.modulate.a = 1.0 - maxf(0.0, (flight - 0.78) / 0.22)
|
|
elif child.name.begins_with("P101MotionDamage"):
|
|
var damage_label := child as Label
|
|
var target_id := String(effect.get("target_id", ""))
|
|
var center := _region_actor_center(target_id)
|
|
damage_label.position = center + Vector2(-damage_label.size.x * 0.5, -38.0 - progress * (9.0 if reduced_motion else 28.0))
|
|
damage_label.modulate.a = 1.0 - progress
|
|
elif child.name.begins_with("P101MotionImpact"):
|
|
var impact := child as Sprite2D
|
|
var impact_center := _region_actor_center(String(effect.get("target_id", "")))
|
|
impact.position = impact_center
|
|
var pulse := sin(clampf(progress / 0.48, 0.0, 1.0) * PI)
|
|
impact.scale = Vector2(impact.get_meta("base_scale")) * (0.65 + pulse * (0.20 if reduced_motion else 0.70))
|
|
impact.modulate.a = 1.0 - progress
|
|
elif child.name.begins_with("P101MotionHealing"):
|
|
var healing := child as Sprite2D
|
|
var healing_center := _region_actor_center(String(effect.get("target_id", "")))
|
|
healing.position = healing_center + Vector2(0, -progress * (5.0 if reduced_motion else 18.0))
|
|
healing.modulate.a = 1.0 - progress
|
|
|
|
|
|
func _region_actor_center(actor_id: String) -> Vector2:
|
|
if actor_id.is_empty() or not battle_actors.has(actor_id):
|
|
return Vector2(size.x * 0.5, size.y * 0.53)
|
|
var actor: TextureRect = battle_actors[actor_id]
|
|
return actor.position + actor.size * 0.5
|
|
|
|
|
|
func _set_motion_line(line: ColorRect, start: Vector2, finish: Vector2, width: float) -> void:
|
|
var delta := finish - start
|
|
line.position = start
|
|
line.size = Vector2(maxf(1.0, delta.length()), width)
|
|
line.pivot_offset = Vector2(0, width * 0.5)
|
|
line.rotation = delta.angle()
|
|
|
|
|
|
func _region_actor_state(recruit_id: String) -> String:
|
|
if int(region_display_hp.get(recruit_id, 1)) <= 0:
|
|
return "idle"
|
|
if region_recent_events.is_empty():
|
|
return "advance"
|
|
var event: Dictionary = region_recent_events[-1]
|
|
if String(event.get("actor_id", event.get("source_id", ""))) == recruit_id:
|
|
return "draw_release" if _recruit_rig(recruit_id) == "Farstring" else "lantern_heal" if _recruit_rig(recruit_id) == "Kindhand" else "basic_attack"
|
|
return "advance"
|
|
|
|
|
|
func _region_enemy_state(enemy_id: String) -> String:
|
|
if region_recent_events.is_empty():
|
|
return "advance"
|
|
var event: Dictionary = region_recent_events[-1]
|
|
return "attack" if String(event.get("enemy_id", "")) == enemy_id or String(event.kind) in ["enemy_basic_damage", "pull_committed", "healing_denial_attached"] else "advance"
|
|
|
|
|
|
func _region_event_text(event: Dictionary) -> String:
|
|
var kind := String(event.get("kind", "battle_started"))
|
|
return {
|
|
"battle_started": _tr("队伍踏入淤潮水路", "THE COMPANY ENTERS SILTWAKE"),
|
|
"wave_entered": _tr("敌群从芦苇间出现", "A WAVE EMERGES FROM THE REEDS"),
|
|
"healing_denial_attached": _tr("泥灯水蛭封锁治疗", "MUDLAMP LEECH BLOCKS HEALING"),
|
|
"healing_converted_to_enemy_guard": _tr("治疗被转化为敌方护盾", "HEALING BECOMES ENEMY GUARD"),
|
|
"indexed_bank_marked": _tr("堤岸位置被标记", "BANK POSITION MARKED"),
|
|
"pull_committed": _tr("芦钩拉乱了战线", "REED HOOK PULLS THE LINE"),
|
|
"pull_resisted": _tr("队伍抵抗了拉拽", "THE COMPANY RESISTS THE PULL"),
|
|
"indexed_mud_created": _tr("跨位留下减速淤泥", "CROSSING LEAVES HINDERING MUD"),
|
|
"tide_shift_warning": _tr("换潮警告:准备移动", "TIDE WARNING: PREPARE TO MOVE"),
|
|
"tide_followed": _tr("队伍顺潮进入新安全位", "THE COMPANY FOLLOWS THE TIDE"),
|
|
"tide_shifted_while_holding": _tr("队伍留在原地并被潮水浸湿", "THE LINE HOLDS AND BECOMES WET"),
|
|
"allied_basic": _tr("队员发起攻击", "A RECRUIT ATTACKS"),
|
|
"enemy_basic_damage": _tr("敌人击中前线", "THE ENEMY HITS THE FRONT"),
|
|
"authoritative_damage": _tr("结算伤害 -%d" % int(event.get("amount", 0)), "DAMAGE SETTLED -%d" % int(event.get("amount", 0))),
|
|
"recruit_fallen": _tr("一名队员永久阵亡", "A RECRUIT IS PERMANENTLY FALLEN"),
|
|
"battle_terminal": _tr("穿越结果已确定", "THE CROSSING RESULT IS SET"),
|
|
}.get(kind, kind.replace("_", " ").to_upper())
|
|
|
|
|
|
func _region_event_color(kind: String) -> Color:
|
|
if kind in ["healing_denial_attached", "healing_converted_to_enemy_guard", "pull_committed", "indexed_mud_created", "enemy_basic_damage", "authoritative_damage", "recruit_fallen"]:
|
|
return CORAL
|
|
if kind in ["pull_resisted", "tide_followed", "battle_terminal"]:
|
|
return JADE
|
|
return GOLD_LIGHT
|
|
|
|
|
|
func _route_summary(risk: int) -> String:
|
|
return [
|
|
_tr("低风险:拉拽会改变邻位受击;稳住前后顺序更省生命。", "LOW RISK: PULL CHANGES ADJACENCY; A STABLE LINE PRESERVES MORE VITALITY."),
|
|
_tr("中风险:先处理治疗封锁,并等迟滞过半后再净化。", "MEDIUM RISK: REMOVE HEALING DENIAL AND WAIT PAST THE HINDRANCE MIDPOINT."),
|
|
_tr("高风险:换潮时留在原地会失去治疗窗口;顺潮移动更安全。", "HIGH RISK: HOLDING THROUGH THE TIDE LOSES THE HEALING WINDOW; FOLLOWING IT IS SAFER."),
|
|
][clampi(risk, 0, 2)]
|
|
|
|
|
|
func _region_name(row_id: String) -> String:
|
|
if row_id.is_empty():
|
|
return ""
|
|
var regional := RegionContent.localized_name(region_catalog, row_id, locale)
|
|
if not regional.is_empty():
|
|
return regional
|
|
return _row_name(row_id, "equipment" if row_id.begins_with("EQ-") else "enemy")
|
|
|
|
|
|
func _equipment_symbol(catalog_id: String) -> String:
|
|
return {"EQ-041": "▰", "EQ-042": "◒", "EQ-043": "◎", "EQ-044": "⚑", "EQ-045": "Ⅱ", "EQ-046": "+", "EQ-047": "▱", "EQ-048": "◁"}.get(catalog_id, "◇")
|
|
|
|
|
|
func _region_enemy_color(enemy_id: String) -> Color:
|
|
var number := int(enemy_id.trim_prefix("EN-R2-"))
|
|
return [Color("#b5e3b2"), VIOLET, Color("#c6a3de"), Color("#9dbd78"), GOLD_LIGHT, CYAN, CORAL][number % 7]
|