feat(m79-b): contract advancement opens region two with its band, boss, battlefield and music
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
| 2026-09-13 | M60 可玩性真相(W1) | `e1cad068`/`dac5de40` | `m60-playthrough` `20260913T145904Z-38281` | runtime_verified | 34 步 0 阻断;视频与逐步截图 |
|
||||
| 2026-09-13 | M61 设计/密度(W2) | `310d2620` | `m61-five-profile` `20260913T162625Z-2172` | simulation_verified | decisions=330, max_gap=31.3s, 反支配/存档/可达通过 |
|
||||
| 2026-09-13 | M62 组件统一(W3) | `2e5540ed` | `m62-ink-baselines` `20260913T164335Z-38711` | runtime_verified | 组件库+矩阵+探针接入;基线 Guild 0 / Contract 1 / Readiness+Result 6 / Market 严格 0 |
|
||||
| 2026-09-14 | M79-B 合同推进/区域前进(第三次合同进区域 2) | `feat(m79-b)` | runtime_verified | 序数取合同行 + 区域带 + Boss 锚定 + 区域战场/音乐;`ABG_M79_REGION_ADVANCE_OK` |
|
||||
| 2026-09-14 | M79-A 区域 2 内容落地(数据+本地化+区域带+立绘+音乐) | `feat(m79-a)` `b3fa611c`/`9aabf9c0` | `m79-region2` `20260914T220343Z-47202` | runtime_verified | 目录 80 装备/15 遗物/24 敌人/4 Boss/2 区域/4 合同 + 201 行中文名与标签;市场与战斗按区域带抽取 |
|
||||
| 2026-09-14 | M78 战斗演员立绘(替换头像贴纸,Owner 指出项) | `feat(m78)` | runtime_verified | 52 张(12 职业 + 12 敌人 + 2 Boss × 待机/出手);姿态随节拍切换 |
|
||||
| 2026-09-14 | M77-D 战斗字幕/无障碍/双档墨迹门 | `feat(m77-d)` `1fe13e7b` | `m77-battle-polish` `20260914T174012Z-45680` | runtime_verified | captions+detail 生效;color_cues 单墨色模式;战斗屏 1.0/1.3 双档 0 违规;立绘高度带 |
|
||||
|
||||
@@ -17,10 +17,13 @@
|
||||
|
||||
## Ordered steps
|
||||
|
||||
1. **M79-A(数据+本地化 COMPLETE;立绘/音乐/可达性 PENDING)区域 2**:EQ-041..080、EN-R2-01..12、BO-R2-01/02、AR-009..015、
|
||||
1. **M79-A(COMPLETE)区域 2**:EQ-041..080、EN-R2-01..12、BO-R2-01/02、AR-009..015、
|
||||
CTR-R2-01/02 的中文名 + 语义标签;注册表计数更新(80/24/4/15/2/4);区域带接线(战斗/装备按区域抽);
|
||||
区域 2 敌人立绘(12×2)+ Boss(2×2);区域 2 战斗/BOSS 音乐;全流程门。
|
||||
2. **M79-B..G 区域 3..8**:同样管线逐区域推进(每区域一片,含立绘/音乐/门)。
|
||||
1b. **M79-B(COMPLETE)合同推进/区域前进**:合同按序数从目录取行(2/区域),区域带随行落地;
|
||||
已归还的合同作为历史保留(含其全部回执),修订号 = 已建合同数 + 已提交动作数;第三次合同进区域 2 ✓;
|
||||
区域 2 战场底图与区域音乐族切换。
|
||||
2. **M79-C..G 区域 3..8**:同样管线逐区域推进(每区域一片,含立绘/音乐/门)。
|
||||
3. **M79-H 收口**:全量计数门(320/100/16/60/8/16)、区域推进(合同按区域前进)、
|
||||
区域图鉴/解锁与区域差异实拍。
|
||||
|
||||
@@ -54,7 +57,10 @@
|
||||
| --- | --- | --- |
|
||||
| `M79-I01` | OPEN | 合同行(16 条)的 round_count/first_clear_renown 为派生值,待复核 |
|
||||
| `M79-I02` | OPEN | 填充词检查对 `copy of` 有误报(EQ-248、BO-R3-02 是机制描述),需登记例外或改写措辞 |
|
||||
| `M79-I03` | OPEN(需 Owner 拍板范围)| 区域 2+ 内容目前在**运行时不可达**:合同循环只到首份合同(`contract_ordinal != 0` 即拒绝),
|
||||
| `M79-I03` | RESOLVED(Owner:'超出 MVP 没事,可以支持你做')| 区域 2+ 内容目前在**运行时不可达**:合同循环只到首份合同(`contract_ordinal != 0` 即拒绝),
|
||||
第二次合同/区域推进属"第二周目"范围(`M31-I03` 曾判定超出首个 MVP 停点)。按区域补内容要真正可玩,需要一步
|
||||
"合同推进/区域前进"(含区域 Boss Muster、区域音乐与立绘切换)。建议作为 M79 的下一步。 |
|
||||
| `M79-I04` | OPEN | 区域 2 敌人/Boss 立绘(12×2 + 2×2 = 28 张)与区域战斗/BOSS 音乐(各 1 套)尚未生成 |
|
||||
| `M79-I04` | RESOLVED | 区域 2 立绘 28 张 + 战斗/Boss 音乐各 1 套已生成并接线 |
|
||||
| `M79-I05` | OPEN | 区域推进已有服务层门(`region_advance_test`),UI 全流程(合同→…→归还→下一合同)尚未驱动验证;
|
||||
收口阶段补一次"两周目"全流程驱动 |
|
||||
| `M79-I06` | OPEN | 16 条合同的 `round_count`/`first_clear_renown` 仍为派生值(待复核);超出已实现区域后合同会重复最后一份(后游戏范围) |
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.5 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.5 MiB |
@@ -2,6 +2,7 @@ class_name ABGContractChoicePageController
|
||||
extends RefCounted
|
||||
|
||||
const GuildContractService = preload("res://entry/guild_contract_service.gd")
|
||||
const ContentRegistry = preload("res://p10_0/content_registry.gd")
|
||||
|
||||
## UI-independent Contract Choice view-data assembly.
|
||||
## Contract creation remains owned by GuildContractService; this controller only
|
||||
@@ -25,11 +26,12 @@ func snapshot(company: Dictionary, selected_difficulty := "standard", locale :=
|
||||
"owned_count": recruits.size(),
|
||||
"recruits": recruits,
|
||||
}
|
||||
var contract_row: Dictionary = _next_contract_row(locale, company)
|
||||
var creation_facts := {
|
||||
"contract_id": GuildContractService.FIRST_CONTRACT_ID,
|
||||
"route_id": "windglass_verge",
|
||||
"route_title": _tr("风镜初渡", "FIRST WINDGLASS CROSSING", locale),
|
||||
"route_copy": _tr("穿越悬浮风镜,建立公会的第一条安全通路。", "Cross the suspended windglass and establish the Guild's first safe route.", locale),
|
||||
"contract_id": String(contract_row.get("id", GuildContractService.FIRST_CONTRACT_ID)),
|
||||
"route_id": String(contract_row.get("region_id", "RG-01")).to_lower(),
|
||||
"route_title": String(contract_row.get("route_title", _tr("风镜初渡", "FIRST WINDGLASS CROSSING", locale))),
|
||||
"route_copy": String(contract_row.get("route_copy", _tr("穿越悬浮风镜,建立公会的第一条安全通路。", "Cross the suspended windglass and establish the Guild's first safe route.", locale))),
|
||||
"starting_coin": GuildContractService.STARTING_COIN,
|
||||
"line_capacity": GuildContractService.STARTING_LINE_CAPACITY,
|
||||
"market_counts": {"recruit": 3, "equipment": 4, "battle": 3},
|
||||
@@ -109,3 +111,26 @@ func _copy_array(value) -> Array:
|
||||
|
||||
func _tr(zh: String, en: String, locale: String) -> String:
|
||||
return en if String(locale).to_lower().begins_with("en") else zh
|
||||
|
||||
func _next_contract_row(locale: String, company: Dictionary) -> Dictionary:
|
||||
# The next contract follows the company's ordinal; region titles come from
|
||||
# the catalogue and its zh authority.
|
||||
var content: Dictionary = ContentRegistry.snapshot()
|
||||
var rows: Array = Array(content.get("contracts", []))
|
||||
rows.sort_custom(func(a, b): return String(a.get("id", "")) < String(b.get("id", "")))
|
||||
if rows.is_empty():
|
||||
return {}
|
||||
var ordinal := 0
|
||||
if company is Dictionary:
|
||||
ordinal = int(company.get("next_contract_ordinal", 0))
|
||||
var row: Dictionary = Dictionary(rows[clampi(ordinal, 0, rows.size() - 1)]).duplicate(true)
|
||||
var region_id := String(row.get("region_id", "RG-01"))
|
||||
row["route_title"] = ContentRegistry.localized_name(content, region_id, locale)
|
||||
if String(row["route_title"]).is_empty():
|
||||
row["route_title"] = region_id
|
||||
var region_rows: Array = Array(content.get("region", []))
|
||||
for region_row in region_rows:
|
||||
if String(Dictionary(region_row).get("id", "")) == region_id:
|
||||
row["route_copy"] = String(Dictionary(region_row).get("framing", ""))
|
||||
break
|
||||
return row
|
||||
|
||||
@@ -92,6 +92,10 @@ const GUILD_DESK_BOTTOM_PARTY = preload("res://assets/entry/guild_desk_v001/bott
|
||||
const BATTLE_OPENING_BACKGROUND = preload("res://assets/backgrounds/first_expedition_field.png")
|
||||
const BATTLE_OPENING_ENEMY = preload("res://assets/enemies/riftblade/idle.png")
|
||||
const BATTLE_FIELD = preload("res://assets/battle/windglass_verge_battlefield.png")
|
||||
const BATTLE_FIELDS := {
|
||||
1: preload("res://assets/battle/windglass_verge_battlefield.png"),
|
||||
2: preload("res://assets/battle/siltwake_fens_battlefield.png"),
|
||||
}
|
||||
const BATTLE_ENEMY_ART := {
|
||||
"EN-R1-01": preload("res://assets/battle/enemies/enemy-en-r1-01-pane-nibbler.png"),
|
||||
"EN-R1-02": preload("res://assets/battle/enemies/enemy-en-r1-02-gust-finch.png"),
|
||||
@@ -3257,7 +3261,7 @@ func _build_battle_opening_canvas() -> void:
|
||||
child.free()
|
||||
var background := TextureRect.new()
|
||||
background.name = "BattleOpeningWorld"
|
||||
background.texture = BATTLE_FIELD
|
||||
background.texture = _battle_field_texture()
|
||||
background.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||
background.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_COVERED
|
||||
background.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS
|
||||
@@ -3666,6 +3670,12 @@ func _battle_profession_parent(profession_id: String) -> String:
|
||||
return found.get_string() if found != null else ""
|
||||
|
||||
|
||||
func _battle_field_texture() -> Texture2D:
|
||||
var contract: Dictionary = Dictionary(guild_company.get("active_contract", {}))
|
||||
var region_index := int(contract.get("region_index", 1))
|
||||
return BATTLE_FIELDS.get(region_index, BATTLE_FIELD)
|
||||
|
||||
|
||||
func _battle_music_family(company: Dictionary) -> String:
|
||||
# Region 1 keeps the original family names; later regions suffix the id so
|
||||
# each region's battle and boss beds stay addressable.
|
||||
@@ -5043,7 +5053,7 @@ func _show_legacy_battle(company := {}, auto_play := true) -> void:
|
||||
battle_operation_id = String(battle.operation_id)
|
||||
guild_company = resolved
|
||||
current_screen = "battle"
|
||||
_build_world_base(false, BATTLE_FIELD)
|
||||
_build_world_base(false, _battle_field_texture())
|
||||
var frame_safe := _framed_page_safe("Battle")
|
||||
frame_safe.add_child(_entry_chrome("BattleFrame", "initial_company_scene_frame"))
|
||||
var safe := _content_page_safe("Battle")
|
||||
|
||||
@@ -29,7 +29,9 @@ func load_company() -> Dictionary:
|
||||
func create_contract(operation_id: String, contract_id: String, difficulty: String) -> Dictionary:
|
||||
if not _is_hex(operation_id, 32):
|
||||
return {"ok": false, "code": "CONTRACT_OPERATION_ID_INVALID"}
|
||||
if contract_id != FIRST_CONTRACT_ID:
|
||||
var contract_rows: Array = _contract_rows()
|
||||
var expected_row := _contract_row_for_ordinal(contract_rows, int(company_ordinal_probe(contract_id)))
|
||||
if contract_id != String(expected_row.get("id", FIRST_CONTRACT_ID)):
|
||||
return {"ok": false, "code": "CONTRACT_LOCKED"}
|
||||
if difficulty not in DIFFICULTIES:
|
||||
return {"ok": false, "code": "CONTRACT_DIFFICULTY_INVALID"}
|
||||
@@ -38,6 +40,17 @@ func create_contract(operation_id: String, contract_id: String, difficulty: Stri
|
||||
if not loaded.ok:
|
||||
return loaded
|
||||
var company: Dictionary = loaded.company.duplicate(true)
|
||||
if not company.active_contract.is_empty() and String(company.active_contract.get("status", "")) == "returned":
|
||||
# A returned contract is history: keep the whole closed contract (its
|
||||
# hire/equipment/return receipts included) so the company accounting and
|
||||
# every prior receipt stay verifiable.
|
||||
var history: Array = Array(company.get("contract_receipts", [])).duplicate(true)
|
||||
if not history.is_empty():
|
||||
history[history.size() - 1] = company.active_contract.duplicate(true)
|
||||
else:
|
||||
history.append(company.active_contract.duplicate(true))
|
||||
company.contract_receipts = history
|
||||
company.active_contract = {}
|
||||
if not company.active_contract.is_empty():
|
||||
var existing_receipt: Dictionary = company.active_contract.creation_receipt
|
||||
var same_intent := String(existing_receipt.contract_id) == contract_id and String(existing_receipt.difficulty) == difficulty
|
||||
@@ -53,6 +66,10 @@ func create_contract(operation_id: String, contract_id: String, difficulty: Stri
|
||||
}
|
||||
return {"ok": false, "code": "CONTRACT_ALREADY_ACTIVE"}
|
||||
var contract_ordinal := int(company.next_contract_ordinal)
|
||||
var ordinal_row: Dictionary = _contract_row_for_ordinal(_contract_rows(), contract_ordinal)
|
||||
if contract_id != String(ordinal_row.get("id", FIRST_CONTRACT_ID)):
|
||||
return {"ok": false, "code": "CONTRACT_ORDINAL_INVALID"}
|
||||
var region_index := _region_index_for_contract(ordinal_row)
|
||||
var intent := {
|
||||
"company_id": String(company.company_id),
|
||||
"contract_ordinal": contract_ordinal,
|
||||
@@ -60,11 +77,10 @@ func create_contract(operation_id: String, contract_id: String, difficulty: Stri
|
||||
"difficulty": difficulty,
|
||||
}
|
||||
var intent_hash := _hash(intent)
|
||||
if contract_ordinal != 0:
|
||||
return {"ok": false, "code": "CONTRACT_ORDINAL_INVALID"}
|
||||
var run_seed := _low128(_digest(String(company.company_id), "dev3-contract-run", [contract_ordinal]))
|
||||
var run_id := _low128(_digest(run_seed, "dev3-contract-id", [contract_id]))
|
||||
var generated: Dictionary = _generator.market_board(run_seed, contract_id, 1, 0)
|
||||
var region_boss := String(ordinal_row.get("boss_id", ""))
|
||||
var generated: Dictionary = _generator.market_board(run_seed, contract_id, 1, 0, 0, region_index, region_boss)
|
||||
if not generated.ok:
|
||||
return {"ok": false, "code": "CONTRACT_MARKET_GENERATION_FAILED"}
|
||||
var board := _price_first_board(generated.board)
|
||||
@@ -86,6 +102,10 @@ func create_contract(operation_id: String, contract_id: String, difficulty: Stri
|
||||
"contract_version": 1,
|
||||
"company_id": String(company.company_id),
|
||||
"contract_id": contract_id,
|
||||
"region_id": String(ordinal_row.get("region_id", "RG-01")),
|
||||
"region_index": region_index,
|
||||
"boss_id": region_boss,
|
||||
"contract_ordinal": contract_ordinal,
|
||||
"run_id": run_id,
|
||||
"run_seed": run_seed,
|
||||
"difficulty": difficulty,
|
||||
@@ -104,7 +124,8 @@ func create_contract(operation_id: String, contract_id: String, difficulty: Stri
|
||||
company.company_revision = int(company.company_revision) + 1
|
||||
company.next_contract_ordinal = contract_ordinal + 1
|
||||
company.active_contract = contract
|
||||
company.contract_receipts = [receipt]
|
||||
company.contract_receipts = Array(company.get("contract_receipts", [])).duplicate(true)
|
||||
company.contract_receipts.append(receipt)
|
||||
company.erase("company_hash")
|
||||
company.company_hash = _hash(company)
|
||||
var saved: Dictionary = store.save_company(company)
|
||||
@@ -520,6 +541,34 @@ func _price_first_board(raw: Dictionary) -> Dictionary:
|
||||
return board
|
||||
|
||||
|
||||
func _contract_rows() -> Array:
|
||||
var content: Dictionary = _content if not _content.is_empty() else ContentRegistry.snapshot()
|
||||
var rows: Array = Array(content.get("contracts", []))
|
||||
rows.sort_custom(func(a, b): return String(a.get("id", "")) < String(b.get("id", "")))
|
||||
return rows
|
||||
|
||||
|
||||
func _contract_row_for_ordinal(rows: Array, ordinal: int) -> Dictionary:
|
||||
if rows.is_empty():
|
||||
return {}
|
||||
var index := clampi(ordinal, 0, rows.size() - 1)
|
||||
return Dictionary(rows[index])
|
||||
|
||||
|
||||
func _region_index_for_contract(row: Dictionary) -> int:
|
||||
var region_id := String(row.get("region_id", "RG-01"))
|
||||
return int(region_id.split("-")[1]) if region_id.begins_with("RG-") else 1
|
||||
|
||||
|
||||
func company_ordinal_probe(contract_id: String) -> int:
|
||||
# Contract ids are `CTR-R<region>-<slot>`; the ordinal follows row order.
|
||||
var rows: Array = _contract_rows()
|
||||
for index in range(rows.size()):
|
||||
if String(Dictionary(rows[index]).get("id", "")) == contract_id:
|
||||
return index
|
||||
return 0
|
||||
|
||||
|
||||
func _offer_ids(offers: Array, id_field: String) -> Array[String]:
|
||||
var result: Array[String] = []
|
||||
for offer in offers:
|
||||
|
||||
@@ -2,6 +2,7 @@ class_name ABGInitialCompanyDraftService
|
||||
extends RefCounted
|
||||
|
||||
const ContentRegistry = preload("res://p10_0/content_registry.gd")
|
||||
const MAX_CONTRACT_ORDINALS := ContentRegistry.CONTENT_STAGE_CONTRACTS
|
||||
const OfferGenerator = preload("res://p10_0/offer_generator.gd")
|
||||
|
||||
const DRAFT_VERSION := 1
|
||||
@@ -202,8 +203,11 @@ func validate_company(value) -> Dictionary:
|
||||
return receipt_validation
|
||||
if not company.active_contract is Dictionary or not company.contract_receipts is Array:
|
||||
return {"ok": false, "code": "COMPANY_CONTRACT_STATE_INVALID"}
|
||||
var contract_ordinal := int(company.next_contract_ordinal)
|
||||
if contract_ordinal < 0 or contract_ordinal > MAX_CONTRACT_ORDINALS:
|
||||
return {"ok": false, "code": "COMPANY_CONTRACT_STATE_INVALID"}
|
||||
if company.active_contract.is_empty():
|
||||
if not company.contract_receipts.is_empty() or int(company.next_contract_ordinal) != 0:
|
||||
if company.contract_receipts.size() != contract_ordinal:
|
||||
return {"ok": false, "code": "COMPANY_CONTRACT_STATE_INVALID"}
|
||||
else:
|
||||
if String(company.active_contract.get("company_id", "")) != String(company.company_id):
|
||||
@@ -215,9 +219,23 @@ func validate_company(value) -> Dictionary:
|
||||
var equipment_count: int = company.active_contract.get("equipment_receipts", []).size()
|
||||
var dismissal_count: int = company.active_contract.get("dismissal_receipts", []).size()
|
||||
var return_count: int = company.active_contract.get("contract_return_receipts", []).size()
|
||||
if int(company.next_contract_ordinal) != 1 or int(company.company_revision) != 1 + hire_count + equipment_count + dismissal_count + return_count:
|
||||
# Contract creation itself does not advance the revision; each committed
|
||||
# company action (hire, equipment, dismissal, return) adds exactly one, in
|
||||
# the active contract or in any closed contract kept in the history.
|
||||
var committed := hire_count + equipment_count + dismissal_count + return_count
|
||||
var history: Array = company.get("contract_receipts", [])
|
||||
for index in range(maxi(0, history.size() - 1)):
|
||||
var entry = history[index]
|
||||
if entry is Dictionary and entry.has("hire_receipts"):
|
||||
committed += Array(entry.hire_receipts).size()
|
||||
committed += Array(entry.get("equipment_receipts", [])).size()
|
||||
committed += Array(entry.get("dismissal_receipts", [])).size()
|
||||
committed += Array(entry.get("contract_return_receipts", [])).size()
|
||||
# Every created contract advances the revision once, and so does each
|
||||
# committed company action anywhere in the contract history.
|
||||
if int(company.company_revision) != contract_ordinal + committed:
|
||||
return {"ok": false, "code": "COMPANY_CONTRACT_REVISION_INVALID"}
|
||||
if company.contract_receipts.size() != 1 or company.contract_receipts[0] != company.active_contract.creation_receipt:
|
||||
if company.contract_receipts.size() != contract_ordinal or company.contract_receipts[company.contract_receipts.size() - 1] != company.active_contract.creation_receipt:
|
||||
return {"ok": false, "code": "COMPANY_CONTRACT_RECEIPT_INVALID"}
|
||||
if company.owned_recruits.size() != 2 + hire_count - dismissal_count:
|
||||
return {"ok": false, "code": "COMPANY_HIRE_ROSTER_INVALID"}
|
||||
@@ -544,7 +562,12 @@ func _validate_active_contract(value: Dictionary, company: Dictionary) -> Dictio
|
||||
return {"ok": false, "code": "CONTRACT_FIELD_MISSING", "field": field}
|
||||
if int(value.contract_version) != 1:
|
||||
return {"ok": false, "code": "CONTRACT_VERSION_UNSUPPORTED"}
|
||||
if String(value.contract_id) != "CTR-R1-01" or not _is_hex(String(value.run_id), 32) or not _is_hex(String(value.run_seed), 32):
|
||||
# M79-B: any contract row from the staged catalogue is a legal identity.
|
||||
var contract_rows: Array = Array(ContentRegistry.snapshot().get("contracts", []))
|
||||
var legal_ids: Array[String] = []
|
||||
for row in contract_rows:
|
||||
legal_ids.append(String(Dictionary(row).get("id", "")))
|
||||
if not legal_ids.has(String(value.contract_id)) or not _is_hex(String(value.run_id), 32) or not _is_hex(String(value.run_seed), 32):
|
||||
return {"ok": false, "code": "CONTRACT_IDENTITY_INVALID"}
|
||||
if String(value.difficulty) not in ["wayfinder", "standard", "stormbound", "iron_oath"]:
|
||||
return {"ok": false, "code": "CONTRACT_DIFFICULTY_INVALID"}
|
||||
@@ -865,7 +888,7 @@ func _validate_contract_creation_receipt(value, contract: Dictionary) -> Diction
|
||||
return {"ok": false, "code": "CONTRACT_RECEIPT_ID_INVALID"}
|
||||
if (
|
||||
String(receipt.company_id) != String(contract.company_id)
|
||||
or int(receipt.contract_ordinal) != 0
|
||||
or int(receipt.contract_ordinal) != int(contract.get("contract_ordinal", 0))
|
||||
or String(receipt.contract_id) != String(contract.contract_id)
|
||||
or String(receipt.difficulty) != String(contract.difficulty)
|
||||
or String(receipt.run_id) != String(contract.run_id)
|
||||
@@ -877,7 +900,7 @@ func _validate_contract_creation_receipt(value, contract: Dictionary) -> Diction
|
||||
return {"ok": false, "code": "CONTRACT_RECEIPT_STATE_INVALID"}
|
||||
var expected_intent := _hash({
|
||||
"company_id": String(contract.company_id),
|
||||
"contract_ordinal": 0,
|
||||
"contract_ordinal": int(contract.get("contract_ordinal", 0)),
|
||||
"contract_id": String(contract.contract_id),
|
||||
"difficulty": String(contract.difficulty),
|
||||
})
|
||||
|
||||
@@ -11,6 +11,8 @@ const SOURCE_REVISION := "phase8-p8-3r2-contact-rear-actions-replacement"
|
||||
## 60 artifacts, 100 enemies, 16 bosses and 16 contracts.
|
||||
const CONTENT_STAGE_REGIONS := 2
|
||||
const CONTENT_EXPECTED_REGIONS := 8
|
||||
## Two contracts per shipped region.
|
||||
const CONTENT_STAGE_CONTRACTS := CONTENT_STAGE_REGIONS * 2
|
||||
const FAMILY_COUNTS := {
|
||||
"professions": 36,
|
||||
"traits": 36,
|
||||
|
||||
@@ -108,12 +108,15 @@ func opening_offers(run_creation_draft: Dictionary, presentation_revision: int =
|
||||
return {"ok": true, "code": "ok", "materialization": materialization, "offers": offers}
|
||||
|
||||
|
||||
func market_board(run_seed: String, contract_id: String, market_index: int, refresh_index: int = 0, presentation_revision: int = 0, region_index: int = 1) -> Dictionary:
|
||||
func market_board(run_seed: String, contract_id: String, market_index: int, refresh_index: int = 0, presentation_revision: int = 0, region_index: int = 1, region_boss: String = "") -> Dictionary:
|
||||
if not bool(_validation.get("ok", false)):
|
||||
return {"ok": false, "code": "invalid_catalog", "errors": _validation.get("errors", []).duplicate()}
|
||||
if not _is_seed(run_seed):
|
||||
return {"ok": false, "code": "invalid_run_seed"}
|
||||
if contract_id not in ["CTR-R1-01", "CTR-R1-02"]:
|
||||
var known_contracts: Array[String] = []
|
||||
for row in Array(ContentRegistry.snapshot().get("contracts", [])):
|
||||
known_contracts.append(String(Dictionary(row).get("id", "")))
|
||||
if not known_contracts.has(contract_id):
|
||||
return {"ok": false, "code": "unknown_contract"}
|
||||
if market_index < 1 or market_index > 12:
|
||||
return {"ok": false, "code": "invalid_market_index"}
|
||||
@@ -146,7 +149,7 @@ func market_board(run_seed: String, contract_id: String, market_index: int, refr
|
||||
recruit_offers.append(record)
|
||||
|
||||
var equipment_offers := _equipment_offers(run_seed, contract_id, market_index, refresh_index, region_index)
|
||||
var battle_offers := _battle_offers(run_seed, contract_id, market_index, region_index)
|
||||
var battle_offers := _battle_offers(run_seed, contract_id, market_index, region_index, region_boss)
|
||||
var board := {
|
||||
"board_id": _low128(_digest(run_seed, "market-board", [contract_id, market_index, refresh_index])),
|
||||
"run_seed": run_seed,
|
||||
@@ -257,7 +260,7 @@ func _equipment_offers(run_seed: String, contract_id: String, market_index: int,
|
||||
return offers
|
||||
|
||||
|
||||
func _battle_offers(run_seed: String, contract_id: String, market_index: int, region_index: int) -> Array[Dictionary]:
|
||||
func _battle_offers(run_seed: String, contract_id: String, market_index: int, region_index: int, region_boss: String = "") -> Array[Dictionary]:
|
||||
var enemy_rows := ContentRegistry.rows_by_id(_catalog, "enemies")
|
||||
var ordinary: Array[String] = []
|
||||
var elite: Array[String] = []
|
||||
@@ -276,7 +279,12 @@ func _battle_offers(run_seed: String, contract_id: String, market_index: int, re
|
||||
var enemy_count := 2 if risk == 0 else 3
|
||||
var enemies := _draw_unique(ordinary, run_seed, "battle-enemy", enemy_count, [contract_id, market_index, risk])
|
||||
if risk == 2:
|
||||
enemies[-1] = elite[_index(run_seed, "battle-elite", elite.size(), [contract_id, market_index, risk])]
|
||||
# The region's contract boss anchors the highest-risk offer.
|
||||
var boss_rows := ContentRegistry.rows_by_id(_catalog, "bosses")
|
||||
if not region_boss.is_empty() and boss_rows.has(region_boss):
|
||||
enemies[-1] = region_boss
|
||||
elif not elite.is_empty():
|
||||
enemies[-1] = elite[_index(run_seed, "battle-elite", elite.size(), [contract_id, market_index, risk])]
|
||||
var offer := {
|
||||
"offer_id": _low128(_digest(run_seed, "battle-offer", [contract_id, market_index, risk])),
|
||||
"risk": risk,
|
||||
@@ -415,9 +423,11 @@ func _default_role_weights(run_seed: String, contract_id: String, market_index:
|
||||
|
||||
|
||||
func _battle_preview_tags(enemy_ids: Array[String], enemy_rows: Dictionary) -> Array[String]:
|
||||
var boss_rows := ContentRegistry.rows_by_id(_catalog, "bosses")
|
||||
var tags: Array[String] = []
|
||||
for enemy_id in enemy_ids:
|
||||
for tag in enemy_rows[enemy_id].semantic_tags:
|
||||
var row: Dictionary = enemy_rows.get(enemy_id, boss_rows.get(enemy_id, {}))
|
||||
for tag in Array(row.get("semantic_tags", [])):
|
||||
if tag not in tags:
|
||||
tags.append(String(tag))
|
||||
tags.sort()
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
extends SceneTree
|
||||
|
||||
## M79-B gate: Contracts advance by ordinal, so the second region becomes
|
||||
## reachable. Contract 1 and 2 are region one; contract 3 opens Siltwake Fens
|
||||
## with its own equipment band, enemy pool and boss anchor.
|
||||
|
||||
const DraftService = preload("res://entry/initial_company_draft_service.gd")
|
||||
const ContractService = preload("res://entry/guild_contract_service.gd")
|
||||
const ContentRegistry = preload("res://p10_0/content_registry.gd")
|
||||
|
||||
const COMPANY_PATH := "user://aetherbound_region_advance_company.json"
|
||||
const DRAFT_PATH := "user://aetherbound_region_advance_draft.json"
|
||||
const OP_CREATE_1 := "11111111111111111111111111111111"
|
||||
const OP_RETURN_1 := "22222222222222222222222222222222"
|
||||
const OP_CREATE_2 := "33333333333333333333333333333333"
|
||||
const OP_RETURN_2 := "44444444444444444444444444444444"
|
||||
const OP_CREATE_3 := "55555555555555555555555555555555"
|
||||
|
||||
var failures: Array[String] = []
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
_cleanup()
|
||||
var company := _create_company()
|
||||
if company.is_empty():
|
||||
push_error("region advance: company creation failed")
|
||||
quit(1)
|
||||
return
|
||||
expect(String(company.active_contract.region_id) == "RG-01", "first contract opens region one")
|
||||
expect(_equipment_band(company) == "EQ-001..EQ-040", "region one market draws EQ-001..EQ-040")
|
||||
expect(int(company.active_contract.region_index) == 1, "region index starts at one")
|
||||
|
||||
var returned_1: Dictionary = _service().return_contract(OP_RETURN_1)
|
||||
if not returned_1.ok:
|
||||
push_error("region advance: contract one return failed: %s" % String(returned_1.get("code", "")))
|
||||
quit(1)
|
||||
return
|
||||
var created_2: Dictionary = _service().create_contract(OP_CREATE_2, "CTR-R1-02", "standard")
|
||||
if not created_2.ok:
|
||||
push_error("region advance: second contract failed: %s" % String(created_2.get("code", "")))
|
||||
quit(1)
|
||||
return
|
||||
expect(String(created_2.company.active_contract.region_id) == "RG-01", "second contract stays in region one")
|
||||
expect(_equipment_band(created_2.company) == "EQ-001..EQ-040", "region one market band holds for contract two")
|
||||
|
||||
var returned_2: Dictionary = _service().return_contract(OP_RETURN_2)
|
||||
if not returned_2.ok:
|
||||
push_error("region advance: contract two return failed: %s" % String(returned_2.get("code", "")))
|
||||
quit(1)
|
||||
return
|
||||
var created_3: Dictionary = _service().create_contract(OP_CREATE_3, "CTR-R2-01", "standard")
|
||||
if not created_3.ok:
|
||||
push_error("region advance: third contract failed: %s" % String(created_3.get("code", "")))
|
||||
quit(1)
|
||||
return
|
||||
var contract: Dictionary = created_3.company.active_contract
|
||||
expect(String(contract.region_id) == "RG-02", "third contract opens region two, got %s" % String(contract.region_id))
|
||||
expect(int(contract.region_index) == 2, "region index advances to two")
|
||||
expect(String(contract.boss_id) == "BO-R2-01", "region two contract anchors its first boss")
|
||||
expect(_equipment_band(created_3.company) == "EQ-041..EQ-080", "region two market draws EQ-041..EQ-080")
|
||||
|
||||
var enemy_ids: Array[String] = []
|
||||
for offer in Array(contract.market_board.battle_offers):
|
||||
for enemy_id in Array(Dictionary(offer).get("enemy_ids", [])):
|
||||
enemy_ids.append(String(enemy_id))
|
||||
var off_region: Array[String] = []
|
||||
for enemy_id in enemy_ids:
|
||||
if not enemy_id.begins_with("EN-R2-") and not enemy_id.begins_with("BO-R2-"):
|
||||
off_region.append(enemy_id)
|
||||
expect(off_region.is_empty(), "region two battles field only region two identities, leaked %s" % str(off_region))
|
||||
var boss_offer: Dictionary = Dictionary(Array(contract.market_board.battle_offers)[2])
|
||||
expect(Array(boss_offer.get("enemy_ids", [])).has("BO-R2-01"), "highest-risk offer anchors the contract boss")
|
||||
|
||||
var content: Dictionary = ContentRegistry.snapshot()
|
||||
expect(ContentRegistry.localized_name(content, "RG-02", "zh_CN") == "淤醒沼泽", "region two zh authority resolves")
|
||||
expect(ContentRegistry.localized_name(content, "RG-02", "en") == "Siltwake Fens", "region two en authority resolves")
|
||||
|
||||
_cleanup()
|
||||
if failures.is_empty():
|
||||
print("ABG_M79_REGION_ADVANCE_OK contracts=3 region=RG-02 equipment=%s enemies=%d boss=%s" % [
|
||||
_equipment_band(created_3.company), enemy_ids.size(), String(contract.boss_id)])
|
||||
quit(0)
|
||||
return
|
||||
for failure in failures:
|
||||
push_error("M79-B region advance: %s" % failure)
|
||||
quit(1)
|
||||
|
||||
|
||||
func _equipment_band(company: Dictionary) -> String:
|
||||
# The market draws four identities from the region band; the gate checks that
|
||||
# every drawn identity stays inside the band rather than the band's extremes.
|
||||
var ids: Array[String] = []
|
||||
for offer in Array(Dictionary(company.get("active_contract", {})).get("market_board", {}).get("equipment_offers", [])):
|
||||
ids.append(String(Dictionary(offer).get("catalog_id", "")))
|
||||
if ids.is_empty():
|
||||
return "none"
|
||||
var low := 9999
|
||||
var high := 0
|
||||
for catalog_id in ids:
|
||||
var ordinal := int(catalog_id.split("-")[1])
|
||||
low = mini(low, ordinal)
|
||||
high = maxi(high, ordinal)
|
||||
var expected_low := 1 + (int(Dictionary(company.get("active_contract", {})).get("region_index", 1)) - 1) * 40
|
||||
var expected_high := expected_low + 39
|
||||
if low >= expected_low and high <= expected_high:
|
||||
return "EQ-%03d..EQ-%03d" % [expected_low, expected_high]
|
||||
return "out_of_band:%d..%d" % [low, high]
|
||||
|
||||
|
||||
func _create_company() -> Dictionary:
|
||||
var draft: Dictionary = _draft_service().begin_or_load("abcdef0123456789abcdef0123456789")
|
||||
if not draft.ok:
|
||||
return {}
|
||||
var selected := [String(draft.draft.offers[0].offer_id), String(draft.draft.offers[1].offer_id)]
|
||||
if not _draft_service().set_selection(selected).ok:
|
||||
return {}
|
||||
if not _draft_service().commit_company().ok:
|
||||
return {}
|
||||
var created: Dictionary = _service().create_contract(OP_CREATE_1, "CTR-R1-01", "standard")
|
||||
return created.company if created.ok else {}
|
||||
|
||||
|
||||
func _service() -> RefCounted:
|
||||
var service := ContractService.new()
|
||||
service.company_path = COMPANY_PATH
|
||||
return service
|
||||
|
||||
|
||||
func _draft_service() -> RefCounted:
|
||||
var service := DraftService.new()
|
||||
service.path = DRAFT_PATH
|
||||
service.company_path = COMPANY_PATH
|
||||
return service
|
||||
|
||||
|
||||
func _cleanup() -> void:
|
||||
for path in [COMPANY_PATH, DRAFT_PATH, "%s.bak" % COMPANY_PATH, "%s.candidate" % COMPANY_PATH]:
|
||||
if FileAccess.file_exists(path):
|
||||
DirAccess.remove_absolute(ProjectSettings.globalize_path(path))
|
||||
|
||||
|
||||
func expect(condition: bool, message: String) -> void:
|
||||
if not condition:
|
||||
failures.append(message)
|
||||
Reference in New Issue
Block a user