Files
aetherbound-guild/tools/build_fg4_rg04_catalog.py
T

491 lines
30 KiB
Python

#!/usr/bin/env python3
"""Build the bilingual RG-04 runtime catalog from accepted design authority."""
from __future__ import annotations
import hashlib
import json
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SOURCE = ROOT / "prototype/site/data/catalog.json"
OUTPUT = ROOT / "runtime/fg4/rg04_catalog.json"
EQUIPMENT_ZH = {
"EQ-121": ("护卫转专注阀", "首次溢出的护卫转化为专注。", "佩戴者下一节拍无法获得护卫。"),
"EQ-122": ("药剂瓶架", "消耗药剂会刷新最早进入冷却的治疗装备。", "装备治疗不能生成药剂。"),
"EQ-123": ("零件回收器", "友方装置被摧毁时,向佩戴者返还 1 点零件。", "若装置持续至完整时长,佩戴者会陷入迟滞。"),
"EQ-124": ("诗律杯", "消耗诗律时,向速度更慢的相邻单位转交 1 点本地资源。", "佩戴者失去下一次来自该相邻单位的诗律。"),
"EQ-125": ("锋刃剑鞘", "目标被击败时,未消耗的锋刃会转移给最近目标并施加标记。", "被标记目标同时获得护卫。"),
"EQ-126": ("定势配重", "遭受强制位移时消耗定势以取消位移并保持目标。", "所有剩余定势都会失去。"),
"EQ-127": ("动量瓶", "消耗动量时同时刷新最早的位置触发器。", "佩戴者下一节拍无法获得动量。"),
"EQ-128": ("瞄准分流器", "对最后方目标消耗瞄准时,同时标记最前方目标。", "这次射击不能施加其他状态。"),
"EQ-129": ("蓄能槽", "被打断的蓄能转入装备,并在下次普通攻击后返还。", "返还蓄能有上限,且佩戴者会陷入迟滞。"),
"EQ-130": ("羁绊计数牌", "连接断裂时,将 1 点羁绊转为原双方的护卫。", "该连接一节拍内不能重建。"),
"EQ-131": ("预兆沙漏", "消耗预兆会暂停目标负面状态在下一次蓄力期间的到期。", "该窗口内目标不能再获得其他负面状态。"),
"EQ-132": ("补给箱钥匙", "消耗补给会立即解除一个相邻装备的冷却。", "同一装备在下一节拍会被锁定。"),
"EQ-133": ("空槽鼓", "本地资源降至零时,提前下一次普通攻击。", "普通攻击前获得资源会取消提前并造成迟滞。"),
"EQ-134": ("满槽封印", "本地资源满值时,下一名敌人不能偷取或削减它。", "该敌人行动结束前佩戴者不能消耗资源。"),
"EQ-135": ("邻位借贷券", "每场一次,向拥有相同资源类型的相邻单位借出 1 点本地资源。", "一节拍内未消耗时,双方各失去 1 点该资源。"),
"EQ-136": ("混合钱袋", "队伍首次获得三种不同资源时生成 1 点补给。", "完成前重复同类资源会重置计数。"),
"EQ-137": ("资源制动器", "直接命中不能把本地资源降到 1 以下。", "制动触发时佩戴者会暴露。"),
"EQ-138": ("溢流嘴", "资源在上限时继续获得,会流向可接收同类资源的相邻单位。", "没有合法接收者时佩戴者会迟滞。"),
"EQ-139": ("逆向账册", "受伤获得的资源下次优先用于敌方;自身行动获得的资源下次优先用于友方。", "只能作用于错误一侧的职业行动无法消耗该资源。"),
"EQ-140": ("末枚令牌坠", "消耗最后一点资源时保留至行动结算,使附带效果仍可触发。", "保留资源随后消失且不能返还。"),
"EQ-141": ("中点净化器", "状态持续过半后净化会移除状态并获得护卫。", "过早净化会把状态改为较短的迟滞。"),
"EQ-142": ("标记接力针", "标记到期时跳到最近的未标记敌人。", "跳转后的标记只持续 1 节拍。"),
"EQ-143": ("迟滞钩", "命中迟滞目标时把它向前拉 1 位。", "拉动会移除迟滞。"),
"EQ-144": ("暴露丝线", "佩戴者护卫破裂时标记破坏者,并使自己暴露。", "暴露结束时标记也会结束。"),
"EQ-145": ("空白石板", "同一节拍首次获得的正面与负面标签互相抵消。", "佩戴者不会从两者获得本地资源。"),
"EQ-146": ("久病符", "重复施加同一负面状态会刷新时长,但不叠加强度。", "状态到期后目标获得 1 节拍免疫。"),
"EQ-147": ("短祝福", "正面标签只持续 1 节拍,但到期附带效果会立即触发。", "该标签不能延长。"),
"EQ-148": ("状态棱镜", "佩戴者首次获得标签时,在合法情况下把相反符号复制给最近敌人。", "原标签保持不变。"),
"EQ-149": ("断根刀", "对根系来源的普通攻击会解除佩戴者的移动锁定。", "该次普通攻击不造成伤害或状态。"),
"EQ-150": ("静音铃舌", "对声音使用者施加迟滞时,同时打断其当前蓄力。", "迟滞时长缩短为 1 节拍。"),
"EQ-151": ("护卫毒瓶", "佩戴者有护卫时施加的负面状态,1 节拍内不能被净化。", "施加时消耗佩戴者全部护卫。"),
"EQ-152": ("开放伤口贴", "目标尝试接受治疗时,禁疗标签会揭示并标记其来源。", "这次治疗仍然会被阻止。"),
"EQ-153": ("到期铃", "最早的标签到期时,提前佩戴者下一次行动。", "主动净化不会触发铃声。"),
"EQ-154": ("净化镜", "敌方首次净化时,也会移除佩戴者最早的负面状态。", "敌方净化仍会完整生效。"),
"EQ-155": ("标记路线图", "朝被标记敌人移动会保持标记时长。", "远离目标会移除标记。"),
"EQ-156": ("状态分割器", "范围行动只对首尾目标施加选定状态。", "中间目标不会获得该状态。"),
"EQ-157": ("双净化法则", "同一友方第二次被净化时,同时刷新一个装备冷却。", "该友方第一次净化会延迟 1 节拍。"),
"EQ-158": ("传染隔栏", "阻止状态跨过佩戴者在相邻单位间传播。", "佩戴者会以较短时长承受两次被阻止的传播标签。"),
"EQ-159": ("真相药杯", "净化隐匿时会揭示目标下一次行动类别。", "目标在揭示后获得护卫。"),
"EQ-160": ("恒久圣甲虫", "首次完整持续到期的负面状态,会转化为本场对该标签的免疫。", "该免疫也会阻止友方把该标签转为增益。"),
}
EQUIPMENT_TAGS = {
121: ["guard_conversion", "focus_timing", "guard_lock"],
122: ["remedy_spend", "healing_cooldown", "heal_source_limit"],
123: ["device_salvage", "parts_return", "duration_hinder"],
124: ["verse_spend", "neighbor_resource", "tempo_debt"],
125: ["edge_transfer", "nearest_mark", "enemy_guard"],
126: ["poise_spend", "movement_cancel", "resource_empty"],
127: ["momentum_spend", "position_refresh", "cadence_lock"],
128: ["aim_spend", "endpoint_mark", "status_exclusion"],
129: ["charge_store", "basic_return", "hinder_return"],
130: ["broken_link", "bond_conversion", "relink_lock"],
131: ["omen_spend", "expiry_pause", "hindrance_lock"],
132: ["supply_spend", "adjacent_cooldown", "followup_lock"],
133: ["empty_resource", "basic_advance", "gain_cancel"],
134: ["full_resource", "drain_immunity", "spend_lock"],
135: ["neighbor_loan", "matching_resource", "loan_expiry"],
136: ["resource_diversity", "supply_creation", "repeat_reset"],
137: ["resource_floor", "direct_hit", "exposed_cost"],
138: ["resource_overflow", "neighbor_transfer", "no_target_hinder"],
139: ["resource_source", "target_inversion", "side_restriction"],
140: ["final_resource", "rider_enable", "no_refund"],
141: ["midpoint_cleanse", "guard_reward", "early_hinder"],
142: ["mark_expiry", "nearest_relay", "one_tick_mark"],
143: ["hindered_target", "forward_pull", "cleanse_on_pull"],
144: ["guard_break", "source_mark", "linked_exposed"],
145: ["opposite_tags", "same_cadence_cancel", "resource_denial"],
146: ["hindrance_refresh", "no_strength_stack", "expiry_immunity"],
147: ["short_positive", "expiry_rider", "no_extension"],
148: ["first_status", "opposite_copy", "source_preserved"],
149: ["root_source", "movement_cleanse", "zero_damage_basic"],
150: ["sound_interrupt", "hinder_apply", "short_duration"],
151: ["guarded_hindrance", "cleanse_lock", "guard_spend"],
152: ["healing_denial", "source_reveal", "attempt_mark"],
153: ["oldest_expiry", "action_advance", "manual_cleanse_excluded"],
154: ["enemy_cleanse", "mirrored_cleanse", "enemy_effect_preserved"],
155: ["marked_target", "direction_choice", "mark_duration"],
156: ["area_status", "endpoint_targets", "middle_exclusion"],
157: ["second_cleanse", "item_refresh", "first_cleanse_delay"],
158: ["status_spread", "neighbor_barrier", "shortened_backlash"],
159: ["conceal_cleanse", "action_reveal", "target_guard"],
160: ["full_duration", "tag_immunity", "conversion_block"],
}
ARTIFACT_ZH = {
"AR-023": ("清澈流向", "双方首次施加的负面状态只持续 1 节拍,且不能延长。", "与 AR-024 冲突;中立规则条会标出已消耗的首个标签。"),
"AR-024": ("深层流向", "双方首次施加的负面状态在持续中点前不能被净化。", "与 AR-023 冲突;状态环会明确显示中点锁。"),
"AR-025": ("透明屏障", "所有屏障少阻挡一次行动,但阻挡时会揭示来源与下一目标。", "与 AR-026 冲突;界面显示剩余次数和揭示卡。"),
"AR-026": ("不透明屏障", "屏障多阻挡一次行动;存在期间隐藏拥有者的下一目标。", "与 AR-025 冲突;隐藏目标会明确标为遗物代价。"),
"AR-027": ("共享节奏", "每个节拍边界,双方最快普通攻击都等待各自较慢的相邻单位。", "与 AR-028 冲突;成对时钟显示受影响行动。"),
"AR-028": ("破碎节律", "重复同一行动节奏带时,会提前对方最慢单位。", "与 AR-027 冲突;重复计数与受益者会提前显示。"),
"AR-029": ("落地竞逐", "双方腾空与隐匿窗口缩短 1 节拍,强制位移效果延长 1 节拍。", "与 AR-030 冲突;预览标签显示全部时长变化。"),
"AR-030": ("掠过竞逐", "双方首次强制位移无视抗性,腾空窗口延长 1 节拍。", "与 AR-029 冲突;首次尝试标记与飞行时钟保持可见。"),
}
ARTIFACT_TAGS = {
23: ["first_hindrance", "one_tick_duration", "no_extension"],
24: ["first_hindrance", "midpoint_lock", "cleanse_delay"],
25: ["screen_count_down", "source_reveal", "target_reveal"],
26: ["screen_count_up", "target_hidden", "declared_cost"],
27: ["cadence_boundary", "fast_waits", "neighbor_sync"],
28: ["cadence_repeat", "enemy_advance", "recipient_forecast"],
29: ["airborne_shorter", "conceal_shorter", "movement_longer"],
30: ["first_movement_open", "airborne_longer", "resistance_disabled"],
}
ENEMY_ZH = {
"EN-R4-01": ("页边蛾", "以较低效果为敌方复制最近一次装备触发,并沿用同类合法目标。", "先喂给它无害触发,或在关键触发前击败它。", "出现三种不同触发后,复制恢复完整时序。"),
"EN-R4-02": ("根系索引员", "给友方位置标记 A/B/C;无论移动与否,攻击都会转向下一个有人占据的标签。", "把护卫移入下一标签,或在循环抵达脆弱区前击败它。", "空标签会让指针一次前进两格。"),
"EN-R4-03": ("擦除蛞蝓", "移除敌方最新的标记或迟滞,并在来源位置留下墨迹。", "最后施加一个可牺牲标签,或在其待命时改用直接伤害。", "墨迹会缩短该位置之后的标签时长。"),
"EN-R4-04": ("脚注豺", "在最慢友方行动后立刻行动,并把该友方作为目标。", "保护慢速单位,或提前、取消那次慢行动。", "连续攻击同一目标时获得额外射程。"),
"EN-R4-05": ("条款甲虫", "预览声明重复或交替;遵守会给甲虫护卫,违背会使其暴露但让行动者迟滞。", "用耐久单位违背,或遵守到终结技就绪。", "每次暴露后条款会切换。"),
"EN-R4-06": ("叶片计数器", "统计目标切换;第三次切换会把该友方行动改向最前方。", "固定目标,或用灵活的范围行动主动触发。", "计数不会因敌人死亡而清除。"),
"EN-R4-07": ("引文鸦", "标记每个正面标签的来源;标签到期时若可达便啄击来源。", "由受保护来源施加短标签,或净化乌鸦的来源标记。", "多个到期事件会排成独立啄击队列。"),
"EN-R4-08": ("装订藤", "连接双方最前方单位;任一方推拉时,另一方在合法情况下镜像移动。", "避免位移,或利用镜像把敌方前排拉出。", "非法镜像会使发起者迟滞。"),
"EN-R4-09": ("改稿影", "首次落空的行动会被它复制,并作用于己方等价目标。", "避免落空,或故意让恢复、无害行动落空。", "完成复制后,它会进入阵线射程。"),
"EN-R4-10": ("书架守卫", "保护其正后方敌人;该敌人移动时,守卫换位并保护下一个。", "把受保护目标拉动两格,或攻击守卫、使用绕过。", "每次完成换位都会获得护卫。"),
"EN-R4-11": ("术语守护者", "声明一个已定义行动标签;只有该标签能打断它,但每次使用也会加速大招。", "保留一次定义标签打断,或用其他标签竞速并承受施法。", "每次被打断后,定义标签都会变化。"),
"EN-R4-12": ("重写雄鹿", "记录前三次友方行动,然后以相反顺序把其目标规则作为敌方攻击重放。", "预先写入安全目标规则,或在反向重放前调整顺序。", "半血后记录四次行动,但所有槽位仍会显示。"),
}
ENEMY_TAGS = {
1: ["copy_equipment", "safe_trigger", "copy_timing"],
2: ["position_labels", "next_occupied", "cycle_interrupt"],
3: ["cleanse_newest", "ink_source", "raw_damage"],
4: ["slowest_followup", "source_guard", "action_cancel"],
5: ["clause_choice", "obey_guard", "break_expose"],
6: ["target_change", "third_redirect", "stable_target"],
7: ["positive_source", "expiry_attack", "source_cleanse"],
8: ["front_link", "mirror_movement", "illegal_hinder"],
9: ["fizzle_copy", "safe_seed", "equivalent_target"],
10: ["rear_protect", "swap_guard", "bypass"],
11: ["defined_tag", "interrupt_only", "cast_acceleration"],
12: ["action_record", "reverse_replay", "safe_target_seed"],
}
BOSS_ROWS = {
"BO-R4-01": {
"name": ("The Walking Index", "行走索引"),
"rule": ("Labels every position and advances a pointer after each action; erase, copy, bind or strike resolves from the current label.", "标记每个位置,并在每次行动后推进指针;当前标签决定擦除、复制、束缚或打击。"),
"responses": ("Steer action count onto a safe label, or interrupt to freeze the pointer for one action.", "操纵行动次数让指针落到安全标签,或打断并冻结指针一次行动。"),
"failure": ("An unplanned action crossed the disclosed dangerous label.", "计划外行动越过了已公开的危险标签。"),
"phase": ("Below half vitality, equipment triggers move the pointer backward.", "半血后,装备触发会让指针反向移动。"),
"tags": ["action_count", "safe_label", "pointer_interrupt"],
},
"BO-R4-02": {
"name": ("Redaction Hart", "删节雄鹿"),
"rule": ("Records four allied rules, redacts one target clause from each, then replays them against equivalent targets.", "记录四条友方规则,从每条中涂黑一个目标条款,再对等价目标重放。"),
"responses": ("Seed rules whose missing clause stays harmless, or reorder and interrupt the most dangerous replay.", "预置删去条款后仍无害的规则,或在重放前重排并打断最危险的一条。"),
"failure": ("A seeded action became a hostile target rule after redaction.", "预置行动在删节后变成了敌对目标规则。"),
"phase": ("Below half vitality, six rules are recorded while the replay order remains visible.", "半血后记录六条规则,但重放顺序始终可见。"),
"tags": ["safe_rule_seed", "replay_order", "danger_interrupt"],
},
}
MARKET_NAMES = {
"CTR-R4-01": [
("First Margin Copy", "初次页边复制"), ("Root Label Walk", "根系标签道"),
("Erasure Shelf", "擦除书架"), ("Footnote Turn", "脚注转角"),
("Clause Stamp", "条款印章"), ("Leaf Count", "叶片计数"),
("Citation Aisle", "引文长廊"), ("Binding Shelf", "装订书架"),
("Redraft Hall", "改稿大厅"), ("Warden Stack", "守卫书垛"),
("Glossary Gate", "术语门"), ("Walking Index", "行走索引"),
],
"CTR-R4-02": [
("Redacted Entry", "删节入口"), ("Four-Rule Draft", "四规则草稿"),
("Black Clause", "黑条款"), ("Reverse Target", "反向目标"),
("Ink Antler", "墨角"), ("Preserve Trap", "保持陷阱"),
("Alternate Replay", "交替重放"), ("Cleanse Decoy", "净化诱饵"),
("Six-Slot Record", "六槽记录"), ("Dangerous Entry", "危险条目"),
("Final Redaction", "最终删节"), ("Hart's Ledger", "雄鹿账册"),
],
}
CLAUSE_KINDS = ["repeat", "alternate", "preserve"]
AFFECTED_FIELDS = ["action_tag", "target_rule", "status_source", "equipment_trigger"]
PRESSURE_FAMILIES = ["copy", "redirect", "cleanse_trap", "clause"]
PRESSURE_LABEL_ZH = {
"copy": "复制",
"redirect": "重定向",
"cleanse_trap": "净化陷阱",
"clause": "条款判定",
}
PRESSURE_COPY = {
"copy": (
"The disclosed source action will be copied at reduced strength onto the same legal target class.",
"已公开的来源行动会以较低强度复制到同类合法目标。",
["feed_safe_source", "deny_copy", "copy_window"],
),
"redirect": (
"The disclosed target rule will redirect to the next indexed position when its pointer resolves.",
"指针结算时,已公开的目标规则会重定向到下一个索引位置。",
["steer_pointer", "guard_next_index", "redirect_window"],
),
"cleanse_trap": (
"The first cleanse records its source and replays the removed tag on an equivalent target.",
"首次净化会记录来源,并把被移除标签重放到等价目标。",
["bait_cleanse", "raw_response", "source_protection"],
),
"clause": (
"The archive judges the declared action field after the visible clause counter completes.",
"可见条款计数完成后,档案会判定已声明的行动字段。",
["obey_clause", "break_clause", "counter_timing"],
),
}
def localized(en: str, zh: str) -> dict[str, str]:
return {"en": en, "zh_CN": zh}
def numeric_id(identifier: str) -> int:
return int(identifier.rsplit("-", 1)[-1])
def fit_tags(raw: str) -> list[str]:
values = []
for part in raw.replace("`", "").split(";"):
value = re.sub(r"[^a-z0-9]+", "_", part.strip().lower()).strip("_")
values.append(value)
return values
def source_rows(catalog: dict, family: str, prefix: str, first: int, last: int) -> list[dict]:
return [
row for row in catalog[family]
if row["id"].startswith(prefix) and first <= numeric_id(row["id"]) <= last
]
def equipment_rows(catalog: dict) -> list[dict]:
rows = []
for source in source_rows(catalog, "equipment", "EQ-", 121, 160):
identifier = source["id"]
name_zh, effect_zh, tradeoff_zh = EQUIPMENT_ZH[identifier]
rows.append({
"id": identifier,
"name": localized(source["name"], name_zh),
"fit": fit_tags(source["fit"]),
"class": source["class"],
"effect": localized(source["effect"], effect_zh),
"tradeoff": localized(source["tradeoff"], tradeoff_zh),
"preview": localized(
f"{source['effect']} Plan around this cost: {source['tradeoff']}",
f"{effect_zh}需要同时承担:{tradeoff_zh}",
),
"response_tags": EQUIPMENT_TAGS[numeric_id(identifier)],
})
return rows
def artifact_rows(catalog: dict) -> list[dict]:
conflicts = {23: 24, 24: 23, 25: 26, 26: 25, 27: 28, 28: 27, 29: 30, 30: 29}
rows = []
for source in source_rows(catalog, "artifacts", "AR-", 23, 30):
identifier = source["id"]
value = numeric_id(identifier)
name_zh, rule_zh, tradeoff_zh = ARTIFACT_ZH[identifier]
rows.append({
"id": identifier,
"name": localized(source["name"], name_zh),
"rule": localized(source["rule"], rule_zh),
"tradeoff": localized(source["tradeoff"], tradeoff_zh),
"conflict_id": f"AR-{conflicts[value]:03d}",
"preview": localized(
f"Apply this run law symmetrically: {source['rule']} Cost: {source['tradeoff']}",
f"该规则对双方对称生效:{rule_zh}代价:{tradeoff_zh}",
),
"response_tags": ARTIFACT_TAGS[value],
})
return rows
def enemy_rows(catalog: dict) -> list[dict]:
rows = []
for source in source_rows(catalog, "enemies", "EN-R4-", 1, 12):
identifier = source["id"]
name_zh, rule_zh, responses_zh, escalation_zh = ENEMY_ZH[identifier]
escalation_en = source["production"].split(" P/A/V/S", 1)[0]
rows.append({
"id": identifier,
"name": localized(source["name"], name_zh),
"class": source["class"],
"rule": localized(source["rule"], rule_zh),
"responses": localized(source["responses"], responses_zh),
"escalation": localized(escalation_en, escalation_zh),
"preview": localized(
f"Rule: {source['rule']} Responses: {source['responses']}",
f"规则:{rule_zh}应对:{responses_zh}",
),
"response_tags": ENEMY_TAGS[numeric_id(identifier)],
})
return rows
def boss_rows() -> list[dict]:
rows = []
for identifier, source in BOSS_ROWS.items():
rows.append({
"id": identifier,
"name": localized(*source["name"]),
"rule": localized(*source["rule"]),
"responses": localized(*source["responses"]),
"failure": localized(*source["failure"]),
"phase_change": localized(*source["phase"]),
"executable": True,
"preview": localized(
f"Inspect the disclosed rule, response timing and failure boundary before the fixed Muster.",
"进入固定集结前,查看已公开的规则、应对时机与失败边界。",
),
"response_tags": source["tags"],
})
return rows
def clause(contract_index: int, market_index: int) -> dict:
kind = CLAUSE_KINDS[(market_index + contract_index - 2) % len(CLAUSE_KINDS)]
field = AFFECTED_FIELDS[(market_index + 2 * contract_index - 3) % len(AFFECTED_FIELDS)]
kind_zh = {"repeat": "重复", "alternate": "交替", "preserve": "保持"}[kind]
field_zh = {
"action_tag": "行动标签", "target_rule": "目标规则",
"status_source": "状态来源", "equipment_trigger": "装备触发",
}[field]
field_en = field.replace("_", " ")
if kind == "repeat":
obey_en = f"Repeat the disclosed {field_en} twice to expose the archive source."
break_en = f"Change {field_en} before the second use to expose the judging enemy instead."
obey_zh = f"连续两次使用已公开的{field_zh},以暴露档案来源。"
break_zh = f"第二次前更换{field_zh},改为暴露判定敌人。"
elif kind == "alternate":
obey_en = f"Alternate the disclosed {field_en} until the counter resolves to gain a safe target window."
break_en = f"Repeat {field_en} deliberately to shorten the enemy copy window at a Guard cost."
obey_zh = f"交替使用已公开的{field_zh}直到计数结算,获得安全目标窗口。"
break_zh = f"主动重复{field_zh},以护卫代价缩短敌方复制窗口。"
else:
obey_en = f"Preserve the disclosed {field_en} until resolution to lock the current legal target."
break_en = f"Change {field_en} once to redirect the archive effect onto the next indexed target."
obey_zh = f"保持已公开的{field_zh}直至结算,锁定当前合法目标。"
break_zh = f"更换一次{field_zh},把档案效果重定向到下一索引目标。"
return {
"id": f"CL-R4-C{contract_index:02d}-M{market_index:02d}",
"kind": kind,
"affected_field": field,
"obey_result": localized(obey_en, obey_zh),
"break_result": localized(break_en, break_zh),
"counter_limit": 2 + ((market_index - 1) % 3),
}
def enemy_sets(market_index: int, contract_index: int) -> list[list[str]]:
start = (market_index * 2 + contract_index * 3) % 12
ids = [f"EN-R4-{((start + offset) % 12) + 1:02d}" for offset in range(8)]
return [ids[0:3], ids[2:5], [ids[1], ids[4], ids[6], ids[7]]]
def market_rows(contract_id: str) -> list[dict]:
contract_index = 1 if contract_id == "CTR-R4-01" else 2
rows = []
for market_index, (name_en, name_zh) in enumerate(MARKET_NAMES[contract_id], start=1):
pressure_family = PRESSURE_FAMILIES[(market_index + contract_index - 2) % 4]
pressure_en, pressure_zh, family_tags = PRESSURE_COPY[pressure_family]
pressure_label_zh = PRESSURE_LABEL_ZH[pressure_family]
clause_row = clause(contract_index, market_index)
clause_kind = clause_row["kind"]
clause_zh = {"repeat": "重复", "alternate": "交替", "preserve": "保持"}[clause_kind]
item_first = 121 + ((market_index - 1 + (contract_index - 1) * 17) % 40)
item_second = 121 + ((market_index + 12 + contract_index * 3) % 40)
if item_second == item_first:
item_second = 121 + ((item_second - 120) % 40)
copy_id = f"CP-R4-C{contract_index:02d}-M{market_index:02d}"
rows.append({
"market_index": market_index,
"round_index": market_index,
"line_capacity": 10,
"offer_level": 1 + (market_index - 1) // 3,
"name": localized(name_en, name_zh),
"pressure": localized(pressure_en, pressure_zh),
"failure": localized(
f"The committed response ignored the disclosed {clause_kind} clause and {pressure_family} resolution.",
f"已提交应对忽略了公开的{clause_zh}条款与{pressure_label_zh}结算。",
),
"preview": localized(
f"Obey the {clause_kind} clause for its safe result, or break it deliberately while answering {pressure_family} pressure.",
f"遵守{clause_zh}条款取得安全结果,或在处理{pressure_label_zh}压力时主动违背。",
),
"milestone": localized(
f"Contract {contract_index} archive decision {market_index} locks one disclosed clause and copy source.",
f"第 {contract_index} 份契约第 {market_index} 次档案决策会锁定一条公开条款与复制来源。",
),
"response_tags": [*family_tags, f"{clause_kind}_clause"],
"required_equipment_ids": [f"EQ-{item_first:03d}", f"EQ-{item_second:03d}"],
"archive_clause": clause_row,
"copy_disclosure": {
"id": copy_id,
"pressure_family": pressure_family,
"source_rule": AFFECTED_FIELDS[(market_index + contract_index) % 4],
"target_rule": ["foremost", "rearmost", "same_index", "next_occupied"][(market_index - 1) % 4],
"warning_ticks": 14 + (market_index % 4) * 2,
"resolve_tick": 42 + market_index + contract_index * 3,
"fixed_tick_ms": 100,
},
"enemy_sets": enemy_sets(market_index, contract_index),
})
return rows
def main() -> int:
source_bytes = SOURCE.read_bytes()
catalog = json.loads(source_bytes)
output = {
"schema": 1,
"contract_revision": "fg4-midgame-r1",
"source": {
"prototype_catalog_sha256": hashlib.sha256(source_bytes).hexdigest(),
"region_id": "RG-04",
},
"region4": {
"id": "RG-04",
"name": localized("Quillroot Archive", "根羽档案馆"),
"preview": localized(
"Restore public navigation copies by inspecting copied actions, redirects, cleanse traps and archive clauses.",
"查看复制行动、目标重定向、净化陷阱与档案条款,修复公共导航副本。",
),
"response_tags": ["copy_inspection", "target_redirect", "archive_clause"],
},
"contract3": {
"id": "CTR-R4-01", "region_id": "RG-04", "market_start": 1,
"market_end": 12, "boss_market": 12, "boss_id": "BO-R4-01",
"name": localized("First Quillroot Contract", "根羽第一契约"),
"preview": localized(
"Carry the RG-03 Company through twelve disclosed copy-and-clause Rounds before The Walking Index.",
"沿用 RG-03 队伍,完成十二轮公开复制与条款挑战,抵达“行走索引”。",
),
"response_tags": ["same_company", "clause_inspection", "walking_index"],
},
"contract4": {
"id": "CTR-R4-02", "region_id": "RG-04", "market_start": 1,
"market_end": 12, "boss_market": 12, "boss_id": "BO-R4-02",
"name": localized("Second Quillroot Contract", "根羽第二契约"),
"preview": localized(
"Carry the surviving Company through twelve redaction-and-replay Rounds before Redaction Hart.",
"沿用存活队伍,完成十二轮删节与重放挑战,抵达“删节雄鹿”。",
),
"response_tags": ["same_run_company", "safe_rule_seed", "redaction_replay"],
},
"equipment": equipment_rows(catalog),
"artifacts": artifact_rows(catalog),
"enemies": enemy_rows(catalog),
"bosses": boss_rows(),
"rg04_markets": market_rows("CTR-R4-01"),
"rg04_contract2_markets": market_rows("CTR-R4-02"),
}
OUTPUT.write_text(json.dumps(output, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
print(
"ABG_FG4_RG04_CATALOG_BUILD_OK "
f"equipment={len(output['equipment'])} artifacts={len(output['artifacts'])} "
f"enemies={len(output['enemies'])} bosses={len(output['bosses'])} "
f"markets={len(output['rg04_markets']) + len(output['rg04_contract2_markets'])}"
)
return 0
if __name__ == "__main__":
raise SystemExit(main())