235 lines
17 KiB
GDScript
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()
|