843 lines
34 KiB
GDScript
843 lines
34 KiB
GDScript
class_name ABGFG4CampaignAdapter
|
|
extends RefCounted
|
|
|
|
const Contract1Battle = preload("res://fg4/contract1_battle_service.gd")
|
|
const Contract1Service = preload("res://fg4/contract1_service.gd")
|
|
const Contract2Activation = preload("res://fg4/contract2_activation_service.gd")
|
|
const Contract2Battle = preload("res://fg4/contract2_battle_service.gd")
|
|
const Contract2Execution = preload("res://fg4/contract2_service.gd")
|
|
const FG4Content = preload("res://fg4/content_registry.gd")
|
|
const P100Content = preload("res://p10_0/content_registry.gd")
|
|
const RG04Activation = preload("res://fg4/rg04_activation_service.gd")
|
|
const RG04Contract1Battle = preload("res://fg4/rg04_contract1_battle_service.gd")
|
|
const RG04Contract1Service = preload("res://fg4/rg04_contract1_service.gd")
|
|
const RG04Contract2Activation = preload("res://fg4/rg04_contract2_activation_service.gd")
|
|
const RG04Contract2Battle = preload("res://fg4/rg04_contract2_battle_service.gd")
|
|
const RG04Contract2Service = preload("res://fg4/rg04_contract2_service.gd")
|
|
const State = preload("res://fg4/run_state.gd")
|
|
|
|
const CONTRACT1_PHASES := [
|
|
"market", "prepared", "recovery", "boss_muster", "boss_prepared",
|
|
"boss_recovery", "contract_result",
|
|
]
|
|
const CONTRACT2_PHASES := [
|
|
"contract2_market", "contract2_prepared", "contract2_recovery",
|
|
"contract2_boss_muster", "contract2_boss_prepared",
|
|
"contract2_boss_recovery", "region_result", "region_closed",
|
|
]
|
|
const RG04_CONTRACT1_PHASES := [
|
|
"rg04_market", "rg04_prepared", "rg04_recovery", "rg04_boss_muster",
|
|
"rg04_boss_prepared", "rg04_boss_recovery", "rg04_contract_result",
|
|
]
|
|
const RG04_CONTRACT2_PHASES := [
|
|
"rg04_contract2_market", "rg04_contract2_prepared",
|
|
"rg04_contract2_recovery", "rg04_contract2_boss_muster",
|
|
"rg04_contract2_boss_prepared", "rg04_contract2_boss_recovery",
|
|
"rg04_region_result", "rg04_region_closed",
|
|
]
|
|
const PLAN_COPY := {
|
|
"vent_and_brace": ["排压并稳固", "VENT & BRACE"],
|
|
"strong_side_burst": ["爆发强侧", "BURST STRONG SIDE"],
|
|
"device_removal": ["拆除构装体", "REMOVE CONSTRUCT"],
|
|
"cooldown_bait": ["诱导冷却", "BAIT COOLDOWN"],
|
|
"vary_tags": ["轮换行动标签", "VARY ACTION TAGS"],
|
|
"safe_promotion": ["允许安全晋升", "ALLOW SAFE PROMOTION"],
|
|
"remove_sparks": ["提前清除火花", "REMOVE SPARKS"],
|
|
"focus_foreman": ["集火监工", "FOCUS FOREMAN"],
|
|
"feed_and_obey": ["喂入安全来源并遵守", "FEED SAFE SOURCE & OBEY"],
|
|
"deny_and_break": ["拒绝复制并主动违背", "DENY COPY & BREAK"],
|
|
"steer_and_obey": ["引导指针并遵守", "STEER POINTER & OBEY"],
|
|
"guard_and_break": ["守住索引并违背", "GUARD INDEX & BREAK"],
|
|
"bait_and_obey": ["诱导净化并遵守", "BAIT CLEANSE & OBEY"],
|
|
"raw_and_break": ["承受原始伤害并违背", "TAKE RAW & BREAK"],
|
|
"hold_and_obey": ["保持条款并遵守", "HOLD CLAUSE & OBEY"],
|
|
"race_and_break": ["抢先结算并违背", "RACE RESOLUTION & BREAK"],
|
|
"steer_action_count": ["操纵行动计数", "STEER ACTION COUNT"],
|
|
"interrupt_pointer": ["打断索引指针", "INTERRUPT INDEX POINTER"],
|
|
"seed_and_obey": ["预置安全规则并遵守", "SEED SAFE RULE & OBEY"],
|
|
"reorder_and_break": ["重排重放并违背", "REORDER REPLAY & BREAK"],
|
|
"seed_and_redirect": ["预置规则并重定向", "SEED RULE & REDIRECT"],
|
|
"interrupt_and_break": ["打断重放并违背", "INTERRUPT REPLAY & BREAK"],
|
|
"seed_and_cleanse": ["预置规则并净化", "SEED RULE & CLEANSE"],
|
|
"interrupt_and_cleanse": ["打断重放并净化", "INTERRUPT & CLEANSE"],
|
|
"seed_and_preserve": ["预置规则并保持", "SEED RULE & PRESERVE"],
|
|
"reorder_and_preserve": ["重排重放并保持", "REORDER & PRESERVE"],
|
|
"safe_rule_seed": ["预置无害规则", "SEED HARMLESS RULES"],
|
|
"reorder_and_interrupt": ["重排并打断危险重放", "REORDER & INTERRUPT"],
|
|
}
|
|
|
|
var service
|
|
var market
|
|
var valid := false
|
|
var error_code := "adapter_not_configured"
|
|
var current_battle: Dictionary = {}
|
|
var event_cursor := 0
|
|
var operation_serial := 0
|
|
var fg4_catalog: Dictionary = FG4Content.snapshot()
|
|
var p100_catalog: Dictionary = P100Content.snapshot()
|
|
var profession_rows: Dictionary = P100Content.rows_by_id(p100_catalog, "professions")
|
|
var enemy_rows: Dictionary = FG4Content.rows_by_id(fg4_catalog, "enemies")
|
|
var boss_rows: Dictionary = FG4Content.rows_by_id(fg4_catalog, "bosses")
|
|
|
|
|
|
func configure(snapshot_value: Dictionary) -> Dictionary:
|
|
current_battle = {}
|
|
event_cursor = 0
|
|
operation_serial = 0
|
|
if not _bind(snapshot_value):
|
|
return _reject(error_code)
|
|
return {
|
|
"ok": true,
|
|
"code": "fg4_adapter_configured",
|
|
"snapshot_hash": snapshot_hash(),
|
|
"view": view_state(),
|
|
}
|
|
|
|
|
|
func snapshot() -> Dictionary:
|
|
return service.state.snapshot() if valid else {}
|
|
|
|
|
|
func snapshot_hash() -> String:
|
|
return hash_value(snapshot())
|
|
|
|
|
|
func view_state(locale := "zh_CN") -> Dictionary:
|
|
if not valid:
|
|
return {}
|
|
var run: Dictionary = service.state.current_run
|
|
var phase := String(run.phase)
|
|
return {
|
|
"region_id": String(run.region_id),
|
|
"contract_id": String(run.contract_id),
|
|
"phase": phase,
|
|
"mode": _mode(phase),
|
|
"market_index": int(run.market_index),
|
|
"round_index": int(run.round_index),
|
|
"cleared_rounds": int(run.cleared_rounds),
|
|
"coin": int(run.coin),
|
|
"replacement_credit": int(run.replacement_credit),
|
|
"line_capacity": int(run.line_capacity),
|
|
"company": _company_view(run, locale),
|
|
"routes": _route_view(run, locale),
|
|
"plans": _plan_view(run, locale),
|
|
"battle": _battle_view(run, locale),
|
|
"reward": _result_view(run, locale),
|
|
"recovery_required": bool(run.recovery_required),
|
|
"recovery_cost": int(run.recovery_cost),
|
|
"boss_id": String(run.boss_muster.get("boss_id", "")),
|
|
"records_count": service.state.guild_profile.company_records.size(),
|
|
"region_records_count": _region_record_count(),
|
|
"persistent_renown": int(service.state.guild_profile.persistent_renown),
|
|
"unlocked_region_ids": Array(service.state.guild_profile.unlocked_region_ids).duplicate(),
|
|
"source_revision": int(service.state.revision),
|
|
"source_snapshot_hash": snapshot_hash(),
|
|
}
|
|
|
|
|
|
func prepare_route(offer_id: String, locale := "zh_CN") -> Dictionary:
|
|
if not valid:
|
|
return _reject(error_code)
|
|
var phase := String(service.state.current_run.phase)
|
|
if phase not in ["market", "contract2_market", "rg04_market", "rg04_contract2_market"]:
|
|
return _reject("market_required")
|
|
var result: Dictionary
|
|
if phase.begins_with("rg04_"):
|
|
result = service.commit_route(
|
|
_operation("prepare", offer_id), service.state.revision, offer_id,
|
|
String(service.state.current_run.market_board.board_hash)
|
|
)
|
|
elif phase == "contract2_market":
|
|
result = service.commit_contract2_route(
|
|
_operation("prepare", offer_id), service.state.revision, offer_id,
|
|
String(service.state.current_run.market_board.board_hash)
|
|
)
|
|
else:
|
|
result = service.commit_route(
|
|
_operation("prepare", offer_id), service.state.revision, offer_id,
|
|
String(service.state.current_run.market_board.board_hash)
|
|
)
|
|
return _replace_from_result(result, locale)
|
|
|
|
|
|
func start_plan(plan_id: String, locale := "zh_CN") -> Dictionary:
|
|
if not valid:
|
|
return _reject(error_code)
|
|
var phase := String(service.state.current_run.phase)
|
|
if phase in ["boss_muster", "contract2_boss_muster", "rg04_boss_muster", "rg04_contract2_boss_muster"]:
|
|
var prepared: Dictionary
|
|
if phase == "contract2_boss_muster":
|
|
prepared = service.commit_contract2_boss_plan(
|
|
_operation("boss-plan", plan_id), service.state.revision,
|
|
String(service.state.current_run.boss_muster.muster_hash), plan_id
|
|
)
|
|
else:
|
|
prepared = service.commit_boss_plan(
|
|
_operation("boss-plan", plan_id), service.state.revision,
|
|
String(service.state.current_run.boss_muster.muster_hash), plan_id
|
|
)
|
|
if not prepared.ok:
|
|
return prepared
|
|
if not _bind(prepared.snapshot):
|
|
return _reject(error_code)
|
|
phase = String(service.state.current_run.phase)
|
|
if phase not in [
|
|
"prepared", "contract2_prepared", "boss_prepared", "contract2_boss_prepared",
|
|
"rg04_prepared", "rg04_boss_prepared", "rg04_contract2_prepared",
|
|
"rg04_contract2_boss_prepared",
|
|
]:
|
|
return _reject("prepared_state_required")
|
|
var battle_service
|
|
if phase in ["rg04_contract2_prepared", "rg04_contract2_boss_prepared"]:
|
|
battle_service = RG04Contract2Battle.new(snapshot())
|
|
elif phase in ["rg04_prepared", "rg04_boss_prepared"]:
|
|
battle_service = RG04Contract1Battle.new(snapshot())
|
|
elif phase.begins_with("contract2_"):
|
|
battle_service = Contract2Battle.new(snapshot())
|
|
else:
|
|
battle_service = Contract1Battle.new(snapshot())
|
|
var battle: Dictionary = battle_service.run_to_terminal(plan_id)
|
|
if not battle.ok:
|
|
return battle
|
|
current_battle = Dictionary(battle.snapshot).duplicate(true)
|
|
event_cursor = 0
|
|
return {
|
|
"ok": true,
|
|
"code": "fg4_battle_started",
|
|
"plan_id": plan_id,
|
|
"event_log_hash": String(current_battle.event_log_hash),
|
|
"view": view_state(locale),
|
|
}
|
|
|
|
|
|
func advance_battle_ticks(count: int, locale := "zh_CN") -> Dictionary:
|
|
if current_battle.is_empty():
|
|
return _reject("battle_required")
|
|
var events: Array = current_battle.event_log
|
|
var next_cursor := mini(events.size(), event_cursor + maxi(1, count))
|
|
var fresh: Array = events.slice(event_cursor, next_cursor)
|
|
event_cursor = next_cursor
|
|
return {
|
|
"ok": true,
|
|
"code": "fg4_battle_terminal" if event_cursor == events.size() else "fg4_battle_advanced",
|
|
"events": normalize_events(fresh),
|
|
"event_log_hash": String(current_battle.event_log_hash),
|
|
"view": view_state(locale),
|
|
}
|
|
|
|
|
|
func run_battle_to_terminal(locale := "zh_CN") -> Dictionary:
|
|
if current_battle.is_empty():
|
|
return _reject("battle_required")
|
|
return advance_battle_ticks(current_battle.event_log.size() - event_cursor + 1, locale)
|
|
|
|
|
|
func settle_terminal(locale := "zh_CN") -> Dictionary:
|
|
if current_battle.is_empty() or event_cursor < current_battle.event_log.size():
|
|
return _reject("visible_terminal_required")
|
|
var operation_id := _operation("settle", String(current_battle.snapshot_hash))
|
|
var result: Dictionary
|
|
match String(current_battle.kind):
|
|
"fg4_rg04_contract1_boss", "fg4_rg04_contract2_boss":
|
|
result = service.settle_boss(operation_id, service.state.revision, current_battle)
|
|
"fg4_rg04_contract1_route", "fg4_rg04_contract2_route":
|
|
result = service.settle_route(operation_id, service.state.revision, current_battle)
|
|
"fg4_contract1_boss":
|
|
result = service.settle_boss(operation_id, service.state.revision, current_battle)
|
|
"fg4_contract2_boss":
|
|
result = service.settle_contract2_boss(operation_id, service.state.revision, current_battle)
|
|
"fg4_contract2_route":
|
|
result = service.settle_contract2_route(operation_id, service.state.revision, current_battle)
|
|
_:
|
|
result = service.settle_route(operation_id, service.state.revision, current_battle)
|
|
if not result.ok:
|
|
return result
|
|
current_battle = {}
|
|
event_cursor = 0
|
|
return _replace_from_result(result, locale)
|
|
|
|
|
|
func continue_result(locale := "zh_CN") -> Dictionary:
|
|
if not valid:
|
|
return _reject(error_code)
|
|
var phase := String(service.state.current_run.phase)
|
|
var result: Dictionary
|
|
if phase == "contract_result":
|
|
result = service.close_contract(
|
|
_operation("close-contract1", "CLEAR-RG03-C01"),
|
|
service.state.revision,
|
|
String(service.state.current_run.pending_boss_result.close_id)
|
|
)
|
|
elif phase == "contract2_handoff":
|
|
result = service.activate_contract(
|
|
_operation("activate-contract2", "CTR-R3-02"),
|
|
service.state.revision,
|
|
String(service.state.current_run.market_board.handoff_hash)
|
|
)
|
|
elif phase == "region_result":
|
|
result = service.close_region(
|
|
_operation("close-region", "RG-03"),
|
|
service.state.revision,
|
|
String(service.state.current_run.pending_boss_result.close_id)
|
|
)
|
|
elif phase == "region_closed":
|
|
var activation := RG04Activation.new(snapshot())
|
|
result = activation.activate_region(
|
|
_operation("activate-region", "RG-04"), activation.state.revision,
|
|
String(activation.state.current_run.market_board.result_hash)
|
|
)
|
|
elif phase == "rg04_contract_result":
|
|
result = service.close_contract(
|
|
_operation("close-contract3", "CLEAR-RG04-C01"),
|
|
service.state.revision,
|
|
String(service.state.current_run.pending_boss_result.close_id)
|
|
)
|
|
elif phase == "rg04_contract2_handoff":
|
|
result = service.activate_contract(
|
|
_operation("activate-contract4", "CTR-R4-02"),
|
|
service.state.revision,
|
|
String(service.state.current_run.market_board.handoff_hash)
|
|
)
|
|
elif phase == "rg04_region_result":
|
|
result = service.close_region(
|
|
_operation("close-region", "RG-04"),
|
|
service.state.revision,
|
|
String(service.state.current_run.pending_boss_result.close_id)
|
|
)
|
|
else:
|
|
return _reject("result_state_required")
|
|
return _replace_from_result(result, locale)
|
|
|
|
|
|
func recover(locale := "zh_CN") -> Dictionary:
|
|
if not valid:
|
|
return _reject(error_code)
|
|
var run: Dictionary = service.state.current_run
|
|
var phase := String(run.phase)
|
|
var result: Dictionary
|
|
if phase == "recovery":
|
|
result = service.recover_route(
|
|
_operation("recover-route", String(run.last_battle_report.battle_hash)),
|
|
service.state.revision,
|
|
String(run.last_battle_report.battle_hash),
|
|
int(run.recovery_cost)
|
|
)
|
|
elif phase == "contract2_recovery":
|
|
result = service.recover_contract2_route(
|
|
_operation("recover-route", String(run.last_battle_report.battle_hash)),
|
|
service.state.revision, String(run.last_battle_report.battle_hash),
|
|
int(run.recovery_cost)
|
|
)
|
|
elif phase == "boss_recovery":
|
|
result = service.recover_boss(
|
|
_operation("recover-boss", String(run.pending_boss_result.battle_hash)),
|
|
service.state.revision,
|
|
String(run.pending_boss_result.battle_hash),
|
|
int(run.recovery_cost)
|
|
)
|
|
elif phase == "contract2_boss_recovery":
|
|
result = service.recover_contract2_boss(
|
|
_operation("recover-boss", String(run.pending_boss_result.battle_hash)),
|
|
service.state.revision, String(run.pending_boss_result.battle_hash),
|
|
int(run.recovery_cost)
|
|
)
|
|
elif phase in ["rg04_recovery", "rg04_contract2_recovery"]:
|
|
result = service.recover_route(
|
|
_operation("recover-route", String(run.last_battle_report.battle_hash)),
|
|
service.state.revision, String(run.last_battle_report.battle_hash),
|
|
int(run.recovery_cost)
|
|
)
|
|
elif phase in ["rg04_boss_recovery", "rg04_contract2_boss_recovery"]:
|
|
result = service.recover_boss(
|
|
_operation("recover-boss", String(run.pending_boss_result.battle_hash)),
|
|
service.state.revision, String(run.pending_boss_result.battle_hash),
|
|
int(run.recovery_cost)
|
|
)
|
|
else:
|
|
return _reject("recovery_state_required")
|
|
return _replace_from_result(result, locale)
|
|
|
|
|
|
func commit_order(_ordered_recruit_ids: Array) -> Dictionary:
|
|
return _reject("formation_authority_frozen")
|
|
|
|
|
|
func normalize_events(events: Array) -> Array[Dictionary]:
|
|
var normalized: Array[Dictionary] = []
|
|
for raw_event in events:
|
|
if not raw_event is Dictionary:
|
|
continue
|
|
var event: Dictionary = raw_event
|
|
var authority_kind := String(event.get("kind", ""))
|
|
var kind: String = String({
|
|
"wave_entered": "wave_arrived",
|
|
"authoritative_damage": "damage",
|
|
"battle_terminal": "outcome",
|
|
"boss_terminal": "outcome",
|
|
}.get(authority_kind, authority_kind))
|
|
var detail: Dictionary = Dictionary(event.get("payload", {})).duplicate(true)
|
|
detail["authority_kind"] = authority_kind
|
|
if kind == "damage":
|
|
detail["raw"] = int(detail.get("amount", 0))
|
|
detail["effective"] = int(detail.get("amount", 0))
|
|
if kind == "outcome":
|
|
detail["outcome"] = String(detail.get("outcome", current_battle.get("outcome", "")))
|
|
normalized.append({
|
|
"event_id": String(event.get("event_id", "")),
|
|
"tick": int(event.get("tick", 0)),
|
|
"kind": String(kind),
|
|
"source_id": String(event.get("source_id", "")),
|
|
"target_id": String(event.get("target_id", "")),
|
|
"detail": detail,
|
|
})
|
|
return normalized
|
|
|
|
|
|
static func hash_value(value) -> String:
|
|
return JSON.stringify(_canonical(value)).sha256_text()
|
|
|
|
|
|
func _bind(snapshot_value: Dictionary) -> bool:
|
|
var probe := State.new()
|
|
if not probe.restore_snapshot(snapshot_value):
|
|
valid = false
|
|
error_code = "invalid_fg4_snapshot"
|
|
return false
|
|
var phase := String(probe.current_run.phase)
|
|
if phase == "contract2_handoff":
|
|
service = Contract2Activation.new(snapshot_value)
|
|
elif phase == "rg04_contract2_handoff":
|
|
service = RG04Contract2Activation.new(snapshot_value)
|
|
elif phase in RG04_CONTRACT2_PHASES:
|
|
service = RG04Contract2Service.new(snapshot_value)
|
|
elif phase in RG04_CONTRACT1_PHASES:
|
|
service = RG04Contract1Service.new(snapshot_value)
|
|
elif phase in CONTRACT2_PHASES:
|
|
service = Contract2Execution.new(snapshot_value)
|
|
elif phase in CONTRACT1_PHASES:
|
|
service = Contract1Service.new(snapshot_value)
|
|
else:
|
|
valid = false
|
|
error_code = "unsupported_fg4_phase:%s" % phase
|
|
return false
|
|
market = service
|
|
valid = bool(service.valid)
|
|
error_code = "" if valid else String(service.error_code)
|
|
return valid
|
|
|
|
|
|
func _replace_from_result(result: Dictionary, locale: String) -> Dictionary:
|
|
if not bool(result.get("ok", false)):
|
|
return result
|
|
if not _bind(Dictionary(result.snapshot)):
|
|
return _reject(error_code)
|
|
var forwarded := result.duplicate(true)
|
|
forwarded["view"] = view_state(locale)
|
|
return forwarded
|
|
|
|
|
|
func _mode(phase: String) -> String:
|
|
if phase in ["market", "contract2_market", "rg04_market", "rg04_contract2_market"]:
|
|
return "market"
|
|
if phase in ["prepared", "contract2_prepared", "rg04_prepared", "rg04_contract2_prepared"]:
|
|
return "plan"
|
|
if phase in ["boss_muster", "contract2_boss_muster", "rg04_boss_muster", "rg04_contract2_boss_muster"]:
|
|
return "boss_muster"
|
|
if phase in ["boss_prepared", "contract2_boss_prepared", "rg04_boss_prepared", "rg04_contract2_boss_prepared"]:
|
|
return "boss_plan"
|
|
if phase in ["recovery", "contract2_recovery", "boss_recovery", "contract2_boss_recovery", "rg04_recovery", "rg04_boss_recovery", "rg04_contract2_recovery", "rg04_contract2_boss_recovery"]:
|
|
return "recovery"
|
|
if phase == "contract_result":
|
|
return "contract_record"
|
|
if phase == "contract2_handoff":
|
|
return "contract_handoff"
|
|
if phase == "region_result":
|
|
return "region_record"
|
|
if phase == "region_closed":
|
|
return "region_closed"
|
|
if phase == "rg04_contract_result":
|
|
return "contract_record"
|
|
if phase == "rg04_contract2_handoff":
|
|
return "contract_handoff"
|
|
if phase == "rg04_region_result":
|
|
return "region_record"
|
|
if phase == "rg04_region_closed":
|
|
return "region_closed"
|
|
return "battle" if not current_battle.is_empty() else "unknown"
|
|
|
|
|
|
func _company_view(run: Dictionary, locale: String) -> Array[Dictionary]:
|
|
var rows: Array[Dictionary] = []
|
|
for recruit_id_value in run.company:
|
|
var recruit_id := String(recruit_id_value)
|
|
var record: Dictionary = run.recruit_records[recruit_id]
|
|
var profession_id := String(record.profession_id)
|
|
var profession: Dictionary = profession_rows.get(profession_id, {})
|
|
rows.append({
|
|
"recruit_id": recruit_id,
|
|
"actor_id": recruit_id,
|
|
"identity_id": profession_id,
|
|
"profession_id": profession_id,
|
|
"profession_name": String(profession.get("name", profession_id)),
|
|
"trait_id": String(record.trait_id),
|
|
"level": int(record.level),
|
|
"hp": int(record.current_hp),
|
|
"max_hp": int(record.max_hp),
|
|
"guard": 0,
|
|
"deployed": recruit_id in run.deployed_recruit_ids,
|
|
"locale": locale,
|
|
})
|
|
return rows
|
|
|
|
|
|
func _route_view(run: Dictionary, locale: String) -> Array[Dictionary]:
|
|
if String(run.phase) not in ["market", "contract2_market", "rg04_market", "rg04_contract2_market"]:
|
|
return []
|
|
var rows: Array[Dictionary] = []
|
|
for offer_value in run.market_board.battle_offers:
|
|
var offer: Dictionary = offer_value
|
|
var enemy_names: Array[String] = []
|
|
for enemy_id in offer.enemy_ids:
|
|
var enemy: Dictionary = enemy_rows.get(str(enemy_id), {})
|
|
enemy_names.append(_localized(enemy, locale, str(enemy_id)))
|
|
rows.append({
|
|
"offer_id": str(offer.offer_id),
|
|
"route_id": str(offer.route_id),
|
|
"name": _localized(offer, locale, str(offer.route_id)),
|
|
"risk": int(offer.risk),
|
|
"wave_count": offer.enemy_ids.size(),
|
|
"enemy_ids": Array(offer.enemy_ids).duplicate(),
|
|
"enemy_names": enemy_names,
|
|
"preview": _localized_field(offer.get("preview", ""), locale),
|
|
"failure_cause": _localized_field(offer.get("failure", offer.get("failure_cause", "")), locale),
|
|
"reward_coin": int(offer.reward_coin),
|
|
"recovery_coin": int(offer.recovery_coin),
|
|
})
|
|
return rows
|
|
|
|
|
|
func _plan_view(run: Dictionary, locale: String) -> Array[Dictionary]:
|
|
var phase := String(run.phase)
|
|
var ids: Array = []
|
|
var primary := ""
|
|
var secondary := ""
|
|
if phase in ["prepared", "contract2_prepared", "rg04_prepared", "rg04_contract2_prepared"]:
|
|
if phase == "prepared":
|
|
ids = Array(Contract1Battle.PLAN_IDS).duplicate()
|
|
var market_index := int(run.pending_encounter.round_index)
|
|
primary = String(Contract1Battle.PRIMARY_PLAN[market_index])
|
|
secondary = String(Contract1Battle.SECONDARY_PLAN[market_index])
|
|
elif phase == "contract2_prepared":
|
|
ids = Array(Contract2Battle.PLAN_IDS).duplicate()
|
|
var market_index := int(run.pending_encounter.round_index)
|
|
primary = String(Contract2Battle.PRIMARY_PLAN[market_index])
|
|
secondary = String(Contract2Battle.SECONDARY_PLAN[market_index])
|
|
elif phase == "rg04_prepared":
|
|
ids = Array(RG04Contract1Battle.PLAN_IDS).duplicate()
|
|
var market_index := int(run.pending_encounter.round_index)
|
|
primary = String(RG04Contract1Battle.PRIMARY_PLAN[market_index])
|
|
secondary = String(RG04Contract1Battle.SECONDARY_PLAN[market_index])
|
|
else:
|
|
ids = Array(RG04Contract2Battle.PLAN_IDS).duplicate()
|
|
var market_index := int(run.pending_encounter.round_index)
|
|
primary = String(RG04Contract2Battle.PRIMARY_PLAN[market_index])
|
|
secondary = String(RG04Contract2Battle.SECONDARY_PLAN[market_index])
|
|
elif phase in ["boss_muster", "contract2_boss_muster", "rg04_boss_muster", "rg04_contract2_boss_muster"]:
|
|
ids = Array(run.boss_muster.plan_ids).duplicate()
|
|
else:
|
|
return []
|
|
var rows: Array[Dictionary] = []
|
|
for plan_id_value in ids:
|
|
var plan_id := String(plan_id_value)
|
|
var copy: Array = PLAN_COPY.get(plan_id, [plan_id, plan_id])
|
|
rows.append({
|
|
"plan_id": plan_id,
|
|
"offer_id": plan_id,
|
|
"name": String(copy[1] if locale == "en" else copy[0]),
|
|
"primary": plan_id == primary,
|
|
"secondary": plan_id == secondary,
|
|
})
|
|
return rows
|
|
|
|
|
|
func _battle_view(run: Dictionary, locale: String) -> Dictionary:
|
|
if current_battle.is_empty():
|
|
return {}
|
|
var allies: Array[Dictionary] = []
|
|
var hp_by_actor := {}
|
|
for recruit_id_value in current_battle.final_order:
|
|
var recruit_id := String(recruit_id_value)
|
|
var actor: Dictionary = current_battle.actors[recruit_id]
|
|
hp_by_actor[recruit_id] = int(actor.hp_before)
|
|
for event in Array(current_battle.event_log).slice(0, event_cursor):
|
|
if String(event.get("kind", "")) == "authoritative_damage":
|
|
hp_by_actor[String(event.target_id)] = int(event.payload.hp_after)
|
|
for index in range(current_battle.final_order.size()):
|
|
var recruit_id := String(current_battle.final_order[index])
|
|
var actor: Dictionary = current_battle.actors[recruit_id]
|
|
var profession_id := String(actor.profession_id)
|
|
var profession: Dictionary = profession_rows.get(profession_id, {})
|
|
allies.append({
|
|
"actor_id": recruit_id,
|
|
"identity_id": profession_id,
|
|
"profession_id": profession_id,
|
|
"profession_name": String(profession.get("name", profession_id)),
|
|
"hp": int(hp_by_actor[recruit_id]),
|
|
"max_hp": int(actor.max_hp),
|
|
"guard": 0,
|
|
"slot_index": index,
|
|
})
|
|
var enemy_ids: Array = []
|
|
if String(current_battle.get("boss_id", "")).is_empty():
|
|
enemy_ids = Array(run.pending_encounter.enemy_ids).duplicate()
|
|
else:
|
|
enemy_ids = [String(current_battle.boss_id)]
|
|
var enemies: Array[Dictionary] = []
|
|
for index in range(enemy_ids.size()):
|
|
var enemy_id := String(enemy_ids[index])
|
|
var row: Dictionary = boss_rows.get(enemy_id, enemy_rows.get(enemy_id, {}))
|
|
enemies.append({
|
|
"actor_id": enemy_id,
|
|
"identity_id": enemy_id,
|
|
"name": _localized(row, locale, enemy_id),
|
|
"hp": 1,
|
|
"max_hp": 1,
|
|
"guard": 0,
|
|
"slot_index": index,
|
|
})
|
|
var revealed: Array = Array(current_battle.event_log).slice(0, event_cursor)
|
|
var terminal: bool = event_cursor >= current_battle.event_log.size()
|
|
var decisive: Dictionary = {}
|
|
for event in normalize_events(revealed):
|
|
if bool(event.detail.get("decisive", false)) or String(event.kind) in ["outcome", "plan_response"]:
|
|
decisive = event
|
|
break
|
|
return {
|
|
"allies": allies,
|
|
"enemies": enemies,
|
|
"terminal": terminal,
|
|
"outcome": String(current_battle.outcome) if terminal else "",
|
|
"cause": String(current_battle.cause) if terminal else "",
|
|
"next_wave": mini(enemy_ids.size(), _event_count(revealed, "wave_entered") + 1),
|
|
"wave_count": enemy_ids.size(),
|
|
"first_decisive_event": decisive,
|
|
"fallen_recruit_ids": Array(current_battle.fallen_recruit_ids).duplicate() if terminal else [],
|
|
"event_count": event_cursor,
|
|
"event_log_hash": String(current_battle.event_log_hash),
|
|
"plan_id": String(current_battle.plan_id),
|
|
"boss_id": String(current_battle.get("boss_id", "")),
|
|
"mechanics": _mechanics_view(revealed),
|
|
}
|
|
|
|
|
|
func _mechanics_view(revealed: Array) -> Dictionary:
|
|
var normalized := normalize_events(revealed)
|
|
var kinds: Dictionary = {}
|
|
var details: Dictionary = {}
|
|
for event in normalized:
|
|
var event_kind := String(event.kind)
|
|
kinds[event_kind] = int(kinds.get(event_kind, 0)) + 1
|
|
details[event_kind] = Dictionary(event.detail).duplicate(true)
|
|
var battle_kind := String(current_battle.get("kind", ""))
|
|
if battle_kind == "fg4_contract1_route":
|
|
return {
|
|
"kind": "valve_heat",
|
|
"valve": Dictionary(current_battle.get("pressure_valve", {})).duplicate(true),
|
|
"valve_disclosed": kinds.has("pressure_valve_disclosed"),
|
|
"construct_visible": kinds.has("construct_state_changed"),
|
|
"construct_removed": bool(details.get("construct_state_changed", {}).get("removed", false)),
|
|
"delayed_heat_visible": kinds.has("delayed_heat_forecast"),
|
|
"heat_resolve_tick": int(details.get("delayed_heat_forecast", {}).get("resolve_tick", 0)),
|
|
"cooldown_visible": kinds.has("item_cooldown_forecast"),
|
|
}
|
|
if battle_kind == "fg4_contract1_boss":
|
|
return {
|
|
"kind": "furnace_pressure",
|
|
"valve_disclosed": kinds.has("pressure_valve_disclosed"),
|
|
"weak_side": String(details.get("pressure_valve_disclosed", {}).get("weak_side", "left")),
|
|
"rupture_tick": int(details.get("pressure_valve_disclosed", {}).get("rupture_tick", 72)),
|
|
"side_swap_visible": kinds.has("side_swap_forecast"),
|
|
"vented": kinds.has("weak_side_vented"),
|
|
"burst": kinds.has("strong_side_burst"),
|
|
}
|
|
if battle_kind in ["fg4_contract2_route", "fg4_contract2_boss"]:
|
|
var disclosure: Dictionary = Dictionary(current_battle.get("spark_disclosure", {})).duplicate(true)
|
|
if disclosure.is_empty():
|
|
disclosure = {
|
|
"repeat_threshold": int(details.get("repeat_counter_disclosed", {}).get("threshold", 3)),
|
|
"next_spark_index": 4,
|
|
"copied_target_rule": String(details.get("next_spark_disclosed", {}).get("target_rule", "declared_action_target")),
|
|
}
|
|
return {
|
|
"kind": "spark_repeat",
|
|
"spark_count": 8,
|
|
"repeat_disclosed": kinds.has("repeat_counter_disclosed"),
|
|
"repeat_threshold": int(disclosure.get("repeat_threshold", 3)),
|
|
"next_spark_index": int(disclosure.get("next_spark_index", 4)),
|
|
"copied_target_rule": String(disclosure.get("copied_target_rule", "declared_action_target")),
|
|
"promoted": kinds.has("spark_promoted"),
|
|
"removed": kinds.has("spark_removed"),
|
|
"reset": kinds.has("repeat_counter_reset"),
|
|
}
|
|
if battle_kind in [
|
|
"fg4_rg04_contract1_route", "fg4_rg04_contract1_boss",
|
|
"fg4_rg04_contract2_route", "fg4_rg04_contract2_boss",
|
|
]:
|
|
var clause: Dictionary = Dictionary(current_battle.get("archive_clause", {})).duplicate(true)
|
|
var copy: Dictionary = Dictionary(current_battle.get("copy_disclosure", {})).duplicate(true)
|
|
var redaction: Dictionary = Dictionary(current_battle.get("redaction_disclosure", {})).duplicate(true)
|
|
return {
|
|
"kind": "quillroot_archive",
|
|
"clause_kind": String(clause.get("kind", "")),
|
|
"affected_field": String(clause.get("affected_field", "")),
|
|
"clause_disclosed": kinds.has("archive_clause_disclosed"),
|
|
"clause_resolved": kinds.has("archive_clause_obeyed") or kinds.has("archive_clause_broken"),
|
|
"pressure_family": String(copy.get("pressure_family", "")),
|
|
"copy_source": String(copy.get("source_rule", "")),
|
|
"copy_target": String(copy.get("target_rule", "")),
|
|
"copy_disclosed": kinds.has("copy_source_disclosed"),
|
|
"redaction_source": String(redaction.get("source_rule", "")),
|
|
"redaction_source_visible": kinds.has("redaction_source_disclosed"),
|
|
"omitted_target_clause": String(current_battle.get("omitted_target_clause", redaction.get("omitted_target_clause", ""))),
|
|
"omitted_target_visible": kinds.has("target_clause_omitted"),
|
|
"replay_order": Array(current_battle.get("replay_order", redaction.get("replay_order", []))).duplicate(),
|
|
"replay_order_visible": kinds.has("replay_order_disclosed"),
|
|
"pointer_visible": kinds.has("index_pointer_disclosed"),
|
|
"pointer_answered": kinds.has("action_count_steered") or kinds.has("index_pointer_interrupted"),
|
|
}
|
|
return {}
|
|
|
|
|
|
func _result_view(run: Dictionary, locale: String) -> Dictionary:
|
|
var phase := String(run.phase)
|
|
if phase == "contract_result":
|
|
return {
|
|
"kind": "contract_record",
|
|
"title": _text(locale, "第一份余烬契约已记录", "FIRST EMBERLOOM CONTRACT RECORDED"),
|
|
"body": _text(locale, "幸存队伍保留伤势、装备与神器,继续追查九火监工。", "THE SURVIVING COMPANY KEEPS HP, EQUIPMENT AND ARTIFACT WHILE PURSUING THE FOREMAN."),
|
|
"renown": 18,
|
|
"action": _text(locale, "写入第一份记录", "WRITE FIRST RECORD"),
|
|
}
|
|
if phase == "contract2_handoff":
|
|
return {
|
|
"kind": "contract_handoff",
|
|
"title": _text(locale, "第二份契约已开放", "CONTRACT TWO IS OPEN"),
|
|
"body": _text(locale, "保留当前成员、生命、装备与神器,进入十二轮火花账战线。", "KEEP COMPANY, HP, EQUIPMENT AND ARTIFACT FOR TWELVE SPARK-LEDGER ROUNDS."),
|
|
"action": _text(locale, "进入第二份契约", "ENTER CONTRACT TWO"),
|
|
}
|
|
if phase == "region_result":
|
|
return {
|
|
"kind": "region_record",
|
|
"title": _text(locale, "九火监工已停工", "FOREMAN NINE-SPARKS IS SILENT"),
|
|
"body": _text(locale, "第二份记录与 22 声望等待写入。", "THE SECOND RECORD AND 22 RENOWN ARE READY."),
|
|
"renown": 22,
|
|
"action": _text(locale, "修复余烬织境", "RESTORE EMBERLOOM"),
|
|
}
|
|
if phase == "region_closed":
|
|
return {
|
|
"kind": "region_closed",
|
|
"title": _text(locale, "余烬织境已修复", "EMBERLOOM RESTORED"),
|
|
"body": _text(locale, "两份队伍记录已保存,契文根庭 RG-04 已开放。", "TWO COMPANY RECORDS SAVED; QUILLROOT RG-04 UNLOCKED."),
|
|
"action": _text(locale, "进入根羽档案馆", "ENTER QUILLROOT ARCHIVE"),
|
|
}
|
|
if phase == "rg04_contract_result":
|
|
return {
|
|
"kind": "contract_record",
|
|
"title": _text(locale, "行走索引已归档", "THE WALKING INDEX IS FILED"),
|
|
"body": _text(locale, "第一份根羽记录与 20 声望等待写入。", "THE FIRST QUILLROOT RECORD AND 20 RENOWN ARE READY."),
|
|
"renown": 20,
|
|
"action": _text(locale, "写入第一份根羽记录", "WRITE FIRST QUILLROOT RECORD"),
|
|
}
|
|
if phase == "rg04_contract2_handoff":
|
|
return {
|
|
"kind": "contract_handoff",
|
|
"title": _text(locale, "最终删节契约已开放", "THE FINAL REDACTION CONTRACT IS OPEN"),
|
|
"body": _text(locale, "保留当前队伍与全部后果,进入十二轮删节重放。", "KEEP THE COMPANY AND EVERY CONSEQUENCE FOR TWELVE REDACTION REPLAYS."),
|
|
"action": _text(locale, "进入最终契约", "ENTER FINAL CONTRACT"),
|
|
}
|
|
if phase == "rg04_region_result":
|
|
return {
|
|
"kind": "region_record",
|
|
"title": _text(locale, "删节雄鹿已停止重写", "REDACTION HART HAS STOPPED REWRITING"),
|
|
"body": _text(locale, "第二份根羽记录与 24 声望等待写入。", "THE SECOND QUILLROOT RECORD AND 24 RENOWN ARE READY."),
|
|
"renown": 24,
|
|
"action": _text(locale, "修复根羽档案馆", "RESTORE QUILLROOT ARCHIVE"),
|
|
}
|
|
if phase == "rg04_region_closed":
|
|
return {
|
|
"kind": "region_closed",
|
|
"title": _text(locale, "根羽档案馆已修复", "QUILLROOT ARCHIVE RESTORED"),
|
|
"body": _text(locale, "四份中期记录已保存,RG-05 已开放。", "FOUR MIDGAME RECORDS SAVED; RG-05 UNLOCKED."),
|
|
"action": "",
|
|
}
|
|
return {}
|
|
|
|
|
|
func _region_record_count() -> int:
|
|
var count := 0
|
|
for record in service.state.guild_profile.company_records:
|
|
var contract_id := String(record.get("contract_id", ""))
|
|
var region_id := String(service.state.current_run.region_id)
|
|
if (
|
|
region_id == "RG-03" and contract_id in ["CTR-R3-01", "CTR-R3-02"]
|
|
or region_id == "RG-04" and contract_id in ["CTR-R4-01", "CTR-R4-02"]
|
|
):
|
|
count += 1
|
|
return count
|
|
|
|
|
|
func _operation(kind: String, target: String) -> String:
|
|
operation_serial += 1
|
|
return "fg4-runtime-%s-%d-%s" % [kind, operation_serial, target.sha256_text().substr(0, 8)]
|
|
|
|
|
|
func _localized(row: Dictionary, locale: String, fallback: String) -> String:
|
|
var names = row.get("name", {})
|
|
if names is Dictionary:
|
|
return str(names.get("en" if locale == "en" else "zh_CN", fallback))
|
|
return str(row.get("name_en", fallback)) if locale == "en" else str(row.get("name_zh", fallback))
|
|
|
|
|
|
func _localized_field(value, locale: String) -> String:
|
|
if value is Dictionary:
|
|
return str(value.get("en" if locale == "en" else "zh_CN", ""))
|
|
return str(value)
|
|
|
|
|
|
func _text(locale: String, zh: String, en: String) -> String:
|
|
return en if locale == "en" else zh
|
|
|
|
|
|
func _event_count(events: Array, kind: String) -> int:
|
|
var count := 0
|
|
for event in events:
|
|
if String(event.get("kind", "")) == kind:
|
|
count += 1
|
|
return count
|
|
|
|
|
|
func _reject(code: String) -> Dictionary:
|
|
return {
|
|
"ok": false,
|
|
"code": code,
|
|
"view": view_state() if valid else {},
|
|
}
|
|
|
|
|
|
static func _canonical(value):
|
|
if value is float and is_equal_approx(value, round(value)):
|
|
return int(round(value))
|
|
if value is Dictionary:
|
|
var result := {}
|
|
var keys: Array = value.keys()
|
|
keys.sort_custom(func(left, right): return String(left) < String(right))
|
|
for key in keys:
|
|
result[key] = _canonical(value[key])
|
|
return result
|
|
if value is Array:
|
|
var result: Array = []
|
|
for item in value:
|
|
result.append(_canonical(item))
|
|
return result
|
|
return value
|