Files
aetherbound-guild/runtime/content/p9_10_content.gd
T

235 lines
17 KiB
GDScript

class_name P910Content
extends RefCounted
const P99Content = preload("res://content/p9_9_content.gd")
const REVISION := "p9.10-content-r1"
const BOSS_ID := "BO-R1-02"
const ROUND_BLUEPRINTS := {
4: {"name_zh": "风窗前廊", "name_en": "Galeglass Forecourt", "mechanic_id": "screened_front_gust", "pressure_zh": "投射屏障 + 前至后阵风", "pressure_en": "Projectile screen + front-to-rear gust", "target": "front_then_rear", "direction": "front_to_rear", "enemy_sets": [["EN-R1-05", "EN-R1-02"], ["EN-R1-05", "EN-R1-02", "EN-R1-03"], ["EN-R1-12", "EN-R1-02", "EN-R1-03"]]},
5: {"name_zh": "羊角回道", "name_en": "Ramturn Walk", "mechanic_id": "push_adjacent_chip", "pressure_zh": "最前推位 + 相邻削血", "pressure_en": "Foremost push + adjacent chip", "target": "foremost", "direction": "front_to_rear", "enemy_sets": [["EN-R1-03", "EN-R1-01"], ["EN-R1-03", "EN-R1-01", "EN-R1-07"], ["EN-R1-03", "EN-R1-01", "EN-R1-11"]]},
6: {"name_zh": "横线追猎", "name_en": "Crossline Pursuit", "mechanic_id": "hinder_changed_target", "pressure_zh": "跨线阻碍 + 换目标追击", "pressure_en": "Cross-line hinder + changed-target pursuit", "target": "rear_then_changed", "direction": "rear_to_front", "enemy_sets": [["EN-R1-09", "EN-R1-10"], ["EN-R1-09", "EN-R1-10", "EN-R1-02"], ["EN-R1-09", "EN-R1-10", "EN-R1-11"]]},
7: {"name_zh": "迟风群带", "name_en": "Latewind Bands", "mechanic_id": "delayed_rear_majority", "pressure_zh": "延迟后风 + 多数职责延迟", "pressure_en": "Delayed rear gust + majority-band delay", "target": "rear_then_majority", "direction": "rear_to_front", "enemy_sets": [["EN-R1-02", "EN-R1-07"], ["EN-R1-02", "EN-R1-07", "EN-R1-10"], ["EN-R1-02", "EN-R1-07", "EN-R1-12"]]},
8: {"name_zh": "棱屏换阵", "name_en": "Prism Order Shift", "mechanic_id": "screen_forced_order", "pressure_zh": "屏障轮转 + 强制换序", "pressure_en": "Screen rotation + forced order shift", "target": "front_then_second", "direction": "front_to_rear", "enemy_sets": [["EN-R1-05", "EN-R1-03"], ["EN-R1-12", "EN-R1-03", "EN-R1-09"], ["EN-R1-12", "EN-R1-03", "EN-R1-11"]]},
9: {"name_zh": "弱轴试场", "name_en": "Spindle Trial", "mechanic_id": "weak_spindle_rehearsal", "pressure_zh": "弱轴反向预演 + 后排暴露", "pressure_en": "Weak-spindle reversal rehearsal + rear exposure", "target": "rear", "direction": "rear_to_front", "enemy_sets": [["EN-R1-02", "EN-R1-10"], ["EN-R1-02", "EN-R1-10", "EN-R1-05"], ["EN-R1-02", "EN-R1-10", "EN-R1-12"]]},
10: {"name_zh": "双端风门", "name_en": "Opposed Windgate", "mechanic_id": "opposed_gust_interrupt", "pressure_zh": "两端阵风 + 施法打断", "pressure_en": "Opposite-end gusts + cast interrupt", "target": "ends_then_caster", "direction": "opposed", "enemy_sets": [["EN-R1-02", "EN-R1-11"], ["EN-R1-02", "EN-R1-11", "EN-R1-09"], ["EN-R1-12", "EN-R1-11", "EN-R1-09"]]},
11: {"name_zh": "裂阵追线", "name_en": "Splitline Hunt", "mechanic_id": "dual_pursuit_split", "pressure_zh": "双目标追击 + 阵线分裂", "pressure_en": "Dual target pursuit + formation split", "target": "front_and_rear", "direction": "opposed", "enemy_sets": [["EN-R1-10", "EN-R1-03"], ["EN-R1-10", "EN-R1-03", "EN-R1-07"], ["EN-R1-10", "EN-R1-03", "EN-R1-12"]]},
12: {"name_zh": "织机终试", "name_en": "Loomward Final", "mechanic_id": "dual_gust_core_race", "pressure_zh": "双向阵风预演 + 核心竞速", "pressure_en": "Two-direction gust rehearsal + core race", "target": "both_ends", "direction": "opposed", "enemy_sets": [["EN-R1-05", "EN-R1-09", "EN-R1-10"], ["EN-R1-12", "EN-R1-09", "EN-R1-10"], ["EN-R1-12", "EN-R1-11", "EN-R1-10"]]},
}
const RECRUIT_OFFERS := {
"P910-M4-H1": {"id": "P910-M4-H1", "market": 4, "offer_index": 0, "generation_seed": "p910-m4-longspoke", "name_zh": "伊拉 · 长辐", "name_en": "Ira Longspoke", "profession_id": "PF-B03", "trait_id": "TR-021", "appearance_seed": 910401, "list_price": 14, "benefit_zh": "贯穿前两名敌人", "benefit_en": "Pierces the first two enemies", "cost_zh": "被推位后需要重整", "cost_en": "Must reset after displacement"},
"P910-M4-H2": {"id": "P910-M4-H2", "market": 4, "offer_index": 1, "generation_seed": "p910-m4-healer", "name_zh": "诺娅 · 暖灯", "name_en": "Noa Warmlamp", "profession_id": "PF-B07", "trait_id": "TR-010", "appearance_seed": 910402, "list_price": 14, "benefit_zh": "治疗生命比例最低者", "benefit_en": "Heals the lowest-health ally", "cost_zh": "直接输出较低", "cost_en": "Low direct damage"},
"P910-M5-H1": {"id": "P910-M5-H1", "market": 5, "offer_index": 0, "generation_seed": "p910-m5-wayrunner", "name_zh": "莱恩 · 逐风", "name_en": "Raine Wayrunner", "profession_id": "PF-B04", "trait_id": "TR-005", "appearance_seed": 910501, "list_price": 15, "benefit_zh": "快速穿越并换目标", "benefit_en": "Crosses quickly and changes target", "cost_zh": "不擅长承受集中攻击", "cost_en": "Poor under focused pressure"},
"P910-M5-H2": {"id": "P910-M5-H2", "market": 5, "offer_index": 1, "generation_seed": "p910-m5-guard", "name_zh": "芬 · 高门", "name_en": "Fen Highgate", "profession_id": "PF-B01", "trait_id": "TR-027", "appearance_seed": 910502, "list_price": 16, "benefit_zh": "稳住前方并产生守势", "benefit_en": "Holds front and generates Guard", "cost_zh": "推进与攻击较慢", "cost_en": "Slow advance and attacks"},
"P910-M7-H1": {"id": "P910-M7-H1", "market": 7, "offer_index": 0, "generation_seed": "p910-m7-rift", "name_zh": "维奥 · 裂星", "name_en": "Vio Riftstar", "profession_id": "PF-B06", "trait_id": "TR-017", "appearance_seed": 910701, "list_price": 16, "benefit_zh": "同时压制两个目标", "benefit_en": "Pressures two targets at once", "cost_zh": "蓄力可能被打断", "cost_en": "Charge can be interrupted"},
"P910-M7-H2": {"id": "P910-M7-H2", "market": 7, "offer_index": 1, "generation_seed": "p910-m7-edge", "name_zh": "亚恩 · 晨刃", "name_en": "Yarn Dawnedge", "profession_id": "PF-B02", "trait_id": "TR-006", "appearance_seed": 910702, "list_price": 15, "benefit_zh": "击倒目标后加快下一击", "benefit_en": "Accelerates after a finishing blow", "cost_zh": "接敌前无法处理远处威胁", "cost_en": "Cannot answer distant threats"},
"P910-M9-H1": {"id": "P910-M9-H1", "market": 9, "offer_index": 0, "generation_seed": "p910-m9-coil", "name_zh": "柯拉 · 针塔", "name_en": "Cora Needlewatch", "profession_id": "PF-B09", "trait_id": "TR-029", "appearance_seed": 910901, "list_price": 17, "benefit_zh": "部署追踪后排的针塔", "benefit_en": "Deploys a rear-tracking needle post", "cost_zh": "装置会被后排弹道摧毁", "cost_en": "Rear projectiles destroy the post"},
"P910-M9-H2": {"id": "P910-M9-H2", "market": 9, "offer_index": 1, "generation_seed": "p910-m9-bond", "name_zh": "露玛 · 三结", "name_en": "Luma Threeknot", "profession_id": "PF-B08", "trait_id": "TR-029", "appearance_seed": 910902, "list_price": 17, "benefit_zh": "不同职责连续行动时提速", "benefit_en": "Accelerates allies across mixed roles", "cost_zh": "单一职责队伍收益低", "cost_en": "Weak in a single-role Company"},
"P910-M11-H1": {"id": "P910-M11-H1", "market": 11, "offer_index": 0, "generation_seed": "p910-m11-bell", "name_zh": "贝尔 · 三拍", "name_en": "Bell Threebeat", "profession_id": "PF-B11", "trait_id": "TR-027", "appearance_seed": 911101, "list_price": 18, "benefit_zh": "延后最长敌方蓄力", "benefit_en": "Delays the longest enemy wind-up", "cost_zh": "需要三次行动才触发", "cost_en": "Requires three actions to trigger"},
"P910-M11-H2": {"id": "P910-M11-H2", "market": 11, "offer_index": 1, "generation_seed": "p910-m11-string", "name_zh": "阿莎 · 云弦", "name_en": "Asha Cloudstring", "profession_id": "PF-B05", "trait_id": "TR-014", "appearance_seed": 911102, "list_price": 17, "benefit_zh": "接敌前射击后方目标", "benefit_en": "Shoots rear targets before contact", "cost_zh": "生命较低且怕后风", "cost_en": "Low vitality under rear gusts"},
}
const EQUIPMENT_MARKETS := {
5: ["EQ-005", "EQ-021"],
8: ["EQ-014", "EQ-023"],
11: ["EQ-019", "EQ-036"],
}
const BOSS := {
"id": BOSS_ID,
"name_zh": "风暴织机",
"name_en": "Gale Loom",
"max_hp": 1760,
"timeout_tick": 1800,
"footprints": 2,
"initial_gust_direction": "front_to_rear",
"gust_cadence": 10,
"weak_spindle_family": "projectile",
"half_vitality_opposed_gusts": true,
"recovery_cost": 16,
"plans": [
{"id": "break_spindle", "name_zh": "破轴逆风", "name_en": "Break the Spindle", "summary_zh": "用投射攻击弱轴,让下一阵风反穿敌方并打开短核心窗口", "summary_en": "Strike the weak spindle with a projectile to reverse the next gust through enemies"},
{"id": "hold_line", "name_zh": "固线攻核", "name_en": "Hold the Line", "summary_zh": "保持队序并持续攻击核心,但后排承受更强风压", "summary_en": "Preserve order and race the core while the rear absorbs stronger gust pressure"},
],
}
static func snapshot(selected_claim_id: String) -> Dictionary:
var previous: Dictionary = P99Content.run_snapshot(selected_claim_id)
var equipment: Dictionary = Dictionary(previous.equipment).duplicate(true)
var routes := _build_routes()
var equipment_offers := _build_equipment_offers(selected_claim_id, equipment)
var boards := _build_boards(selected_claim_id, routes, equipment_offers)
var result := {
"revision": REVISION,
"predecessor_hash": String(previous.content_hash),
"selected_claim_id": selected_claim_id,
"professions": Dictionary(previous.professions).duplicate(true),
"traits": Dictionary(previous.traits).duplicate(true),
"equipment": equipment,
"enemies": Dictionary(previous.enemies).duplicate(true),
"recruit_offers": RECRUIT_OFFERS.duplicate(true),
"equipment_offers": equipment_offers,
"routes": routes,
"route_order": _route_order(),
"boards": boards,
"bosses": {BOSS_ID: BOSS.duplicate(true)},
}
result["content_hash"] = _hash(result)
return result
static func validate(content: Dictionary) -> Dictionary:
var errors: Array[String] = []
var required := ["revision", "predecessor_hash", "selected_claim_id", "professions", "traits", "equipment", "enemies", "recruit_offers", "equipment_offers", "routes", "route_order", "boards", "bosses", "content_hash"]
for key in required:
if not content.has(key):
errors.append("missing:%s" % key)
if not errors.is_empty():
return {"ok": false, "errors": errors}
if String(content.selected_claim_id) not in P99Content.CHARTER_CLAIMS:
errors.append("claim")
if content.recruit_offers.size() != 10 or content.equipment_offers.size() != 6 or content.routes.size() != 27 or content.boards.size() != 9 or content.bosses.size() != 1:
errors.append("counts")
if content.route_order != _route_order():
errors.append("route_order")
for market in range(4, 13):
if not content.boards.has(market):
errors.append("board_missing:%d" % market)
continue
var board: Dictionary = content.boards[market]
if int(board.get("market", 0)) != market or Array(board.get("battle_offer_ids", [])).size() != 3:
errors.append("board_shape:%d" % market)
for risk in range(3):
var route_id := "P910-R%d-Q%d" % [market, risk]
if route_id not in board.battle_offer_ids or not content.routes.has(route_id):
errors.append("route_missing:%s" % route_id)
continue
var route: Dictionary = content.routes[route_id]
if int(route.round) != market or int(route.risk) != risk or int(route.reward) <= 0 or String(route.mechanic_id).is_empty():
errors.append("route_identity:%s" % route_id)
for enemy_id in route.enemy_ids:
if not content.enemies.has(enemy_id):
errors.append("enemy_missing:%s" % enemy_id)
for offer_id in content.recruit_offers:
var row: Dictionary = content.recruit_offers[offer_id]
if String(row.id) != offer_id or int(row.market) not in [4, 5, 7, 9, 11] or String(row.profession_id) not in content.professions or String(row.trait_id) not in content.traits:
errors.append("recruit:%s" % offer_id)
for offer_id in content.equipment_offers:
var row: Dictionary = content.equipment_offers[offer_id]
if String(row.id) != offer_id or int(row.market) not in [5, 8, 11] or String(row.catalog_id) not in content.equipment:
errors.append("equipment:%s" % offer_id)
var boss: Dictionary = content.bosses.get(BOSS_ID, {})
if int(boss.get("footprints", 0)) != 2 or int(boss.get("recovery_cost", 0)) != 16 or Array(boss.get("plans", [])).map(func(row): return String(row.id)) != ["break_spindle", "hold_line"]:
errors.append("boss")
if int(boss.get("max_hp", 0)) != 1760 or int(boss.get("timeout_tick", 0)) != 1800 or int(boss.get("gust_cadence", 0)) != 10:
errors.append("boss_timing")
if String(boss.get("initial_gust_direction", "")) != "front_to_rear" or String(boss.get("weak_spindle_family", "")) != "projectile" or not bool(boss.get("half_vitality_opposed_gusts", false)):
errors.append("boss_rules")
var canonical := content.duplicate(true)
var actual_hash := String(canonical.get("content_hash", ""))
canonical.erase("content_hash")
if actual_hash != _hash(canonical):
errors.append("content_hash")
return {"ok": errors.is_empty(), "errors": errors}
static func _build_routes() -> Dictionary:
var result := {}
var suffix_zh := ["稳行", "急行", "险行"]
var suffix_en := ["Survey", "Press", "Hazard"]
for round_index in range(4, 13):
var blueprint: Dictionary = ROUND_BLUEPRINTS[round_index]
for risk in range(3):
var route_id := "P910-R%d-Q%d" % [round_index, risk]
result[route_id] = {
"id": route_id,
"round": round_index,
"risk": risk,
"seed": 910000 + round_index * 10 + risk,
"reward": 12 + round_index * 2 + risk * 5,
"name_zh": "%s · %s" % [String(blueprint.name_zh), suffix_zh[risk]],
"name_en": "%s · %s" % [String(blueprint.name_en), suffix_en[risk]],
"enemy_ids": Array(blueprint.enemy_sets[risk]).duplicate(),
"pressure_zh": String(blueprint.pressure_zh),
"pressure_en": String(blueprint.pressure_en),
"mechanic_id": String(blueprint.mechanic_id),
"arrival_tick": maxi(0, 3 - risk),
"selected_target": String(blueprint.target),
"gust_cadence": maxi(6, 12 - risk - int(round_index / 4)),
"gust_direction": String(blueprint.direction),
"gust_damage": 3 + risk * 2 + int((round_index - 4) / 4),
}
return result
static func _build_equipment_offers(selected_claim_id: String, equipment: Dictionary) -> Dictionary:
var result := {}
for market in [5, 8, 11]:
var catalogs: Array = Array(EQUIPMENT_MARKETS[market]).duplicate()
if market == 8 and selected_claim_id == "CH-RG01-CATALOG-01":
catalogs[1] = "EQ-041"
for index in catalogs.size():
var catalog_id := String(catalogs[index])
var item: Dictionary = equipment[catalog_id]
var offer_id := "P910-M%d-E%d" % [market, index + 1]
result[offer_id] = {
"id": offer_id,
"market": market,
"offer_index": index,
"catalog_id": catalog_id,
"list_price": int(item.get("list_price", 12)) + int((market - 5) / 3),
"name_zh": String(item.name_zh),
"name_en": String(item.name_en),
"behavior": String(item.behavior),
"benefit_zh": String(item.get("benefit_zh", item.behavior)),
"benefit_en": String(item.get("benefit_en", item.behavior)),
"cost_zh": String(item.get("cost_zh", "替换当前装备")),
"cost_en": String(item.get("cost_en", "Replaces the current item")),
}
return result
static func _build_boards(selected_claim_id: String, routes: Dictionary, equipment_offers: Dictionary) -> Dictionary:
var result := {}
for market in range(4, 13):
var recruits: Array[String] = []
for offer_id in RECRUIT_OFFERS:
if int(RECRUIT_OFFERS[offer_id].market) == market:
recruits.append(offer_id)
recruits.sort_custom(func(left: String, right: String): return int(RECRUIT_OFFERS[left].offer_index) < int(RECRUIT_OFFERS[right].offer_index))
var items: Array[String] = []
for offer_id in equipment_offers:
if int(equipment_offers[offer_id].market) == market:
items.append(offer_id)
items.sort_custom(func(left: String, right: String): return int(equipment_offers[left].offer_index) < int(equipment_offers[right].offer_index))
var battle_ids: Array[String] = []
for risk in range(3):
battle_ids.append("P910-R%d-Q%d" % [market, risk])
var board := {
"id": "P910-MARKET-%d" % market,
"market": market,
"recruit_offer_ids": recruits,
"equipment_offer_ids": items,
"battle_offer_ids": battle_ids,
"allows_final_reorder": market == 12,
"selected_claim_id": selected_claim_id,
}
board["board_hash"] = _hash(board)
result[market] = board
return result
static func _route_order() -> Array[String]:
var result: Array[String] = []
for round_index in range(4, 13):
for risk in range(3):
result.append("P910-R%d-Q%d" % [round_index, risk])
return result
static func _hash(value: Variant) -> String:
return JSON.stringify(value).sha256_text()