60 KiB
SHD.build architecture
The team builds the project in vertical slices.
Application stack
- SvelteKit 2 / Svelte 5 with TypeScript
- Embedded Turso Database via
@tursodatabase/databasefor durable application data - Auth.js-compatible server authentication with Discord OAuth and credentials providers
- Argon2id password hashing, server-side sessions, CSRF protection, and role checks
- Browser
localStorageonly for anonymous build drafts
Product boundaries
- Public: browse, sort, and view published builds and guides.
- Anonymous: create and edit local drafts.
- Authenticated: publish, rate, comment, favorite, copy, and manage owned builds.
- Editor: manage builder source data.
- Admin: manage users, roles, source data, and audit history.
Server code checks authorization for each mutation. UI visibility has no effect on server authorization.
Builder and shared Build pages currently expose only deterministic totals from
the equipped items, selected rolls, fixed bonuses, inherited profiles, augments,
and active set bonuses. Conditional Talent outcomes, weapon and Skill DPS,
encounter assumptions, activation profiles, rotations, and simulation remain
planned. Explicit COMING SOON panels identify those player-facing
capabilities.
Weapon and gear range calculations must preserve decimal endpoints and follow
the tested progression rules in
STAT_LEVEL_SCALING.md. Leveling is linear rather than
compounded. The presentation layer truncates game-matching integer values.
Local credentials use Argon2id PHC strings with a unique random salt per password. Current work factors meet OWASP's 19 MiB / 2 iteration / single-lane minimum profile. Successful login rehashes credentials with outdated parameters.
Database runtime
The application uses Turso Database in-process through its native Node package. It opens a local SQLite-compatible database file. The application does not use Turso Cloud, libSQL HTTP, remote replicas, auth tokens, or synchronization.
The database defaults to ./data/shd-build.db. Production deployments must
provide a persistent writable filesystem and can override the location with
TDR_DATABASE_PATH. This rules out ephemeral serverless filesystems unless a
deployment attaches a persistent volume.
Site launch controls
The singleton site_launch_settings row stores the public launch gate, account
creation state, and availability for News, Database, Builds, Guides, and
Builder. Schema migration creates the row with Coming Soon enabled, account
creation enabled, and all five sections enabled. The application caches the row
for five seconds and clears the process cache after an operator saves changes.
Agents with site.settings.manage edit the row from ISAC → System Configuration
→ Site Controls. The save action requires an explicit value for all seven
controls and a confirmation token. A successful update records
system_configuration / site_access.updated with the operator, target row, and
complete before/after snapshots.
The request hook applies the launch gate to anonymous traffic and section availability to every request:
- With Coming Soon enabled, anonymous
GETandHEADrequests redirect to/coming-soon. Anonymous mutations outside exempt routes receive a JSON 404. - Authenticated agents bypass the Coming Soon gate and enter the application.
- A disabled public section blocks its route family for every user, including
authenticated agents:
/news,/database,/builds,/guides, or/builder.GETandHEADrequests redirect to/coming-soonwith the unavailable section identified. Mutations receive a JSON 404. - Authentication callbacks, sign-in, contact, legal pages, health endpoints, scheduler endpoints, application assets, and the prelaunch page remain reachable without a session.
The global navigation omits disabled sections for every user. Server checks
remain authoritative for direct URLs and mutations. An authenticated operator
may open /coming-soon to preview the promotional page, but the preview does
not override section availability.
Account creation uses a separate control. Closing registration blocks password registration, a magic-link request for an unknown email, and Discord OAuth for an unknown Discord identity. Existing password, email, and Discord identities can still sign in. The sign-in page hides the password registration form and explains registration closure. This guard runs on the server, so a crafted form submission or OAuth callback cannot create an account while registration is closed.
Community content moderation
Comments, profile biographies, custom avatars, and Guides share the moderation
lifecycle documented in CONTENT_MODERATION.md. Local
rules and the configured provider classify each submission before publication.
Clean submissions can publish without human work. Flagged submissions and
fail-closed provider errors enter the appropriate Oversight queue.
Agents may report visible content they do not own. Reports enter Oversight without hiding the content. Human decisions require a reason and append an audit event. Long-form edits that need review use immutable pending revisions so the last approved biography or Guide remains public.
ISAC Site Controls owns provider behavior, content scopes, thresholds, link and PII checks, and allowed or banned terms. API credentials remain in server environment configuration.
Guides and News articles
articles stores the stable identity, kind, owner, immutable slug, visibility,
and published or pending revision pointers for Guides and News. Authors edit one
mutable article_drafts row with optimistic version checks. Publication creates
an immutable article_revisions snapshot. Public readers and search load only
the revision named by articles.published_revision_id.
Guides require OpenAI moderation before an unlisted or public revision can
publish. The moderation request includes the title, deck, document text, image
alternative text and captions, cover image, and inline images. A failed provider
request stays in Guide Oversight. Clean revisions may publish automatically;
flagged revisions preserve the previous public revision while Oversight decides.
News bypasses this lifecycle because news.manage is the editorial gate. News
comments still use community moderation.
Ubisoft News syndication
The Ubisoft News importer reads the English-language Division Resurgence feed from Ubisoft Nimbus. It stores each Nimbus item ID as the stable external ID and publishes the source title, excerpt, canonical Ubisoft link, and validated Ubisoft-hosted thumbnail. SHD.build does not copy the article body or image file. A bounded same-origin endpoint proxies each thumbnail so opening the News listing does not send visitor metadata to Ubisoft. News cards link to the canonical Ubisoft article. Imported articles use Ubisoft's original publication timestamp rather than the import time.
external_news_sources maps each Nimbus ID to one SHD.build article. A SHA-256
content hash covers the normalized title, excerpt, canonical URL, and source
publication timestamp. An unchanged hash updates last_seen_at without creating
a revision. Thumbnail changes update the audited source metadata without
creating an article revision. A changed content hash creates and publishes an
immutable revision. The importer does not delete or unpublish an article when
Nimbus omits it from a later feed, and it does not restore an article that an
editor deleted. The Newsroom treats source-linked articles as read-only so local
edits cannot retain Ubisoft attribution. The importer defers future-dated items
until their source publication time.
The seeded, non-public Ubisoft Official News service identity owns imported
articles and acts on their audit events. A first import records
editorial_content / news.imported; a later content sync records
editorial_content / news.import_synced. Audit snapshots identify the provider,
Nimbus ID, canonical URL, thumbnail metadata, source timestamp, and resulting
revision.
The existing bearer-authenticated scheduler endpoint invokes the importer. A successful run sets the next check one hour later. The importer stores the last attempt, last success, next check, consecutive failure count, and bounded last error in provider state. Failures retry after 15 minutes with exponential backoff capped at six hours. A successful run clears the error and failure count.
The article document is a versioned, strictly validated Tiptap JSON envelope.
The application stores no author-supplied HTML and renders accepted nodes with
Svelte components. article_revision_build_embeds supports several Builds per
Guide and several Guides per Build. Each relation records a bounded publication
snapshot. Authors may embed any Build they own; another Agent's Build must be
public and becomes unavailable in the Guide if its source stops being public.
Guide taxonomy combines fixed activity and difficulty terms with active
Specializations, Focuses, and weapon categories. article_search stores the
published title, deck, body text, and tags. Weighted token scoring prioritizes
title, deck, tags, then body because the embedded Turso engine does not ship the
FTS5 module. Search rows exist only for current public revisions.
Registered and anonymous visitors may rate a Guide from 1 through 5. Anonymous ratings and Guide reports use a random HTTP-only browser token; the database stores only its domain-separated hash. Repeated anonymous activity requires a Turnstile result. Guide reports reference the exact published revision so a report against an older revision cannot hide newer work.
The Builder stores loadout data and displays deterministic equipped-item totals.
It has no Guide body. Shared Build pages use the same player-facing calculation
boundary. An owned Build page links to /guides/new?build=<id>, where the
server verifies ownership and initializes a removable Build block in a new
private Guide.
Discord-compatible Build sharing experiment
Persisted public and unlisted Builds expose absolute Open Graph metadata for
their canonical /builds/[slug] URL. Their og:image and Twitter card metadata
reference the same-origin 1200 × 630 PNG at /builds/[slug]/preview.png so
Discord and other link unfurlers can render a Build summary. Private Builds set
noindex,nofollow, expose no crawler image, and provide no usable sharing
control.
The preview endpoint derives its image from canonical Build facts and the current known-stat summary in the shared Build projection. It includes the Build title, author, visibility, direct URL, equipped weapon names, core totals, and up to three supporting Attributes. The renderer performs no external fetches. It normalizes and bounds every text field by grapheme count, escapes text before inserting it into XML, builds a text-only SVG, and rasterizes that SVG to PNG with Sharp. Production installs DejaVu Sans from the Debian package and the renderer names that fixed family explicitly.
The endpoint hashes a versioned serialization of the image input for its bounded
in-process render cache and derives the HTTP ETag from the resulting PNG bytes.
A bounded request queue limits concurrent database projections and Sharp work.
Responses require cache revalidation so a visibility change to Private blocks
the next request instead of leaving a stale response window. Matching
conditional requests receive 304 Not Modified. Private, missing, and
inaccessible Builds receive no preview. Unlisted images send the
X-Robots-Tag: noindex response header.
For eligible Builds, the Build Uplink invokes the native Web Share API as the primary action when the browser supports it. An explicit Copy Link action remains available for every public or unlisted persisted Build. This experiment does not install a Discord bot, post to Discord, extend Discord OAuth scopes, or store Discord tokens or webhooks. Discord receives only a link that an Agent chooses to share and may fetch the public metadata and same-origin preview.
Public data corrections and discussion
Every public database card links to its canonical ISAC record with an exact
item ID and offers a moderated Community Discussion. Authenticated Agents can
post notes and replies; Community Oversight may pin one visible comment as the
record's Community Note. A public correction opens the same ISAC editor used by
data operators. Contributor-mode submissions create a proposal rather than
changing canonical data. Contributors can attach an optional bounded rationale;
the rationale is retained with the proposal and visible to Data Oversight, but
does not become part of the canonical action replay.
Increment plan
- Product shell and anonymous local-draft builder.
- Embedded Turso migrations, repositories, and seed data.
- Credentials and Discord authentication.
- Full equipment/stat builder driven by database definitions.
- Publishing, discovery, ratings, comments, favorites, and copy flow.
- Rich build guides and moderation.
- Admin editors, validation, audit log, and data import/export.
Administrator bootstrap
The authentication service promotes the first authenticated user to admin if
the database does not contain an administrator. Promotion uses one conditional
database update so concurrent first sign-ins cannot both become administrators.
Once an administrator exists, later sign-ins never alter roles.
Database backup and restore
Embedded Turso databases contain contributor-entered source data and must be preserved across development changes.
Create a timestamped development backup:
npm run db:backup
Create a production backup:
npm run db:backup:production
The scripts write versioned JSON documents beneath data/backups/ by default.
Git ignores that directory because backups contain private user data, including
password hashes.
To restore, first create an empty database and apply the schema. Then run:
npm run db:restore -- data/backups/<backup-file>.json
The restore operation refuses to import into any database containing records. This guard prevents accidental merging or overwriting of contributor data. Stop the application before backup or restore so the embedded database file is not locked by another process.
Development database maintenance
Treat the development database as persistent data. Stop this project's development server before backup, export, restore, migration, or other work requiring exclusive access. Create and verify a JSON backup, perform the prepared operation, verify schema state and critical record counts, then restart and health-check the application. Prefer additive, repeatable migrations and retain the verified backup if maintenance fails.
Production and staging remain shared persistent environments and retain the
deployment, backup, synchronization, and maintenance safeguards documented in
docs/DEPLOYMENT.md.
ISAC audit and compensating reversals
Every successful ISAC mutation appends an event to audit_log through the
shared server audit utility. Events identify the operator, operational category,
specific action, target entity, timestamp, and useful before/after evidence. The
audit registry is read-only and requires audit.view; the seeded Auditor role
grants that permission without granting mutation capabilities.
Audit history is immutable. The server implements “Revert” as a new compensating mutation rather than deletion or modification of the original event:
- The operator must hold
audit.revertin addition to any other access. - The reversion registry must support the original action and the event must contain a usable before-snapshot.
- The UI requires a second, explicit confirmation.
- The server reloads the target and compares it with the original after-snapshot, ignoring bookkeeping timestamps. A mismatch indicates newer work and blocks automatic compensation.
- The server revalidates referenced roles, permissions, and relationships before restoration. Restoring administrator authority also requires full administrative access.
- The server restores the previous state and appends an
audit.revertedevent whose target is the original audit-event ID. - The server rejects a second reversion of the same event.
Moderated game-data contributions
ISAC data access uses data.view, data.propose, data.manage, and
data.approve. Data Viewers can inspect registries. Data Contributors can
submit the same editor forms, but the server serializes each request into
data_change_proposals instead of mutating canonical tables. Data Oversight
reviews the queue and must confirm approval or rejection.
Approval replays the stored form through its original SvelteKit server action.
This preserves canonical validation, relational updates, icon processing, and
the normal game-data audit event. Proposal creation and review decisions also
produce separate data_moderation audit events. Failed canonical validation
marks a proposal as failed without changing live data. Pending counts appear
alongside each data registry in ISAC so collaborators can see concurrent
suggestions.
The protected seeded roles are Data Viewer, Data Contributor, Data Editor, and Data Oversight. Data Editor retains direct-write access for trusted operators; Data Oversight combines direct-write access with queue review. New ISAC data modules must use the shared moderation interception path and audit both the final mutation and proposal lifecycle.
Mechanic programs remain part of their owning source record. Talent, OS Protocol, Skill Chip, and Specialization proposals carry the complete typed program with the rest of the source payload. Oversight reviews and approves that program through the source module's canonical validation path. There is no standalone Trigger proposal module or pending Trigger dependency.
Each authenticated, active registered agent receives isac.access, data.view,
and data.propose as baseline permissions in the authorization layer. The
authorization layer grants this baseline without a removable role assignment, so
existing users and each account-creation method receive the same access.
Registered agents can inspect live source data and submit moderated proposals,
but cannot mutate canonical records, review the queue, or access unrelated ISAC
operations without additional permissions. Unsupported destructive identity
operations, account deletion, incomplete snapshots, stale targets, and events
whose dependencies no longer exist are marked non-reversible or rejected.
Recovery for those cases requires a specialized workflow rather than inferred
database writes.
System metrics
ISAC uses /isac as its canonical route namespace. The server redirects legacy
/admin URLs to their matching /isac location so old notifications and
external bookmarks remain usable.
The read-only /isac/metrics route requires metrics.view and collects a
point-in-time process-local health snapshot. Node and operating-system APIs
provide process CPU and memory, event-loop utilization and delay, host memory,
load average, core count, platform, and uptime. Embedded Turso pragmas provide
page allocation, page size, and free-page information; aggregate SQL queries
provide application record volumes.
The interface refreshes on demand and may poll every ten seconds while the operator keeps auto-refresh enabled. CPU and event-loop values are deltas from the previous server-side sample, so the first sample and samples separated by long gaps provide direction rather than a monitoring-grade time series. Production alerting and historical analysis should use a dedicated external telemetry system. This surface omits secrets, environment-variable values, and full database paths.
The same surface reports remove.bg paid credits and free API calls as separate
capacity pools. The scheduler reconciles the account balance no more often than
the configured interval, while successful image processing applies the
provider's X-Credits-Charged value to the persisted estimate. Warning and
critical transitions create one alert episode with a unique delivery for each
active Administrator. A later critical transition may alert again; repeated
scheduler runs within the same episode do not. Provider errors preserve the last
known balance and never expose the API key or raw response body.
Weapon category bonuses
weapon_category_attribute_bonuses stores three ordered fixed Attribute
contributions for each Shotgun, Rifle, MMR, AR, LMG, and SMG category. Each
profile grants 5% Weapon Critical Hit Chance and 50% Weapon Critical Hit Damage.
Shotgun, Rifle, AR, LMG, and SMG grant 40% Headshot Damage; MMR grants 70%.
Weapon records do not own copies of these rows. Both builder catalog loaders attach the matching category profile when they hydrate a weapon. Exotic weapons retain an underlying category and inherit the same profile as a standard weapon of that type. The combat summary preserves the existing equipped-weapon semantics: each equipped weapon contributes its inherited profile to the Agent's accumulated Attributes.
ISAC exposes one category-profile editor above the weapon registry. Operators
may change the three positive values, while the Attribute identities and order
remain fixed. Each successful save appends a
game_data/weapon_bonus_profile.updated audit event with the complete previous
and resulting profile.
Referenced exotic weapon talents
Standard weapon talent pools remain reusable across weapons and use Talent 1 /
Top or Talent 2 / Bottom. Contributors create dedicated talents in the Talent
Registry under Exotic Weapon. The database stores these records as Weapon
talents with Exotic scope and no standard slot.
An exotic weapon references one active exotic talent through
weapons.exotic_talent_id. The weapon editor lists registered exotic talents
and identifies talents assigned to another weapon. Server validation rejects
standard talents, inactive talents, and talents already assigned elsewhere.
Changing or clearing a weapon relationship leaves the previous talent active and
available for another assignment.
Tier I and its modeled mechanic are required when a contributor saves an exotic
talent. Tier II may remain undocumented. An undocumented Tier II has no
talent_tiers row or conditional-effect value variant. A later Talent Registry
edit adds the Tier II projection and mechanic values without changing the weapon
reference.
OS Protocol progression
OS Protocols use dedicated normalized tables because an assigned Talent has six
calculated states: its base value plus five upgrades. Each protocol owns one
os_protocol_primary_effects record and may own one os_protocol_talents
record. The optional Talent owns its mechanic program through the shared
conditional-effect tables. ISAC keeps the Talent workspace collapsed until an
operator assigns or opens it, and removing an existing Talent requires explicit
confirmation.
Dedicated profile tables normalize the three secondary attributes. One
os_protocol_secondary_profiles record exists for each configured quality +
attribute-type combination, with its three fixed stat/value pairs in
os_protocol_secondary_effects. Every matching protocol inherits that profile;
secondary values are not copied onto individual protocol records.
The unique primary value uses base × (1 + (level − 1) / 49) for levels 1–50,
while talent effects use base × (1 + upgrades / 5) for zero to five upgrades.
Server actions validate all relationships and append the full owned graph to the
existing game_data audit history.
Weapon Mods and tiered weapon slots
weapon_mod_slots records each weapon's available slot vocabulary per tier. The
editor captures one shared set for T1/T2 and the single shared slot omitted at
T0, then expands that input into the normalized per-tier rows. Operators may
save partial rows when the in-game configuration is unavailable. ISAC derives
CONFIG REQUIRED until every available tier contains four unique slots at T0 or
five at T1/T2. Regular Weapon Mods declare a single slot type and one or more
compatible base weapon categories through weapon_mod_compatible_categories;
eligibility requires both a category match and the slot to exist on the equipped
weapon at its selected tier.
The eight code-defined slot types have stable UUIDs and one shared icon each.
Operators manage the artwork in the Weapon Mod registry through the game-data
icon pipeline. Each upload or removal appends a
game_data/weapon_mod_slot_icon.updated audit event, and contributors may send
the same action to Data Oversight. Public weapon cards use icon-only slot chips
when artwork exists, with the slot name available to screen readers and on
hover. Cards show the name when artwork is missing and retain the visible T1+
marker for slots unlocked at Tier 1.
Exotic Weapon Mods instead reference one exotic_weapon_id. They do not carry
regular category compatibility. The builder treats them as permanent equipment
and lets the user change only their level. They do not use quality profiles,
random primary rolls, secondary pools, or regular Mod Effects. Their canonical
contributions are fixed attribute references with explicit level-1 and level-20
values in weapon_mod_exotic_attributes; intermediate values use linear
interpolation.
All regular Weapon Mods permit Firepower, Toughness, or Engineering as a
builder-time primary choice. weapon_mod_quality_values stores each
definition's level-1 and level-20 primary roll endpoints for each quality;
primary values use linear interpolation from levels 1–20.
weapon_mod_secondary_options stores one global secondary Attribute pool for
all regular Weapon Mods. Each Attribute has Level 1 and Level 20 minimum and
maximum ranges for Specialized, Superior, and High-end quality through
weapon_mod_secondary_quality_values; values at intermediate levels use linear
interpolation. Secondary capacity is fixed by quality: Standard has zero,
Specialized has one, Superior has two, and High-end has two. A build selects
distinct Attributes from the pool up to that capacity. Individual Weapon Mod
records do not own secondary options or ranges. Pool updates append a
game_data/weapon_mod_secondary_pool.updated audit event. Mod Effects remain
triggerless, hold one or two Attribute references, and store fixed per-quality
values in weapon_mod_effect_quality_values.
Gear Mods and tier slot capacity
Gear Mod definitions are standard-quality records only; there is no exotic Gear Mod branch. Every definition supplies primary roll ranges for Standard, Specialized, Superior, and High-end quality at Levels 1 and 20. Values between those endpoints use linear interpolation. A build instance selects one primary attribute from Firepower, Toughness, Engineering, Health, or Armor.
One global Gear Mod secondary Attribute pool serves every Gear Mod definition.
Each Attribute has Level 1 and Level 20 minimum and maximum ranges for
Specialized, Superior, and High-end quality through
gear_mod_secondary_quality_values; values at intermediate levels use linear
interpolation. Secondary capacity is fixed by quality: Standard has zero,
Specialized has one, Superior has two, and High-end has two. A build selects
distinct Attributes from the pool up to that capacity. Individual Gear Mod
records do not own secondary options or ranges. Gear tier still determines mod
slot capacity without per-item configuration: T0 gear has one Gear Mod slot; T1
and T2 gear have two.
The gear_mods and gear_mod_quality_values tables store individual definition
data. gear_mod_secondary_options and gear_mod_secondary_quality_values store
the family-wide pool. Pool updates append a
game_data/gear_mod_secondary_pool.updated audit event. Individual definition
mutations retain the gear_mod.created, gear_mod.updated,
gear_mod.archived, and gear_mod.unarchived audit actions. Contributor
submissions use the gearmods proposal module and remain non-canonical until
approved by Oversight.
Skill Chip quality scaling
Each Skill Chip definition belongs to one specialization ability. Each set owns one 2-piece bonus and may own one 3-piece Talent with a typed conditional rule. ISAC keeps the complete Talent workspace, including its description and mechanic program, inside one collapsible optional section.
All definitions use one fixed primary profile. The profile contains Skill
Duration, Skill Radius, Skill Cooldown Recovery, and Skill Intensity, with
direct minimum and maximum values for Standard, Specialized, Superior, and
High-end. Primary values apply to the assigned ability rather than the Agent.
Builder and shared Build pages label them NOT YET DEFINED and exclude them
from current Agent and combat totals.
All definitions also use one shared secondary Attribute pool. Each pool entry stores direct minimum and maximum values for Specialized, Superior, and High-end. Standard chips have no secondary slot, Specialized chips have one, and Superior and High-end chips have two. A configured chip selects distinct secondary Attributes up to its quality's capacity. Skill Chips have no item level, so neither profile uses level interpolation.
Active definitions reference an active specialization ability through
skill_chip_sets.specialization_ability_id. An ability has capacity for three
active definitions. The save and restore actions enforce that capacity, and the
schema requires the foreign key for each row. A specialization focus is
builder-ready only when each of its three assigned abilities has all three
active Skill Chip definitions. Skill Chip mutations refresh readiness for
focuses that reference both the previous and resulting ability.
Every equipped quality counts toward the two-piece threshold. After the loadout
meets that threshold, only equipped High-end chips increase the interpolation
step. For h High-end chips, where h is zero through three, a configured
minimum and maximum resolve as minimum + (maximum − minimum) × h / 3. Zero
High-end chips resolve to the minimum, while three resolve to the maximum. The
editor stores endpoints and previews all four states.
ISAC presents the fixed primary profile and secondary pool as two shared
registry/editor cards above the definition registry. Successful profile saves
append game_data/skill_chip_primary_profile.updated and
game_data/skill_chip_secondary_pool.updated, respectively. Definition
mutations continue to capture their owned bonus, optional Talent, ability
relationship, and safe icon metadata in the game_data audit stream.
Mechanic programs and loadout state
Mechanic programs belong to the record whose behavior they describe. One
conditional_effect_rules row owns the program for one standard or exotic
Talent, OS Protocol Talent, Skill Chip Talent, or Specialization tree rank. The
source_type and source_id pair identifies that owner. No standalone Trigger
record or shared Trigger relationship participates in execution.
The mechanic runtime accepts normalized timeline events for each equipped
mechanic. Current player pages do not construct this timeline or invoke the
runtime. movement_snapshot.values.state is immobile, moving, or
sprinting; the mechanic's moving and sprinting scenario controls select that
state, with sprinting taking precedence. Enabled Skill activation profiles emit
skill_deployed events on the same cadence as Skill calculations and identify
the deployed ability. An on_event Skill modifier applies to that activation
until the ability's next deployment or the encounter ends.
skill_deployed.values.abilityId and skill_hit.values.abilityId identify the
same ability for mechanics that bind an effect to the next deployed Skill.
Each source supplies a fixed value-variant contract:
- Standard Talents require Tier I and Tier II values. Exotic Talents require Tier I and may omit Tier II until contributors document its game values. Runtime calculations use Tier I while an equipped exotic Talent has no Tier II mechanic variant.
- OS Protocol Talents store a base value. An outcome may use an expression that
reads
equipment.protocolTalentUpgrades, from zero through five, so selected values can scale to twice the base at upgrade five. - Skill Chip Talents store minimum and maximum values for quality composition interpolation.
- Specialization tree-rank effects store the rank's base value.
The rule row stores the script version, source-defined scenario-input controls, configuration state, and one of three simulation-readiness states:
ready: the runtime has enough typed data to calculate the mechanic.requires_assumptions: the mechanic needs one or more scenario inputs before the runtime can include it.captured_only: ISAC preserves the authored mechanic, but the runtime cannot calculate it.
The code-defined MECHANIC_EVENT_REGISTRY is the event authority. Its 28
normalized events are shot_fired, weapon_hit, skill_deployed,
signature_ability_deployed, skill_hit, skill_multishot_triggered,
agent_hit, health_damage_taken, damage_dealt, enemy_killed, assist,
ally_downed, agent_downed, ally_rescued, grenade_thrown,
medical_kit_used, healing_applied, extra_health_granted,
extra_health_changed, movement_snapshot, proximity_snapshot,
firing_started, firing_stopped, reload_started, reload_completed,
weapon_swapped, and tick. Event details such as target, distance, critical
state, source, movement, and nearby counts travel as typed event values. The
event registry publishes authoritative field metadata where branches need a
bounded contract. For those events, the mechanic editor derives its event-field
controls from that metadata. For example, weapon_hit.values.headshot is a
boolean that distinguishes headshots from body shots. enemy_killed and
assist may carry values.assignedAbilityActive; a Skill Chip Talent uses that
boolean to restrict a kill or assist trigger to its assigned ability's active
period. skill_deployed and signature_ability_deployed may carry
values.assignedAbilityDurationSeconds to start assigned-ability state. The
runtime expires that state before it evaluates another event at the same
timestamp.
weapon_hit.values.aiming records whether the Agent was aiming down sights when
the hit resolved. A producer omits the field when it lacks authoritative aiming
state; the runtime does not replace missing evidence with a scenario assumption.
Predicates compare this field explicitly with true or false; negation and
inequality cannot turn missing evidence into a match. A weapon_hit or
skill_hit event may include an event-target-centered area sample. The bounded
sample lists up to 256 valid ally and enemy entity IDs with their distances from
the hit target and includes that target as an enemy at zero meters. Duplicate
IDs, negative distances, or a missing center target invalidate the evaluation.
extra_health_changed is Agent-scoped. Normalized timeline producers emit it
when the Agent receives an Extra Health grant, when the remaining amount
changes, and when it is depleted to zero. Every event requires the boolean
values.hasExtraHealth. An event may include the non-negative
values.currentExtraHealth; when present, the amount must agree with the
boolean status. extra_health_granted represents a grant action and remains a
separate event from changes to the Agent's current Extra Health state.
The optional Action Family catalog provides authoring recipes for common mechanics. Applying a recipe copies its typed scenario inputs, state pools, and clauses into the source-owned program. Those branches become editable authoritative data; the recipe key remains provenance, not a runtime dependency. Adding another recipe appends branches. Replacing a program with a recipe requires confirmation because replacement discards the current branch graph.
A Talent may save an empty captured_only program while an author records its
name, category, image, and source description. The registry marks that Talent
CONFIG REQUIRED, and calculations exclude it. Changing readiness to ready or
requires_assumptions restores strict branch, numbered-outcome, and
description-placeholder validation.
The program graph uses three child tables:
-
conditional_effect_state_poolsdefines agent- or target-scoped flags, counts, stacks, durations, scalar values, marks, and resources. Each pool owns its bounds, expiry policy, refresh policy, and best-case value. -
conditional_effect_clauseslistens for one event from the registry. A clause owns a typed predicate, reducer, optional cooldown, optional weight expression, state mutations, and optional recipe provenance. Reducers convert event streams into counts, distinct or consecutive matches, sliding windows, durations, intervals, or derived values. Mutations can set, add, consume, clear, derive, or transfer state. A mutation may bind a target-scoped pool to the current event target, event actor, or another typed entity selector. Each target owns an independent value and expiry timer. A clause may require active assigned-ability state and carryprocChancePercent. The runtime evaluates that chance after predicates and cooldown checks, before reducers, mutations, and outcomes. -
conditional_effect_outcomesapplies Attribute modifiers, direct damage, restoration, resource costs, or markers. Each outcome names its target scope, value basis, optional resource or Attribute, combat timing, optional state band, and phase:on_event,after_event,on_enter,on_exit, orwhile_active. One typed value or expression exists for every variant required by the owning source. The common value models are static values, live Attribute values, percentage-normalized Attribute values, event-field values, reducer output, source-defined scenario values, and OS Protocol upgrade scaling. An outcome may carryprobabilityPercent; each eligible outcome rolls separately. Restoration outcomes may deliver their total immediately or continuously over an explicit duration.Target-scoped direct damage may use
event_target_areatargeting for an explosion around its event target. This targeting selects valid enemies and stores a positive radius in meters. It is valid only foron_eventorafter_eventoutcomes. The legacywithin_radiusshape remains descriptive targeting metadata and does not select runtime entities.
The generic runtime in src/lib/calculations/mechanic.ts processes combat
events in timestamp order. Predicates decide whether an event enters a clause.
Reducers convert event streams into activations or derived values. The runtime
applies state mutations, enforces pool bounds and expiry, emits outcomes in
phase order, and records telemetry for each activation.
When event_target_area damage activates, the runtime requires the triggering
event's authoritative area sample. It includes valid enemies whose distance is
less than or equal to the configured radius, sorts them by entity ID, and emits
one equal damage outcome for each entity. The outcome chance resolves once
before fan-out. A missing sample invalidates the evaluation instead of producing
single-target damage or treating the area as empty.
Custom evaluation uses a Mulberry32 stream seeded by randomSeed; an omitted
seed resolves from the stable mechanic program ID. The returned evaluation
reports the seed. Maximum mode resolves every nonzero clause or outcome chance
as successful. extend_duration changes only an active assigned ability's
remaining duration. start_cooldown_on_max writes an expiring Agent cooldown
pool only when an earlier mutation in the same clause transitions a capped stack
pool to its maximum.
Each write to an expiring pool refreshes its timer. When a pool expires,
on_exit outcomes evaluate stack thresholds and per-stack expressions against
the final value before reset. The runtime records the bound target on emitted
outcomes. A one-second target debuff with a one-second damage cadence can
therefore resolve once at its refreshed shared expiry without inventing
intermediate ticks.
Live Attribute references use a stable Attribute ID and read the authoritative loadout total that the builder captures for the evaluation pass. Percentage Attributes retain their displayed units, so an expression that applies a coefficient to a percentage divides the Attribute value by 100. Evaluation fails when the loadout does not supply a referenced Attribute; the runtime does not substitute zero.
The read-only combat.currentHealthPercent reference divides resolved current
Health by resolved maximum Health and returns a value from 0 through 100. The
runtime derives it when a predicate is evaluated instead of storing a second
Health value that could become stale. Pre-event timing reads the combat snapshot
before mechanic branches process the event. Current timing observes
deterministic resource changes from earlier branches at the same event.
Health-band mechanics use strict upper bounds and inclusive lower bounds, so
bands do not overlap and an exact threshold enters the next weaker band.
Maximum-potential evaluation applies the same predicates before emitting passive
outcomes.
Continuous restoration resolves the outcome's total when the branch activates, then delivers that total at a constant rate over the configured duration. Activations create independent streams, overlapping streams add, and restoration above the resource maximum is lost when it occurs. The runtime integrates delivery at event and expiry boundaries before processing the event at that timestamp, which keeps results independent of an arbitrary tick interval.
Periodic behavior requires an interval in the reducer. The runtime does not
infer a one-second cadence from missing data. A mechanic with an unknown cadence
uses requires_assumptions, identifies the missing input, and stays out of the
calculation until an operator or builder scenario supplies it.
The runtime derives a read-only pseudo-language listing from the typed program. ISAC uses that listing to explain events, predicates, reducers, state changes, phases, and formulas. Planned player calculations can reuse it when those pages consume mechanic outcomes. No route accepts the generated text as input, and no server process executes it. Operators edit structured fields; the formatter regenerates the listing after each change.
AI-assisted mechanic drafting
Manual mechanic authoring remains the complete authoring path. The AI assistant produces an optional draft for the existing structured editor. It cannot save, publish, approve, or submit game data, and it cannot alter application code. An author may ignore, revise, or replace any generated field through the manual controls.
The shared mechanic editor exposes the mechanic assistant for standard and exotic Talents, OS Protocol Talents, Skill Chip Talents, and Specialization tree-rank effects. Calculation revisions use a separate assistant contract because their typed stage schema and publication authority differ from source-owned mechanic programs.
The server owns a versioned mechanic-authoring prompt and context builder. Each request receives the source type, progression contract, submitted game text, current draft when supplied, code-defined mechanic registries, and the active ISAC Attribute catalog. Attribute context includes stable IDs, names, units, descriptions, and calculation metadata. The client cannot replace the system instructions, choose arbitrary models, attach files, add tools, or add external URLs.
The deterministic draft parser owns the canonical outcome-capability manifest. The AI context serializes that manifest instead of maintaining a second outcome grammar. The parser rejects outcome fields absent from the manifest. Contract tests build a draft from each advertised capability example and pass it through the parser, so CI blocks a prompt that advertises unsupported fields. A mechanic grammar change must update the manifest and example, pass the round-trip test, and increment the prompt contract version before release.
The OpenAI integration uses the Responses API with Structured Outputs, no tools,
and store: false. The application stores the bounded clarification history
needed for the current drafting session. Requests use a fixed server-configured
model, a privacy-preserving user safety identifier, input and output limits,
timeouts, duplicate-request protection, and per-agent rate limits. The API key
remains on the server.
The model returns one structured result:
clarification_requiredcontains bounded questions whose answers affect the calculation model.draft_readycontains the typed mechanic draft, assumptions, warnings, readiness state, and a short explanation.capability_gapnames unsupported runtime behavior, the nearest supported representation, an optional workaround, and the schema, runtime, editor, reference, and test changes a developer would need to add support.
For a mechanic capability gap, the client builds a developer prompt from the
exact analyzed wording, clarification transcript, source profile, and structured
gap response. The author can copy that prompt without another model request. The
author can also submit the gap to active full administrators. The submission
endpoint validates bounded fields, confirms that the request ID belongs to the
author's audited capability_gap generation, suppresses repeat submissions,
sends mandatory ISAC notifications, and appends a mechanic_gap.submitted event
to the ai_assistance audit category. The notification links to the filtered
audit evidence. Submission does not mutate the mechanic draft or parent
game-data record.
The server validates draft_ready output against the response schema and the
same deterministic mechanic parser used by manual authoring. Unknown events,
Attributes, value models, scopes, operators, reducers, or outcomes fail
validation. The client receives no draft when local validation fails.
The author reviews a validated draft and chooses whether to apply it to the unsaved editor. Applying a draft changes client form state only. The existing module action performs canonical validation, contribution interception, approval, persistence, and game-data audit logging after the author initiates the normal save flow.
The ai.mechanic.generate permission authorizes generation and clarification
requests. The server checks it for every request. Agents without the permission
may see the disabled assistant entry point, but they retain all manual controls
allowed by their data role.
Successful generation operations append sanitized events under the
ai_assistance audit category. Events identify the operator, source type,
prompt-context version, model, result type, token usage, request ID, and
validation result. Audit snapshots exclude the API key, environment
configuration, hidden model reasoning, authentication data, and unbounded prompt
content. Applying a draft changes client state and creates no server event. The
owning module's game-data audit event remains the record of any saved mechanic
change.
AI-assisted calculation-revision drafting
The calculation assistant converts an operator's plain-language combat-model request into an optional draft revision. It receives the current active or edited revision, the calculation-stage vocabulary, weapon and Skill profile contracts, and the active ISAC Attribute catalog. Its versioned prompt, structured response schema, validation, and application adapter remain separate from mechanic drafting.
The assistant returns clarification questions, a validated revision draft, or a calculation capability gap. A capability gap identifies the missing stage, operator, curve, binding, runtime behavior, or validation rule and lists the application and test changes needed to support it. The server rejects unknown stage keys, locked-position changes, duplicate bindings, invalid Attribute references, unsupported operators, and malformed curve or cap values through the deterministic calculation-revision parser.
The ai.calculation.generate permission authorizes calculation drafting and
clarification requests. It does not grant revision mutation or publication.
Applying a result changes the open revision form in the client. The operator
must review and save that draft through the existing revision action. The
separate calculation.publish permission and activation confirmation remain
mandatory before the saved draft becomes the active authored model. Current
player pages do not consume that model.
Successful calculation-generation requests append sanitized events under
ai_assistance. These events use calculation-specific actions, identify the
base revision and generated contract version, and follow the same request,
token-usage, result, and secret-exclusion rules as mechanic-assistance events.
Applying a draft creates no server event. Existing calculation_configuration
events remain the evidence for revision saves and activation.
Source authors define planned player scenario controls as typed toggle, slider,
or numeric inputs. Each input records its label, guidance, bounds when
applicable, default value, and best-case value. ISAC retains these definitions
for continued mechanic authoring. Current Builder and shared Build pages do not
render or evaluate them. Their calculation panels display COMING SOON instead.
Loadout schema version 6 stores one focus, selected tree ranks, nine Skill
Chips, independent T1/T2 choices for each equipped gear and weapon Talent, a
unified conditional-effect state map, encounter assumptions, per-ability
activation controls, and calculationMode. Runtime readers accept the exact
version 6 structure. A backed-up migration translated version 5 equipment
Talents from their previous equipment-tier-derived behavior. The schema retains
its simulation fields, but the current player UI ignores them. Version 4 remains
obsolete prototype data.
Planned player calculation modes
Loadout v6 defines maximum and custom modes for the planned simulation UI:
- Maximum mode supplies each scenario input's best-case value, selects peak conditional-effect state, and runs selected abilities on cooldown against their configured target caps. The interface hides granular assumptions in this mode.
- Custom mode uses the agent's encounter inputs, effect states, activation
rates, target counts, and other supported assumptions. A missing required
input produces
REQUIRES_ASSUMPTIONSrather than a fabricated value.
Saved and shared loadouts preserve the selected mode as compatibility data. The
current Builder and shared Build UI does not expose a mode selector, read
encounter assumptions, or execute either mode. Explicit COMING SOON panels
reserve the player-facing space for these calculations.
Specialization ability attacks
specialization_ability_attacks stores intrinsic base damage, Blast, Pierce, or
Shredding damage, direct or periodic delivery, hit or tick counts, cast time,
target cap, and charge behavior. skill_attribute_calculation_profiles binds
Attributes to the versioned Skill damage and cadence stages. Cooldown Recovery
changes charge and activation readiness. Duration changes the number of periodic
ticks.
Periodic delivery waits one configured tick interval before its first tick and excludes ticks at or after the encounter boundary. The Skill engine reevaluates active modifier windows at each delivered hit or tick. A missing tick interval marks the attack as requiring configuration; the engine does not supply a default cadence.
Versioned combat calculations
calculation_revisions supplies one active rule set. Operators can change draft
revisions, while active and retired revisions stay immutable.
calculation_stages stores the ordered typed pipeline. The base-damage stage
holds the first position, and the firing-cycle stage holds the last.
attribute_calculation_profiles maps an Attribute to a weapon stage with its
weight, optional caps, curve, and weapon-category scope.
ISAC keeps combat revision editing, publication, calibration, assisted suggestions, and the calculation AI assistant available so operators can continue authoring the model. Builder and shared Build pages do not load the active revision or consume revision, calibration, assistant, mechanic-runtime, or combat-engine output at the current player-facing boundary.
The shared combat engine consumes weapon facts, accumulated Attributes, calculation profiles, and mechanic outcomes. It produces four hit values, burst DPS, reload-aware sustained DPS, encounter DPS, and stack telemetry. Custom mode walks the expected shot timeline from RPM, magazine size, reload time, hit rate, configured burst size, pauses between bursts, and encounter duration. It resolves the active Attribute state at each landed shot before calculating that shot's expected damage.
The deterministic equipment path scales a weapon definition's level-1 damage
range to the equipped item level, then the agent selects one damage value inside
that valid range. A combat invocation receives equipped_weapon.damage as its
base damage and consumes that number once. The combat pipeline has no
level_scaled_damage value and must not apply item-level scaling a second time.
The generated calculation listing names each typed pipeline stage and its bound
Attributes. The listing explains the active revision with statements such as
TAKE equipped_weapon.damage AS base_damage, ADD, APPLY, WEIGHT, and
RETURN. The typed stages and profiles remain authoritative. The application
neither parses nor executes the listing.
The Headshot Kill Share assumption marks the matching portion of generated kill events for headshot-kill triggers. Rate of Fire, Magazine Size, and Reload Speed bindings change the firing-cycle calculation. Maximum mode applies maximum state. Custom mode can set an effect to Peak, Encounter, or Off.
The Skill engine uses a dedicated intrinsic ability-damage model. It supports direct and periodic delivery, critical and multi-shot expectation, target caps, per-ability activation rates, and charge recovery. Skills do not headshot. Dynamic Talent and tree-effect windows apply to each Skill hit at its delivery time. Encounter-triggered Skill modifiers use the primary weapon timeline until the builder supports a combined weapon-rotation model.
The engine returns INSUFFICIENT_DATA with missing requirement keys when a
calculation lacks required inputs or an Attribute binding. Callers must render
that state instead of a partial result. Current player routes do not call the
engine; their planned calculation surfaces display COMING SOON.
calculation_calibration_cases stores a versioned loadout snapshot, observed
hit values, timing values, optional encounter DPS, and the model prediction.
Ready cases require the four hit values plus magazine and reload times. The
assisted suggestion action compares ready observations with their predictions
and creates a draft revision for operator review.
Rule activation requires calculation.publish and a confirmation. ISAC records
draft creation, draft edits, publication, calibration changes, and assisted
suggestions under the calculation_configuration audit category. ISAC data
export and environment synchronization include calculation rules, typed mechanic
programs, ability attacks, calibration cases, and their relationships in
dependency order.
OS Protocol upgrade expressions and health timing
OS Protocol talents expose zero through five upgrade states. Value expressions
reference equipment.protocolTalentUpgrades and use the linear multiplier
1 + upgrades / 5. Base values resolve at upgrade zero, and scalable values
reach twice their base at upgrade five. Each outcome chooses whether to use this
multiplier, so a fixed resource cost can remain unchanged while restoration and
damage increase.
Happy Survival uses three clauses:
- Its proximity clause records the current enemy count within 10 meters.
- Its periodic clause restores
min(enemyCount × 0.5%, 2.5%) × (1 + upgrades / 5)of maximum Health. The reducer reads the configured interval from an explicit scenario input. The runtime caps applied restoration at missing Health. - Its
shot_firedclause consumes 2% of maximum Health, then deals direct damage equal to0.6% × (1 + upgrades / 5)of current Health. Theresource_costoutcome runs first. The damage outcome uses the remaining current Health after that cost, rather than the pre-event Health value.
At upgrade five, the restoration becomes 1% of maximum Health per nearby enemy up to 5%, and the shot damage becomes 1.2% of current Health. The 2% Health cost does not scale.
Game-data icon storage
Talents, brand sets, OS Protocols, Skill Chips, Specializations, and shared
Weapon Mod slots reference uploaded artwork through the polymorphic
game_data_icons metadata table. The shared server pipeline accepts transparent
SVG, PNG, WebP, and AVIF sources, rejects active SVG content and external
references, and uses Sharp’s native worker pool to rasterize and normalize each
accepted icon to a transparent 256 × 256 WebP.
Normalized files live beneath the ignored data/game-icons/ directory. The
validated /icons/game-data/[type]/[id] endpoint serves them with immutable
cache headers and disables MIME sniffing. ISAC save actions process files before
mutation, apply upload or removal after record validation, and include safe icon
metadata, rather than image bytes, in audit snapshots.
Icon metadata is optional. Canonical records and contributor proposals may be
created without an upload, which lets contributors submit values and artwork in
separate dependency-safe changes. The shared MISSING_ITEM_ICON_URL constant
and ItemIcon component render /missing-item-icon.svg for missing artwork.
The placeholder does not enter game_data_icons. A later edit or proposal can
attach the real icon without recreating the canonical item.
source_entity_type and source_entity_id link each game_data_icons row to a
managed_images record. Both records share one storage key and one WebP file.
The managed record adds the searchable library name, category, tags, creator,
dimensions, and assignment state. Game-data icon saves upsert this record, keep
operator-edited metadata, and merge current entity-context tags. Removing a
source icon removes the linked library record and shared file.
The additive database migration registers older game-data icons and selects an administrator or the earliest active account as the migration actor. ISAC and managed-image exports accept linked storage keys. Environment synchronization keeps the library metadata and one copy of each file.
Oversight notification dispatch
The notification router sends Data Change review items to agents who hold
data.approve and Avatar Oversight review items to agents who hold
avatars.review. Each reviewer chooses Immediate, 15-minute, one-hour, or daily
delivery for each authorized queue. Submission creates Immediate alerts;
oversight_notification_queue stores consolidated alerts.
The production Compose scheduler calls the internal scheduler endpoint once per minute. That endpoint runs the idempotent dispatcher in the application process, which owns the embedded-database connection. Do not run a second host cron job.
The dispatcher groups due rows by reviewer, queue, cadence, and delivery window.
Daily rows use the next 10:00 AM wall-clock boundary in America/New_York,
including daylight-saving transitions. Dispatched rows retain delivered_at
evidence, and later queries exclude them. Existing ISAC notification delivery
preferences determine optional browser and email fan-out after the dispatcher
creates the consolidated inbox message.
Automated ISAC system messages
Automated message definitions live in automated_system_messages and are
managed through ISAC → System Configuration → Automated Messages by agents with
system.notifications.manage. The audit log records edits under the
system_configuration audit category. Definitions identify a lifecycle event,
enabled state, delay in minutes, message content, primary destination, and
structured action callouts.
Password, magic-link, and Discord registration emit account.created. The
delivery table has a unique message/user/event/event-ID constraint, so the same
account creation cannot deliver the Welcome Agent message more than once. The
default welcome introduces News, Database, Builds, Guides, Builder, ISAC
Contributor Mode, and the SHD Discord community.
The registration request delivers Immediate definitions.
automated_system_message_deliveries stores definitions with a delay. The same
Compose scheduler tick runs the delayed dispatcher. Only one scheduler instance
should call the embedded-database application. Delivered rows retain both their
delivery timestamp and generated notification ID.