Phase:P4 — Quick Combat
本文件是 P4 的具體實作契約。產品行為以實作規格.md與根目錄規格企劃.md為準;測試與 closeout evidence以測試指南.md為準。
最後更新:2026-09-13
三份 P4 文件固定使用:
P4-A — Monster & Combatant Foundation
P4-B — Combat Lifecycle, Initiative & Action Economy
P4-C — Attack, Damage & Core Action Resolution
P4-D — Spells, Conditions, Concentration & Reactions
P4-E — Quick Combat UI, DM Adjudication & AI Tool Surface
P4-F — Full P4 Integration & Closeout
每個 Subphase完成 code + tests + static review後才進下一段;需要 Actions時依 AGENTS.md gate執行。
P4必須建立在 P3已完成的 actor / event / roll / Current State substrate上:
Human Web UI ─┐
├─> Combat application services ─> Rules / Character Current State
MCP Adapter ──┘ │
├─> P3 RollGroup / RollRequest
├─> P3 TableActorContext / DM proxy
└─> P3 Session event stream
Persistence <─ Combat application services
Standalone Character Core ─X→ Combat multiplayer modules
MCP Adapter ─X→ raw repository mutation
固定原則:
TableActorContext /既有 Room management path衍生,不新增 Combat bearer / cookie。因 Session End不結束 Combat,canonical Combat不得以 session_id 作 ownership root。建議最小模型:
combats
id UUID PK
room_id FK
campaign_id FK
mode = quick | tactical # P4只建立 quick;保留P5可演進shape
status = active | ended
round nullable int
current_turn_entry_id nullable FK
revision bigint
created_in_session_id nullable FK # audit only
ended_in_session_id nullable FK # audit only
started_at
ended_at nullable
combat_entries
id UUID PK
combat_id FK
kind = character | monster
character_id nullable FK
monster_instance_id nullable FK
initiative nullable int
initiative_group_key nullable
initiative_tiebreaker / order_key
turn_order_index
surprise_state
action_used bool
bonus_action_used bool
reaction_used bool
attacks_used int
attacks_allowed int
ready_state nullable JSON/typed columns
status
position_note nullable text
visibility
joined_round nullable int
removed_at nullable
實作可以調整命名/正規化,但必須滿足:
campaign_id是 active Combat uniqueness scope。created_in_session_id只是 provenance,不決定 lifecycle。revision或等價 optimistic concurrency token用於 UI / tool stale-write防護;不能依前端 current turn index當真值。建議 unique invariant:
UNIQUE one active combat per campaign
PostgreSQL可用 partial unique index;SQLite standalone不建這些表。
P4 不能把所有 Combat state都塞進 combat_entries。ownership固定為:
Character Current State(shared character track / Standalone 可攜)
current_hp / temporary_hp
persistent conditions
exhaustion_level
concentration
death_save_state
persistent temporary_effects
slots / class resources / inventory
combat_entries(web multiplayer track)
initiative / order
action / bonus / reaction availability
attacks_used / attacks_allowed
surprise
ready action / reaction window
position_note
combat-only temporary bookkeeping
Concentration / DeathSaveState / TemporaryEffect 等 shared DTO不得保存 Room / Campaign / Session / Combat FK;只使用 Character自身資料、stable content/rule refs與可攜 scalar metadata,避免 Character Core反向依賴多人層。
Character JSON v1的 envelope目前把 current_state.state_payload視為 payload object;P4的新增 Current State欄位採 additive/default-safe演進,不改既有 v1欄位語意,因此不因 P4 自動升成 v2。Importer仍必須接受既有 locked v1與最後 M03 unstable fixture;Web / Standalone都要能保存、export、import新欄位。若日後需要破壞 v1既有欄位語意,再另開 schema version,不在 P4暗改。
Character persistence migration若需要新欄位/JSON shape,落在 shared character Alembic track;Combat / Monster Instance tables才落 web track。P4 implementation同時擴充 M03 standalone schema-parity / import-boundary regression。
SRD Monster / Beast走正式 data/ content pipeline。不要在 runtime parse docs/暫用規則資訊、Markdown或 hardcoded Python dict。
P4-A的 authoritative upstream固定沿用 repo既有 SRD provenance:
repository: 5e-bits/5e-database
commit: ce47a18dfeb3e41a1b2a2dfe00a25761c3c3a4f1
path: src/2014/en/5e-SRD-Monsters.json
expected_records: 334
expected_beasts: 87
Beast不是另一份 dataset;是上述 334 records中 type == "beast" 的 87 筆 subset。Authoring/import工具把 pinned source materialize成 repo內正式 data/srd5.1/ runtime資料,並 checked-in manifest記錄 source repo / commit / path / expected records / expected beasts(可再記 source blob/hash);runtime與tests都以 materialized data + manifest為真,不在啟動時抓 GitHub。
Locale scope同 P4-A一起鎖死:
en:保存完整 canonical stat block。zh-TW:P4-A至少完整覆蓋 Monster name 與所有 user-visible named combat affordances(Trait / Action / Bonus Action / Reaction / Legendary Action等 name)。description/desc 若 P4-A 尚未 expose給 Human UI / MCP,可以保留 canonical English並延後翻譯;一旦某 Subphase首次正式 expose / search該 description欄位,該 Subphase必須同步提供 zh-TW。Monster Template建議沿用現有 content registry的 stable key / pack pattern:
srd5.1:monster:goblin
srd5.1:monster:wolf
...
實際 key convention以現有 registry規則為準,不為 P4另外造命名系統。
Template結構建議拆:
MonsterTemplate
identity / taxonomy
armor_class
hit_points { average, formula }
speed
abilities
saves / skills
damage_resistances / immunities / vulnerabilities
condition_immunities
senses / languages / cr
traits[]
actions[]
bonus_actions[]
reactions[]
legendary_actions[]
spellcasting[]
Action不要只存純 description。常見 attack / save / damage欄位應 machine-readable:
MonsterAction
name
kind = attack | save | heal | multiattack | other
attack_kind = melee_weapon | ranged_weapon | melee_spell | ranged_spell | null
attack_bonus nullable
reach_or_range_text nullable # P4展示;不做geometry calculation
target_text nullable
save_ability nullable
save_dc nullable
damage_parts[] { dice, flat, damage_type }
condition_effects[]
recharge nullable
resource_cost nullable
description
automation_level = structured | partial | dm_adjudication
automation_level不是 CRPG script engine;它只是告訴 application/UI「可直接 resolve到哪裡」。
建議 durable world/combat instance:
monster_instances
id
room_id
campaign_id
template_ref nullable
display_name
max_hp
current_hp
temp_hp
armor_class
speed_text / speed_structured
ability_scores
quick_attacks JSON/child rows
status
visibility
position_note nullable
conditions/effects via shared state model
resources/recharge via typed state
created_at / updated_at
Quick Enemy可 template_ref = null。Template-derived instance在建立時要保存足夠的 resolved combat stats,避免 content pack後續更新讓進行中的敵人瞬間改 HP / AC;但 source template ref保留 provenance。
不要把 initiative / turn order永久放 Monster Template;那是 Combat Entry state。
建立單一 CombatantProjector /等價 projection service:
DM -> full combatant view
Player -> safe combatant view
Player view不得含精確敵人 HP / AC / hidden resources。模糊傷勢從 current/max HP即時計算,不另存 writable wound_label truth。
所有 REST / Resume / event / MCP context共用 projector,避免「UI隱藏了但 MCP拿得到」。
建議集中在 actor-neutral application service:
CombatService
start_quick_combat(actor, input)
get_active_combat(actor)
add_character(actor, ...)
add_monster(actor, ...)
request_initiative(actor, ...)
submit/complete initiative via RollService
resolve_initiative_order(actor, ...)
start_turn(actor, ...)
advance_turn(actor, ...)
end_combat(actor, ...)
真正 API / MCP adapter只做 DTO與auth resolution。
Initiative不要另存 opaque random integer而失去 roll audit。每個 initiative result應能 reference既有 RollRequest / RollResult;monster group initiative可讓多個 combat entries reference同一 initiative group result。
Tie:
最小狀態:
combat inactive/active/ended
active -> initiative_pending -> running
running:
round >= 1
current_turn_entry_id
若實作不需要獨立 initiative_pending status可用 derived state,但不得在 initiative未完成時假裝已有 Current Turn。
每次新 Turn重置該 combatant:
不要在 UI自己 reset。
所有正式 combat actions先通過:
CombatActionContext
acting_actor
subject_combatant
action_kind
economy_cost
current_turn / reaction_window
Player self、DM proxy、DM控制敵人只在 permission resolution不同;後面的 economy validator相同。
Freeform Action可以記錄 economy_cost 或由 DM指定 none/action/bonus/reaction,但不能用 freeform偷偷繞過資源/turn invariant去執行已結構化的 Attack / Cast。
Combat是 Campaign-scoped;entry的 subject identity以 Character / Monster Instance為準,不以某一場 Session Seat snapshot為準。
新 Session resume active Combat時:
TableActorContext / Active Character重新 resolve controller authority,操作既有 entry,不複製 entry。add_character後,以 mid-combat entrant流程 roll initiative並插入 canonical order。建議每個正式 resolve入口使用單一 transaction boundary:
validate actor/current turn/economy
resolve source action definition
create/resolve required RollRequest(s)
compute outcome
apply damage/healing/state
consume action economy/resources
append durable event(s)
commit once
若 formal roll需要跨 request等待 Player擲骰,則 PendingAction / CombatAction先進 waiting_for_roll;最終 resolve transaction需有 compare-and-set / status guard,避免兩個 callback都 apply damage。
Character weapon / feature attack與Monster action需 normalize成相同 runtime shape:
ResolvedAttack
source_ref
attack_bonus
attack_kind
damage_parts
crit_behavior
ability/source metadata
notes
Rules resolver從 Character Build / Current equipment或 Monster Instance action產生,不把 Character logic複製進 Combat controller。
建議順序:
raw damage parts
→ crit dice expansion
→ per-type resistance / immunity / vulnerability
→ flat / special adjustments
→ Temp HP absorption
→ Current HP
→ concentration trigger if applicable
→ 0 HP consequences
每一層保存足夠 audit detail供 Log展開,但不要把整個 Character snapshot塞 event。
Damage type與 resistance資料用 stable enum/key;顯示名稱走 locale data。
P3既有 TableCharacterStatePatch.current_hp 是 absolute Current State patch;它不能在 active Combat中被當成「受到 N 點傷害」的替代品,因為 absolute target HP無法可靠推導:
因此 active Combat的正常 gameplay入口統一成 semantic command:
apply_damage(target, amount, source...)
apply_healing(target, amount, source...)
實際 function / DTO名稱可依 codebase調整,但 Attack / Spell、Player自行扣血 UI、AI DM、DM proxy都必須進同一 damage/healing application service。UI仍可維持桌上習慣的「扣 7」快速操作,不要求 Player走大型 Attack workflow。
Human DM Direct Edit是例外的 correction path:可以 absolute set HP / Temp HP / condition / economy,用於修正桌上錯誤;它不反推成 damage、也不自動觸發 Concentration或0 HP transition,並照既有規格記錄正式 edit event。
active Combat中一般 Player / AI Player不得以 raw current_hp patch繞過 semantic pipeline;outside Combat的既有 Current State手動編輯可維持原行為。
每個 write tool / REST action接受 idempotency_key(沿 P3慣例)或有等價 command id。至少保證:
DB-level unique / state transition guard要有測試,不只 application if-check。
不要建立第二套 Spell資料。P4只讀既有 Character spell access / prepared / resource pools與 content spell definitions,新增 combat-facing resolver:
ResolvedCombatSpell
casting_source
spellcasting_ability
attack_bonus / save_dc
slot/resource options
upcast choice
targeting_mode
automation_level
concentration
effects[]
casting_source 至少允許兩種正式來源:
若現有 SRD spell data不足以 machine-resolve某 spell,P4-D補的是正式 content fields / adapters,不是在 UI hardcode spell name。
Quick Combat沒有 geometry。對 AoE spell:
Server只驗 target identities合法、沒有跨 Room/Campaign;不計算「半徑20呎內」誰真的在範圍。
建議 shared typed modifier contribution,而不是 condition name到處寫 if:
ConditionDefinition
stable key
mechanical contributions
action restrictions
save/attack/check modifiers
notes / partial automation flags
但 P4不能因此打造任意 user scripting DSL。SRD 2014 Conditions逐個用 typed code / data adapter即可。
Temporary Effect採可序列化 typed contributions,未能結構化部分保留 note;未知 note不得偷偷被 Rules Engine猜成 modifier。
沿用 P3 PendingAction / durable event思想,建議:
combat_reaction_requests
id
combat_id
trigger_kind
source_entry_id
eligible_entry_ids
target_entry_id nullable
payload safe/secret split
status = pending | accepted | declined | resolved | cancelled
created_in_session_id
expires_at nullable # 不依 wall-clock自動 fiction timeout,僅技術選項
OA Quick trigger由 DM action建立;Tactical P5未來可由 movement engine建立同一種 request。
Ready也落到相同 request / reaction resolve service,不另造 Ready engine。
可依現有 Session routes形狀新增:
/api/rooms/{room}/campaigns/{campaign}/combats/active
/api/.../combats/{combat}/...
具體 route可調整,但所有 mutation需同時知道/驗證 current Session actor;不能因 Combat campaign-scoped就允許「人在 Room但不在本場 Session」直接操作 active combat。
read projection可由 current Session resume附 active Combat摘要,再按需 fetch detail;避免每個 P3 event都重抓整場 monster data。
建議 stable kinds:
combat.started
combat.combatant_added
combat.initiative_requested
combat.initiative_resolved
combat.turn_started
combat.action_declared
combat.attack_resolved
combat.save_resolved
combat.damage_applied
combat.healing_applied
combat.condition_changed
combat.concentration_changed
combat.reaction_requested
combat.reaction_resolved
combat.adjudication_requested
combat.adjudication_resolved
combat.ended
實際拆分可收斂,但不要用一個 combat.updated + 巨型 mutable payload取代可 audit event。
Player-safe event payload也要 projector;例如 combat.damage_applied可公開「Goblin受傷」與已公開 damage number,但不必附 goblin remaining HP。
建立通用但有限的 Combat adjudication,不是 P6世界 adjudication framework:
CombatAdjudicationRequest
kind = range | cover | affected_targets | opportunity_attack | special
action_id
subject / proposed targets
DM-only hints
status
decision typed fields + optional note
這個 model只服務 P4 Quick Combat缺 geometry的決策;P5可在有 geometry後減少產生,但不必刪 contract。
Tool數量保持小,偏高階 command,不把 repository CRUD全部暴露。建議候選:
DM:
get_combat_context
start_quick_combat
add_combatant
request_initiative / resolve initiative orchestration
perform_combat_action
resolve_combat_adjudication
advance_turn
end_combat
Player:
get_combat_context
perform_combat_action
respond_to_reaction
若 perform_combat_action schema過大,可按 Attack / Cast / Freeform拆工具,但各 tool仍只轉入同一 Combat action service。
不要提供:
set_enemy_hp_raw
set_current_turn_raw
set_combat_json
DM Direct Edit若需要 MCP,必須是明確 DM-only typed repair tool,且 AI DM是否允許依既有「AI DM不能 Undo」產品限制審慎處理;P4預設不要為 AI DM開 raw repair escape hatch。
M04 guide / tool descriptions / get_session_context.briefing同源規則繼續適用。Active Combat的 mandatory loop至少包含:
wait_for_event,不要停在 host chat。P4若新增 web migrations,建立 P4_POSTGRES_URL測試名稱可以,但 CI同時必須提供所有直接被跑到的 legacy env,避免 silent skip。至少 review P2 / P3 migration suites實際讀哪些 env後明確設定。
P4 full-stack workflow可沿用或演進既有 P3 Full-Stack E2E,但 phase closeout evidence必須能具名指出 P4 Combat journey的 workflow run;不可只說「某次 regression綠」。若建立新 workflow,名稱建議:
P4 Full-Stack E2E
同 P3:
data/srd5.1/ 的 Monster Template content可以存在於 standalone ContentRegistry;content registry載入 Monster definition本身不等於 Standalone支援 Combat。app.domain.combat若屬 multiplayer,加入 standalone forbidden import coverage;boundary test / regex應針對 multiplayer module / table,不得因名稱含 monster 就 blanket reject共用 Monster Template content。/api/meta capability保持 combat=false / route unavailable。P4不得先做 P5 spatial schema,但 Combat Engine的非空間 abstraction要讓 P5加 geometry而不是重寫:
mode可演進到 tactical。position_note與 P5 token coordinate分離。perform_combat_action前提供 range / target / OA facts,取代部分 DM adjudication,而不是建立另一套 attack engine。P4不得:
combat_rolls取代 P3 RollRequest。combat_state包全部 canonical truth而沒有 DB invariant / concurrent transition guard。max(order)+1 / local current turn等無鎖方式處理併發。