Skip to main content

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.OwnerId and Front.OwnerId were renamed to FactionKind in .secs source, 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 stays int.
  • A new hash constant H.FactionKind = 0x6B6309DA4345C951UL was added to Generated/Hashes.cs. Force/Front no longer reference H.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 to long end-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, 556 was fixed: (int)settlementId casts removed from ExploreTerritory calls. The method signature changed from int? ownerId to long? 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 / PendingChoiceSave protocol 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:

  1. Picks a concrete Forces feature to ship (smallest playable: raise → move → see on map?)
  2. Answers each design question below in the context of needing it for that feature
  3. Wires Add* helpers, EntityTypes registration, and only the bridge arms the feature exercises
  4. Writes tests against the feature's behavior
  5. Leaves remaining bridge arms throw InvalidOperationException for 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.

ScopeWhat it representsParent scope (walks_to)
ForceAn army. Owns doctrine, strength, supply, lifecycle, primary officer Character, current vs home territory, FactionKind (Crown/Allied/Hostile).self (top-level entity)
ForceDetachmentSub-unit carved out of a Force. Has kind, strength, committed flag, optional assigned territory/site.Force
OperationDiscrete tactical operation a Force is executing (raid, siege, scout, …). Has phase, status, progress, target territory/site/route/force/objective.Force
DefenseZoneProvince-level defensive grouping. Has doctrine, reserve %, priority, assigned force, fallback zone, supply depot site, RoE.ProvinceDefensePlan, Province
DefenseZoneMemberA territory/site/route the zone is defending.DefenseZone
DefenseZoneThreatPriority"If threat-kind X at target Y, this is priority Z" sortable rule.DefenseZone
ProvinceDefensePlanProvince-level doctrine container owning DefenseZones.Province
FrontCrown-scale grouping ("the Northern Front"). HQ territory, status, casualty tolerance, FactionKind.self (top-level entity)
FrontProvincePlanRefLink from a Front to a ProvinceDefensePlan it coordinates.Front
FrontForceRefLink from a Front to a Force it directs.Front
FrontObjectiveCampaign goal under a Front (kind, status, priority, target).Front
FrontSupplyLineSupply 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 (@HomeTerritory and @Territory as 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 WalkScope arm for Force→Territory in HostBridge.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?

OptionCrownAlliedHostileReserved 0?
A — 0/1/2 dense012No
B — 1/2/3 with 0=unset123Yes — 0 = Unset
C — bitfield (faction sets)0x010x020x04No
  • A matches the C# enum default (first member = 0). Risk: a newly-allocated ForceData defaults to FactionKind = 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 TerritoryKnowledgeState precedent (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.Crown instead of force.FactionKind == 0. Matches TerritoryKnowledgeState, ItemSlotKind precedent.
  • No — keep raw int consistent with sibling enum-coded fields on the same scope (Doctrine, LifecycleState, Status, SupplyState are all raw int with inline comments listing values).

In code

  • Sub-question 2A affects: ForceData.cs defaults, AddForce helper signature, any factory test fixtures, the Client TypeScript display logic.
  • Sub-question 2B affects: whether a new FactionKind.cs enum file is created in Host/Data/, whether ForceData.FactionKind is typed int or FactionKind, and whether the SECS field declaration stays SecsTypeRef.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 with FactionKind=0 is a loud bug; a Force with FactionKind=1 is explicitly Crown.
  • 2B: Yes — add enum FactionKind. Matches Valenar's established *Kind pattern 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.md documentation (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 versus AddForce(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, SetCasualtyTolerance all 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), AssignForce on 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:

CommandPatternWhy
Force.SetDisplayName(string)APure field write
Force.SetDoctrine(int)APure field write
Force.SetReservePercent(int)A or BIf reserve % change should recompute strength split, then B
Force.AssignHome(long)BShould reset current location + trigger resupply?
Force.AssignOfficer(long)A or BIf officer change should recompute morale, then B
ForceDetachment.SetStrength(int)A or BIf detachment strength change should recompute parent Force totals, then B
ForceDetachment.SetCommitted(int)BReserves vs committed redistribution
Operation.SetTarget(long)A or BIf target change should reset progress, then B
Operation.AdvancePhase()BGates by status, updates progress
Operation.Begin() / Cancel()BLifecycle transitions
DefenseZone.*mostly A; AssignForce probably BPure field writes mostly
ProvinceDefensePlan.SetDefault*APure field writes
Front.SetDisplayName / SetHq / SetStatus / SetCasualtyToleranceAPure field writes
FrontObjective.SetStatus(int)A or BIf 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 reached comment.
  • 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.md so 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:

ScopeFieldLikely semanticRationale
ForceDoctrine, ReservePercent, Strength*, Supply*, LifecycleState, FactionKindPass-through (mutable)All represent state that changes during gameplay
ForceHomeTerritoryId, CurrentTerritoryId, CurrentRouteId, CurrentSiteId, PrimaryOfficerCharacterIdPass-through (mutable, but maybe gated by command)Position changes during movement
ForcePlacementKindPass-through OR immutable?Is placement (e.g., "field army" vs "garrison") fixed at creation or can it change?
ForceDetachmentParentForceId (host-only, no SECS field)N/A — no bridge armNot exposed to SECS
ForceDetachmentKind, Strength, IsCommitted, AssignedTerritoryId, AssignedSiteIdPass-throughDetachments adjust through Force operations
OperationKind, Phase, Status, Progress, Target*, DoctrineOverridePass-through (or Pattern B commands?)Operations evolve
DefenseZoneDoctrine, ReservePercent, Priority, AssignedForceId, Fallback*, RulesOfEngagementPass-throughPlans adjust
DefenseZoneMemberMemberKind, TerritoryId, SiteId, RouteIdPass-through (or immutable: "remove + re-add" pattern?)Membership might be add/remove only, not mutate
DefenseZoneThreatPriorityPriority, ThreatKind, ThreatTarget*Pass-throughPriorities tune
ProvinceDefensePlanDefaultDoctrine, DefaultReservePercentPass-throughPlans adjust
FrontFactionKind, Status, HqTerritoryId, CasualtyTolerancePass-throughFront state evolves
FrontProvincePlanRefProvinceId, ProvinceDefensePlanIdImmutable (no-op) or pass-through?Is "remap" allowed mid-game or is it "remove + re-add"?
FrontForceRefForceId (declared as H.AssignedForceId)Immutable (no-op) or pass-through?Same question
FrontObjectiveKind, Status, Priority, Target*Pass-throughObjectives evolve
FrontSupplyLineRouteId, From/ToTerritoryId, StatusPass-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/WriteLong arm 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 on FactionKind == 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: AddForce helper, EntityTypes[id] = H.Force, Force scope 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.AssignHome command (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:

  1. Force→Territory walk policy: Home / Current / Both (with new sigil)
  2. FactionKind integer mapping: 0/1/2 dense / 1/2/3 with 0=Unset / Bitfield
  3. enum FactionKind C# companion type: Yes / No
  4. 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.
  5. 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.
  6. Add* helper API style: Record-style (5A) / Named-parameter (5B) / Builder (5C)
  7. 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:

  1. Writes the chosen first Forces feature in gd-forces.md (or a dedicated gd-forces-phase-b.md) to lock in the design
  2. Adds Add* helper(s) for the scope types the feature touches in GameWorld.cs
  3. Adds EntityTypes[id] = H.<Scope> registration in those helpers
  4. Wires the ReadInt/ReadLong arms in HostBridge.cs for every field the feature reads
  5. Wires the WriteInt/WriteLong arms for every field the feature writes (default pass-through except parent-pointers)
  6. Wires the WalkScope arms for any cross-scope walks the feature exercises
  7. Wires the CallScopeCommand arms for commands the feature uses
  8. Wires the GetChildren arms for collections the feature iterates
  9. Adds tests against the feature's behavior (creation, mutation, display)
  10. Updates examples/valenar/Client/src/api/types.ts for any ReadModel/Snapshot wire-shape additions
  11. 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 .secs declarations: 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 in examples/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 OwnerIdFactionKind rename: 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:

  1. Read the gameplay framing
  2. Compare the options
  3. Pick (or note "want to explore both with prototypes first")
  4. 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.