Forces Phase B — Open Design Questions
Purpose
This document captures the open design decisions that gate the Force-family bridge implementation in Valenar. The questions are gameplay-significant, not implementation trivia — each choice shapes how Forces behave at the table.
Take this doc into a design-focused conversation. Answer the questions with intent; the answers then drive a focused implementation wave that wires the Force-family scope dispatch into the host bridge.
Do not implement based on guesses. Each question below has at least two defensible answers. Picking by coincidence-of-implementation-order is exactly the kind of "tune parameters as a substitute for fixing architecture" the project tenets reject.
What's already decided (do not re-litigate)
The prior implementation session locked in the following. They are not open questions.
Force.OwnerIdandFront.OwnerIdwere renamed toFactionKindin.secssource,Generated/Declarations.cs,Host/Data/ForceData.cs,Host/Data/FrontData.cs, both ReadModels, both Snapshot records, and the Client TypeScript wire types. Investigation confirmed this is a 3-value enum (Crown/Allied/Hostile), not an entity-id-backref. The field staysint.- A new hash constant
H.FactionKind = 0x6B6309DA4345C951ULwas added toGenerated/Hashes.cs. Force/Front no longer referenceH.OwnerId; that constant is now reserved for the 6 genuine entity-ref OwnerIds (Province, Territory, Site, Camp, Skill, Item). - Every other entity-id-backref
OwnerId(Province/Territory/Site/Camp/Skill/Item) was promoted tolongend-to-end (.secs+Declarations.cs+ Host/Data + HostBridge ReadInt→ReadLong + WriteLong arms). Wave G + Wave H1. - A live silent-truncation bug at
examples/valenar/Host/GameRuntime.cs:553, 556was fixed:(int)settlementIdcasts removed fromExploreTerritorycalls. The method signature changed fromint? ownerIdtolong? ownerId. - Build is green, 686 tests pass, TypeScript compiles clean. The codebase is in a coherent state ready for the Forces Phase B work to slot in.
- Stable option identity remains DEFERRED. The future
EventOption.Key/ResolveChoiceByKey/PendingChoiceSaveprotocol stays unimplemented. Forces Phase B should not touch this surface.
Why this is one focused wave, not a series of patches
The Force-family domain is mechanically absent. examples/valenar/Host/GameWorld.cs:42-56 declares typed dictionaries for Forces, ForceDetachments, Operations, DefenseZones, DefenseZoneMembers, DefenseZoneThreatPriorities, ProvinceDefensePlans, Fronts, FrontProvincePlanRefs, FrontForceRefs, FrontObjectives, FrontSupplyLines — but the dictionaries are always empty. No Add* helpers exist. No EntityTypes[id] = H.Force registration ever happens. No HostBridge.cs arm answers (scopeId == H.Force, ...) reads. No SECS system in Generated/Systems/ walks or reads Force scopes today.
This means three things:
- The bridge gap is honestly absent, not wrongly wired. Engine reads of Force-family fields throw
InvalidOperationException— loud failure, not silent corruption. - There are no callers to verify implementation against. Any bridge wired today would be structurally pattern-matched against Settlement/Skill/Camp/Item bridge sections, not behaviorally validated.
- The 5+ design decisions below will become concrete the day a Forces feature gets scheduled (e.g., "raise a Crown Force and march it to a territory") because the feature can't ship without picking answers. Picking now without that feature context risks baking in wrong answers.
The right shape is a single Phase B wave that:
- Picks a concrete Forces feature to ship (smallest playable: raise → move → see on map?)
- Answers each design question below in the context of needing it for that feature
- Wires
Add*helpers, EntityTypes registration, and only the bridge arms the feature exercises - Writes tests against the feature's behavior
- Leaves remaining bridge arms
throw InvalidOperationExceptionfor the next feature wave to fill in
This document is the pre-work for that wave: answer the questions first, then dispatch implementation.
Scope reference — the 12 Force-family scopes
Source: examples/valenar/Content/forces/scopes.secs (299 lines). Existing gameplay docs: gd-forces.md, gd-fronts.md, gd-defense-zones.md, gd-operations.md.
| Scope | What it represents | Parent scope (walks_to) |
|---|---|---|
Force | An army. Owns doctrine, strength, supply, lifecycle, primary officer Character, current vs home territory, FactionKind (Crown/Allied/Hostile). | self (top-level entity) |
ForceDetachment | Sub-unit carved out of a Force. Has kind, strength, committed flag, optional assigned territory/site. | Force |
Operation | Discrete tactical operation a Force is executing (raid, siege, scout, …). Has phase, status, progress, target territory/site/route/force/objective. | Force |
DefenseZone | Province-level defensive grouping. Has doctrine, reserve %, priority, assigned force, fallback zone, supply depot site, RoE. | ProvinceDefensePlan, Province |
DefenseZoneMember | A territory/site/route the zone is defending. | DefenseZone |
DefenseZoneThreatPriority | "If threat-kind X at target Y, this is priority Z" sortable rule. | DefenseZone |
ProvinceDefensePlan | Province-level doctrine container owning DefenseZones. | Province |
Front | Crown-scale grouping ("the Northern Front"). HQ territory, status, casualty tolerance, FactionKind. | self (top-level entity) |
FrontProvincePlanRef | Link from a Front to a ProvinceDefensePlan it coordinates. | Front |
FrontForceRef | Link from a Front to a Force it directs. | Front |
FrontObjective | Campaign goal under a Front (kind, status, priority, target). | Front |
FrontSupplyLine | Supply route between two territories, with status. | Front |
A complete strategic hierarchy: Crown polity → Front → ProvinceDefensePlan → DefenseZone → DefenseZoneMember, with Forces and Operations attached at various levels.
Open question 1 — Where does a Force "stand" for terrain modifiers?
At the table
A Force has two territory references in its host data:
- HomeTerritoryId — where the Force is based (its garrison province)
- CurrentTerritoryId — where the Force currently is (could be home, could be marching, could be at a battle site, could be in friendly territory en route)
When a SECS modifier or channel says "scout-trained Forces get +10% effective speed in their territory," which territory does "their territory" mean?
Option 1A — Home base (terrain modifiers anchor to garrison)
"A Force gets bonuses for the kind of land it was raised in. Mountain-raised mountaineers stay mountaineers wherever they march."
- Gameplay flavor: identity stays with the unit. A desert-raised Force keeps its desert-tolerance bonus even when marching through temperate plains. Like Total War "armies fight better near home" or HoMM creature ecology.
- Strategy implication: raising a Force in good terrain becomes a long-term investment.
- Counter: can feel weird when an obviously-not-on-home-terrain Force still pulls home-terrain modifiers.
Option 1B — Current position (terrain modifiers anchor to where army is)
"Force performance reflects current battlefield terrain. Marching into a swamp is bad for everyone."
- Gameplay flavor: geography shapes combat directly. Like Crusader Kings battlefield terrain, Field of Glory environment modifiers, or Civilization combat bonuses.
- Strategy implication: terrain becomes a tactical lever — bait enemies into bad terrain.
- Counter: harder to plan around since modifiers shift turn-by-turn as forces move.
Option 1C — Both, depending on the modifier (most expressive, most complex)
"Discipline modifiers anchor to Home (training is sticky); supply / fatigue / battlefield modifiers anchor to Current."
- Gameplay flavor: richest, but requires deciding per-modifier which axis applies.
- Implementation: two separate scope walks needed (
@HomeTerritoryand@Territoryas distinct sigils). - Counter: complexity tax on every future modifier author.
In code
The bridge's WalkScope(forceHandle, H.Territory) answers exactly one of these. The choice is made once in code (~20 lines in HostBridge.cs), but affects every modifier and channel that walks Force→Territory thereafter.
If Option 1C is chosen, the scope-language grammar needs a way to disambiguate at the source level — likely a new @HomeTerritory sigil that compiles to a separate walk path.
Orchestrator recommendation
Option 1B (Current position) unless there's a clear flavor reason to pick Home. CK3/Total War/most strategy games default to current-terrain modifiers because they create more dynamic gameplay (terrain becomes a tactical lever). Option 1C is tempting but adds permanent grammar surface for marginal expressiveness — defer until concrete modifier examples demand it.
Downstream impact
- Determines the body of the
WalkScopearm for Force→Territory inHostBridge.cs - Affects every existing/future modifier that says
@Territory.<field>in a Force-context formula - Whether Operation→Territory walks should match (probably yes — Operations inherit their parent Force's "current location" sense)
Open question 2 — FactionKind: integer values and typed-enum decision
At the table
Force.FactionKind and Front.FactionKind are 3-value enums:
- Crown — player-owned Force/Front, acts under player's Crown polity
- Allied — friendly faction's Force/Front, acts cooperatively with player
- Hostile — enemy faction's Force/Front, targets of player Operations
The field's existence is settled. Two sub-questions remain.
Sub-question 2A — Integer mapping
What integer value represents each faction?
| Option | Crown | Allied | Hostile | Reserved 0? |
|---|---|---|---|---|
| A — 0/1/2 dense | 0 | 1 | 2 | No |
| B — 1/2/3 with 0=unset | 1 | 2 | 3 | Yes — 0 = Unset |
| C — bitfield (faction sets) | 0x01 | 0x02 | 0x04 | No |
- A matches the C# enum default (first member = 0). Risk: a newly-allocated
ForceDatadefaults toFactionKind = 0 = Crown, meaning unset Forces silently look player-owned. - B reserves 0 for "uninitialized so a creation bug is loud (Force exists but no faction assigned)." Matches the
TerritoryKnowledgeStateprecedent (0 = Unknown). - C allows future "Force could be both Allied and Hostile to different observers" combinations. Probably premature; YAGNI applies.
Sub-question 2B — Typed C# enum companion
Should there be a public enum FactionKind { Crown, Allied, Hostile } (or { Unset, Crown, Allied, Hostile }) C# type in Host/Data/?
- Yes — host code writes
force.FactionKind == FactionKind.Crowninstead offorce.FactionKind == 0. MatchesTerritoryKnowledgeState,ItemSlotKindprecedent. - No — keep raw
intconsistent with sibling enum-coded fields on the same scope (Doctrine,LifecycleState,Status,SupplyStateare all raw int with inline comments listing values).
In code
- Sub-question 2A affects:
ForceData.csdefaults,AddForcehelper signature, any factory test fixtures, the Client TypeScript display logic. - Sub-question 2B affects: whether a new
FactionKind.csenum file is created inHost/Data/, whetherForceData.FactionKindis typedintorFactionKind, and whether the SECS field declaration staysSecsTypeRef.Int(which it must — the engine doesn't know about C# enums).
Orchestrator recommendation
- 2A: Option B (0=Unset, 1=Crown, 2=Allied, 3=Hostile). Matches the existing safe-default precedent (
TerritoryKnowledgeState). A Force withFactionKind=0is a loud bug; a Force withFactionKind=1is explicitly Crown. - 2B: Yes — add
enum FactionKind. Matches Valenar's established*Kindpattern for typed enum companions. Cost is one tiny enum file; benefit is every host call-site reads as English.
Downstream impact
- The chosen integer mapping is committed in
gd-forces.mddocumentation (so map markers and tooltips display correctly) - The Client TypeScript may want a parallel TS enum or numeric-literal type if Sub-question 2B = yes
AddForce(factionKind: FactionKind, ...)signature versusAddForce(int factionKind, ...)is determined by Sub-question 2B
Open question 3 — Command dispatch pattern per scope command
At the table
Force-family scopes expose ~23 commands like Force.SetDoctrine(int), Force.AssignHome(long), Force.AssignOfficer(long), Operation.AdvancePhase(), DefenseZone.AssignForce(long), Front.SetStatus(int), etc.
When a SECS system calls one of these commands, the engine routes it through HostBridge.cs:CallScopeCommand. The bridge has to dispatch to host code that actually performs the change.
There are two established patterns in the codebase:
Pattern A — Through the command buffer (Territory.Reveal style)
// Engine emits via CommandBuffer:
ctx.Commands.SetScopeField(forceHandle, H.Force, H.Doctrine, value);
// Bridge receives via WriteInt:
if (fieldId == H.Doctrine) { force.Doctrine = value; return; }
// Effect: field-level write, queued at tick end, no side effects.
- Fits when: the command is "just write this scalar value."
SetDoctrine,SetReservePercent,SetCasualtyToleranceall fit. - Side effects: none beyond the field write.
Pattern B — Direct mutation with side effects (Character.AdvanceLead style)
// Engine emits via CommandBuffer:
ctx.Commands.CallScopeCommand(forceHandle, H.Scope_Force_AssignHome_Long_Void, args);
// Bridge directly mutates host structures, possibly multiple fields atomically:
// world.Forces[forceId].HomeTerritoryId = args.Territory;
// world.Forces[forceId].CurrentTerritoryId = args.Territory; // also reset current
// world.Forces[forceId].SupplyState = (int)SupplyState.Resupplying; // trigger resupply
// Effect: multi-field atomic side effect; bridge owns the semantics.
- Fits when: the command has implied side effects beyond writing one field.
AssignHome(resets current location + supply state?),AdvancePhase(gates by status, updates phase + status + progress reset),AssignForceon DefenseZone (assigns + bumps priority + updates Front coordination?). - Side effects: explicit. Documented inline in the bridge arm.
In code
For each of the ~23 commands, a decision is needed per command. Most likely:
| Command | Pattern | Why |
|---|---|---|
Force.SetDisplayName(string) | A | Pure field write |
Force.SetDoctrine(int) | A | Pure field write |
Force.SetReservePercent(int) | A or B | If reserve % change should recompute strength split, then B |
Force.AssignHome(long) | B | Should reset current location + trigger resupply? |
Force.AssignOfficer(long) | A or B | If officer change should recompute morale, then B |
ForceDetachment.SetStrength(int) | A or B | If detachment strength change should recompute parent Force totals, then B |
ForceDetachment.SetCommitted(int) | B | Reserves vs committed redistribution |
Operation.SetTarget(long) | A or B | If target change should reset progress, then B |
Operation.AdvancePhase() | B | Gates by status, updates progress |
Operation.Begin() / Cancel() | B | Lifecycle transitions |
DefenseZone.* | mostly A; AssignForce probably B | Pure field writes mostly |
ProvinceDefensePlan.SetDefault* | A | Pure field writes |
Front.SetDisplayName / SetHq / SetStatus / SetCasualtyTolerance | A | Pure field writes |
FrontObjective.SetStatus(int) | A or B | If status transition has side effects |
The design call per command is: does this change have observable side effects beyond the field, or is it a pure scalar set?
Orchestrator recommendation
- Default to Pattern A for all SetXxx fields. Simpler, more uniform, less per-command code.
- Pattern B only where side effects are explicit and documented. The current Character.AdvanceLead is a good template: its bridge arm has a clear
// Side effects: progresses lead, sets territory, bumps milestone counter if reachedcomment. - Defer all questions about which commands need Pattern B to the Forces Phase B feature wave. When you ship "raise a Force," you only need
AddForce(a host method, not a SECS command) — no scope commands need to ship in that first feature wave.
Downstream impact
- Each Pattern B command needs explicit side-effect documentation in
gd-forces.md/gd-operations.md/gd-defense-zones.mdso designers don't accidentally double-trigger them - Pattern B commands are harder to test because they touch multiple fields atomically; tests need fixture state setup for the side effects
Open question 4 — Write-arm immutability semantics per scope-field
At the table
When a SECS system writes a field via WriteInt or WriteLong, the bridge can:
- Pass-through: apply the write to host data (
force.Doctrine = value;) - No-op: silently ignore the write (
return;with a comment explaining why)
Some fields are conceptually mutable (a Force's Doctrine can change mid-game), others are conceptually immutable through the engine (a Skill's OwnerId is set at creation by the host and the engine should not be able to reassign it).
Existing precedents
- Mutable (pass-through): Settlement.Gold, Territory.OwnerId (was a live truncation site, now correct), Character.Stamina, every "current state" field
- Immutable (no-op with explicit
_ = X; return;pattern + rationale comment): Skill.OwnerId, Camp.OwnerId, Item.OwnerId — the "owning Character" of these child entities, set at entity-creation, never reassigned via SECS
Force-family field-by-field decision
For each Force-family writable field, decide which semantic applies:
| Scope | Field | Likely semantic | Rationale |
|---|---|---|---|
| Force | Doctrine, ReservePercent, Strength*, Supply*, LifecycleState, FactionKind | Pass-through (mutable) | All represent state that changes during gameplay |
| Force | HomeTerritoryId, CurrentTerritoryId, CurrentRouteId, CurrentSiteId, PrimaryOfficerCharacterId | Pass-through (mutable, but maybe gated by command) | Position changes during movement |
| Force | PlacementKind | Pass-through OR immutable? | Is placement (e.g., "field army" vs "garrison") fixed at creation or can it change? |
| ForceDetachment | ParentForceId (host-only, no SECS field) | N/A — no bridge arm | Not exposed to SECS |
| ForceDetachment | Kind, Strength, IsCommitted, AssignedTerritoryId, AssignedSiteId | Pass-through | Detachments adjust through Force operations |
| Operation | Kind, Phase, Status, Progress, Target*, DoctrineOverride | Pass-through (or Pattern B commands?) | Operations evolve |
| DefenseZone | Doctrine, ReservePercent, Priority, AssignedForceId, Fallback*, RulesOfEngagement | Pass-through | Plans adjust |
| DefenseZoneMember | MemberKind, TerritoryId, SiteId, RouteId | Pass-through (or immutable: "remove + re-add" pattern?) | Membership might be add/remove only, not mutate |
| DefenseZoneThreatPriority | Priority, ThreatKind, ThreatTarget* | Pass-through | Priorities tune |
| ProvinceDefensePlan | DefaultDoctrine, DefaultReservePercent | Pass-through | Plans adjust |
| Front | FactionKind, Status, HqTerritoryId, CasualtyTolerance | Pass-through | Front state evolves |
| FrontProvincePlanRef | ProvinceId, ProvinceDefensePlanId | Immutable (no-op) or pass-through? | Is "remap" allowed mid-game or is it "remove + re-add"? |
| FrontForceRef | ForceId (declared as H.AssignedForceId) | Immutable (no-op) or pass-through? | Same question |
| FrontObjective | Kind, Status, Priority, Target* | Pass-through | Objectives evolve |
| FrontSupplyLine | RouteId, From/ToTerritoryId, Status | Pass-through (Route maybe immutable?) | Supply lines tune; can route change? |
In code
For each field, the WriteInt/WriteLong arm in HostBridge.cs either:
- Pass-through:
if (fieldId == H.X) { data.X = value; return; } - No-op:
if (fieldId == H.X) { _ = data; return; }with a rationale comment
There are roughly 60-70 writable fields across the 12 scopes. Most are obvious pass-through; a handful (the parent-pointer fields like FrontProvincePlanRef.ProvinceId) need an explicit call.
Orchestrator recommendation
- Default to pass-through for every scalar state field. Doctrine, Strength, Status, Priority, etc.
- Immutable no-op only for parent-pointer fields on child scopes (FrontProvincePlanRef → ProvinceId, FrontForceRef → ForceId, DefenseZoneMember → TerritoryId/SiteId/RouteId). The pattern is "child entities can be added or removed; rewiring an existing child's parent is a remove + add."
- Defer to Forces Phase B feature wave for any specific field where the answer isn't obvious. Each ambiguous case becomes a 5-minute call when a feature actually needs to write to that field.
Downstream impact
- Determines
WriteInt/WriteLongarm bodies for every Force-family field - Determines whether SECS systems can use
ctx.Commands.SetScopeField(...)to write a field (mutable) or have to call a scope command method (which can handle add/remove semantics)
Open question 5 — Add* helper API design
At the table
Force-family entities don't exist today. To create them, host code (eventually invoked by UI / SECS systems) needs Add* methods on GameWorld. The shape of these methods is a design choice.
Look at existing patterns:
// AddSettlement signature (existing):
public long AddSettlement(SettlementData data)
{
var id = AllocateEntityId();
Settlements[id] = data;
EntityTypes[id] = H.Settlement;
return id;
}
This takes a fully-populated SettlementData record and returns the assigned entity id. The caller pre-builds the record.
For Forces, two shapes are possible:
Option 5A — Record-style (matches AddSettlement)
public long AddForce(ForceData data)
{
var id = AllocateEntityId();
Forces[id] = data;
EntityTypes[id] = H.Force;
return id;
}
- Caller builds
new ForceData { FactionKind = (int)FactionKind.Crown, HomeTerritoryId = t1, PlacementKind = 0, Doctrine = 1, ... }with every field. - Pro: terse, uniform with existing pattern, all initialization is visible at the call site.
- Con: caller must know every required field; missing fields silently default to 0 (which for FactionKind = Unset under option 2A-B).
Option 5B — Named-parameter style with required + optional
public long AddForce(
long homeTerritoryId,
long primaryOfficerId,
FactionKind faction = FactionKind.Crown,
int doctrine = 1,
int placement = 0,
int reservePercent = 30)
{
var data = new ForceData {
HomeTerritoryId = homeTerritoryId,
CurrentTerritoryId = homeTerritoryId,
PrimaryOfficerCharacterId = primaryOfficerId,
FactionKind = (int)faction,
Doctrine = doctrine,
PlacementKind = placement,
ReservePercent = reservePercent,
// ...sensible defaults for SupplyState, LifecycleState, etc.
};
var id = AllocateEntityId();
Forces[id] = data;
EntityTypes[id] = H.Force;
return id;
}
- Caller passes the minimum required values; the helper fills in safe defaults for everything else.
- Pro: easier to call correctly; required fields are explicit in the signature; mis-construction is harder.
- Con: doesn't match existing AddSettlement/AddTerritory pattern; per-scope helper code is more substantial.
Option 5C — Builder pattern
public long AddForce(ForceBuilder build)
{
var data = build.ToForceData();
// ...
}
// Caller:
world.AddForce(new ForceBuilder()
.HomeTerritory(t1)
.Officer(c1)
.Crown()
.Doctrine(Aggressive));
- Most expressive, but introduces a new pattern not seen elsewhere in the codebase.
- Probably overkill for the current scale; YAGNI applies.
In code
The choice affects GameWorld.cs substantially (one Add* method per scope type = 12 methods, plus EntityTypes registration each), and affects every caller (UI, tests, future SECS systems that create entities via host-mediated paths).
Orchestrator recommendation
- Option 5A (record-style) for uniformity with the existing AddSettlement/AddTerritory pattern. Trades safety for consistency.
- OR Option 5B if the codebase already has any precedent for named-parameter helpers — I didn't find one in the existing scopes but a fresh design agent should check.
- Add an
EnsureForceDefaults(ForceData data)validation method in either case that throws onFactionKind == 0(under option 2A-B) or other obvious uninitialized values. Makes "I forgot to set the faction" a loud bug rather than a silent default-to-Crown.
Downstream impact
- Affects every future call site that creates a Force/Front/Operation/DefenseZone/Front
- Tests will use these helpers — choosing 5A vs 5B affects test fixture readability
Bonus question — Which Force-family feature ships first in Phase B?
The wave can't wire all 12 scope bridges at once — there's nothing exercising them. Pick a concrete smallest playable Forces feature:
Option A — "Raise a single Crown Force"
- UI: button on Settlement panel to spawn a Force tied to the settlement's territory.
- Bridge needs:
AddForcehelper,EntityTypes[id] = H.Force,Forcescope ReadInt/ReadLong arms for fields the UI displays. - Doesn't need: Operations, DefenseZones, Fronts, walks, commands.
Option B — "Raise a Force and march it to an adjacent territory"
- Adds:
Force.AssignHomecommand (Pattern B?),WalkScope(force, H.Territory)(Option 1 decision needed), basic movement system.
Option C — "Raise a Force, march to adjacent territory, attack a hostile site"
- Adds: Operations (Operation scope arms, Operation.SetTarget, Operation.Begin), more walks, target validation.
Option D — Defensive: "Designate a DefenseZone and assign a Force to defend it"
- Adds: DefenseZone scope arms, ProvinceDefensePlan scope arms, DefenseZone.AssignForce command.
Option E — Strategic: "Create a Front, attach a ProvinceDefensePlan, observe Force coordination"
- Largest scope; touches Front + ProvinceDefensePlan + FrontProvincePlanRef + FrontForceRef + cross-scope walks.
The earlier the question, the easier the design call:
- Option A: ~30 of the 186 bridge arms; ~0 cross-scope walks; ~0 commands needed
- Option B: ~50 arms; 1 walk; 1 command
- Option C: ~80 arms; 3 walks; 4 commands
- Option D: ~70 arms; 2 walks; 3 commands
- Option E: ~120 arms; 5 walks; 6 commands
Orchestrator recommendation
Option A first ("raise a single Crown Force"). Smallest valuable feature. Lets the team validate the bridge wiring pattern, the AddForce API, the snapshot/ReadModel flow, and the Client wire shape with minimum surface area. Subsequent features (movement, operations, defense zones, fronts) extend the bridge incrementally — each adds 30-50 more arms against a feature that actually exercises them.
Summary — Decisions needed before Phase B implementation can dispatch
Answer the following with intent before any code touches the Force-family bridge:
- Force→Territory walk policy: Home / Current / Both (with new sigil)
- FactionKind integer mapping: 0/1/2 dense / 1/2/3 with 0=Unset / Bitfield
enum FactionKindC# companion type: Yes / No- Per-command dispatch pattern (Pattern A vs B) for the ~23 Force-family scope commands. Likely default A; Pattern B only where side effects are explicit. Can be answered command-by-command as features need them.
- Per-field write-arm semantics (pass-through vs immutable-no-op). Default pass-through; immutable only for parent-pointer fields on child scopes. Can be answered field-by-field as features need them.
Add*helper API style: Record-style (5A) / Named-parameter (5B) / Builder (5C)- First Forces feature to ship in Phase B: Option A / B / C / D / E above
Questions 4 and 5 can be partially answered — defaults set now, specific commands/fields answered when needed. Questions 1, 2, 3, 6, 7 should be decided up-front.
Implementation scope summary (for whoever dispatches Phase B)
After the questions above are answered, the Phase B implementation wave:
- Writes the chosen first Forces feature in
gd-forces.md(or a dedicatedgd-forces-phase-b.md) to lock in the design - Adds
Add*helper(s) for the scope types the feature touches inGameWorld.cs - Adds
EntityTypes[id] = H.<Scope>registration in those helpers - Wires the ReadInt/ReadLong arms in
HostBridge.csfor every field the feature reads - Wires the WriteInt/WriteLong arms for every field the feature writes (default pass-through except parent-pointers)
- Wires the WalkScope arms for any cross-scope walks the feature exercises
- Wires the CallScopeCommand arms for commands the feature uses
- Wires the GetChildren arms for collections the feature iterates
- Adds tests against the feature's behavior (creation, mutation, display)
- Updates
examples/valenar/Client/src/api/types.tsfor any ReadModel/Snapshot wire-shape additions - Subsequent Forces features extend the bridge incrementally as needed
Bridge arms remaining unwired stay throwing InvalidOperationException — they advertise their absence loudly, the right behavior until a feature exercises them.
References
- Current Force-family
.secsdeclarations:examples/valenar/Content/forces/scopes.secs(299 lines) - Existing Force gameplay docs:
gd-forces.md,gd-fronts.md,gd-defense-zones.md,gd-operations.md(all inexamples/valenar/docs/systems/) - Host data records:
examples/valenar/Host/Data/ForceData.cs,FrontData.cs, plus 10 others in the same directory - Bridge file (where 186 arms eventually land):
examples/valenar/Host/Bridge/HostBridge.cs - Generated declarations:
examples/valenar/Generated/Declarations.cs:688-777(Force-family rows) - Generated hashes:
examples/valenar/Generated/Hashes.cs(Force-family scope/field/command constants) - ReadModels (already exist, ready for wire-shape extension):
examples/valenar/Host/ReadModels/ForcesReadModel.cs,FrontsReadModel.cs,OperationsReadModel.cs,DefenseZonesReadModel.cs,ProvinceDefensePlansReadModel.cs - Stable-identity DEFERRED status (do not touch in Phase B):
docs/design/04-behavior.md:573,docs/design/10-host-secs-execution-boundary.md § 19.5 / 20.3 - Investigation that locked in the
OwnerId→FactionKindrename: prior session's Wave H2 investigation explorer report (this doc supersedes that record for the design-question summary)
How to use this doc in the design conversation
Bring it to a design-focused agent (or a teammate) and walk through the questions in order. For each one:
- Read the gameplay framing
- Compare the options
- Pick (or note "want to explore both with prototypes first")
- Note any cross-cutting implications
When all questions are answered, the Phase B implementation wave becomes mechanical. The waves that built up to this point (Waves D through H) all leaned on the same discipline: when a question is design-laden, surface it cleanly and answer it before code touches it. This doc keeps the Forces Phase B work honest to that pattern.