feat: model specialization combat effects

This commit is contained in:
JB
2026-07-26 19:04:26 -04:00
parent b1656c430f
commit 338ea8761d
34 changed files with 3396 additions and 146 deletions
+140 -1
View File
@@ -242,6 +242,7 @@ INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-metric
INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-system-notifications-manage','system.notifications.manage','View and edit automated ISAC system notification messages');
INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-images-manage','images.manage','View, upload, rename, tag, and categorize managed image assets');
INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-avatars-review','avatars.review','Review and remove custom user avatars');
INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-calculation-publish','calculation.publish','Activate combat calculation revisions used by every loadout');
INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-data-editor','permission-isac-access');
INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-data-editor','permission-data-manage');
INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-data-editor','permission-data-view');
@@ -277,7 +278,7 @@ INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-moder
CREATE TABLE IF NOT EXISTS oauth_states (state_hash TEXT PRIMARY KEY, provider TEXT NOT NULL, mode TEXT NOT NULL, user_id TEXT REFERENCES users(id) ON DELETE CASCADE, expires_at TEXT NOT NULL, created_at TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS data_change_proposals (
id TEXT PRIMARY KEY,
module TEXT NOT NULL CHECK(module IN ('configuration','attributes','talents','brands','gear','weapons','weaponmods','gearmods','protocols','skillchips','specializations')),
module TEXT NOT NULL CHECK(module IN ('configuration','calculations','attributes','talents','brands','gear','weapons','weaponmods','gearmods','protocols','skillchips','specializations')),
action TEXT NOT NULL,
entity_id TEXT,
draft_entity_id TEXT,
@@ -922,6 +923,144 @@ CREATE TABLE IF NOT EXISTS specialization_tree_rank_costs (
PRIMARY KEY(node_id,rank,resource_id),
FOREIGN KEY(node_id,rank) REFERENCES specialization_tree_node_ranks(node_id,rank) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS specialization_combat_effects (
id TEXT PRIMARY KEY,
specialization_id TEXT NOT NULL REFERENCES specializations(id) ON DELETE CASCADE,
name TEXT NOT NULL,
description_template TEXT NOT NULL,
active INTEGER NOT NULL DEFAULT 1 CHECK(active IN (0,1)),
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
UNIQUE(specialization_id,name)
);
CREATE INDEX IF NOT EXISTS specialization_combat_effects_specialization_idx
ON specialization_combat_effects(specialization_id,active,name);
CREATE TABLE IF NOT EXISTS specialization_tree_rank_combat_effects (
id TEXT PRIMARY KEY,
node_id TEXT NOT NULL,
rank INTEGER NOT NULL,
combat_effect_id TEXT NOT NULL REFERENCES specialization_combat_effects(id),
effect_order INTEGER NOT NULL CHECK(effect_order>=1),
trigger_type TEXT NOT NULL CHECK(trigger_type IN ('enemy_hits_within_window')),
trigger_count INTEGER NOT NULL CHECK(trigger_count>0),
trigger_window_seconds REAL NOT NULL CHECK(trigger_window_seconds>0),
duration_seconds REAL NOT NULL CHECK(duration_seconds>0),
max_stacks INTEGER NOT NULL CHECK(max_stacks>0),
stacks_per_trigger INTEGER NOT NULL DEFAULT 1 CHECK(stacks_per_trigger>0),
counter_policy TEXT NOT NULL DEFAULT 'reset_on_proc' CHECK(counter_policy IN ('reset_on_proc')),
refresh_policy TEXT NOT NULL DEFAULT 'refresh_on_stack_gain' CHECK(refresh_policy IN ('refresh_on_stack_gain')),
expiry_policy TEXT NOT NULL DEFAULT 'clear_all' CHECK(expiry_policy IN ('clear_all')),
FOREIGN KEY(node_id,rank) REFERENCES specialization_tree_node_ranks(node_id,rank) ON DELETE CASCADE,
UNIQUE(node_id,rank,effect_order)
);
CREATE INDEX IF NOT EXISTS specialization_tree_rank_combat_effects_effect_idx
ON specialization_tree_rank_combat_effects(combat_effect_id);
CREATE TABLE IF NOT EXISTS specialization_tree_rank_combat_modifiers (
id TEXT PRIMARY KEY,
rank_combat_effect_id TEXT NOT NULL REFERENCES specialization_tree_rank_combat_effects(id) ON DELETE CASCADE,
modifier_order INTEGER NOT NULL CHECK(modifier_order>=1),
stat_attribute_id TEXT NOT NULL REFERENCES stat_attributes(id),
value_per_stack REAL NOT NULL,
unit TEXT,
UNIQUE(rank_combat_effect_id,modifier_order)
);
CREATE TABLE IF NOT EXISTS calculation_revisions (
id TEXT PRIMARY KEY,
version INTEGER NOT NULL UNIQUE CHECK(version>0),
name TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'draft' CHECK(status IN ('draft','active','retired')),
notes TEXT,
based_on_revision_id TEXT REFERENCES calculation_revisions(id),
created_by TEXT REFERENCES users(id),
created_at TEXT NOT NULL,
published_by TEXT REFERENCES users(id),
published_at TEXT
);
CREATE UNIQUE INDEX IF NOT EXISTS calculation_revisions_active_idx
ON calculation_revisions(status) WHERE status='active';
CREATE TABLE IF NOT EXISTS calculation_stages (
id TEXT PRIMARY KEY,
revision_id TEXT NOT NULL REFERENCES calculation_revisions(id) ON DELETE CASCADE,
stage_key TEXT NOT NULL,
label TEXT NOT NULL,
operator TEXT NOT NULL CHECK(operator IN (
'base_damage','flat_add','additive_percent','multiplicative_percent',
'expected_critical','expected_headshot','rate_of_fire','magazine_size',
'reload_speed','firing_cycle'
)),
display_order INTEGER NOT NULL CHECK(display_order>=1),
locked_position TEXT CHECK(locked_position IS NULL OR locked_position IN ('first','last')),
UNIQUE(revision_id,stage_key),
UNIQUE(revision_id,display_order)
);
CREATE TABLE IF NOT EXISTS attribute_calculation_profiles (
id TEXT PRIMARY KEY,
revision_id TEXT NOT NULL REFERENCES calculation_revisions(id) ON DELETE CASCADE,
stat_attribute_id TEXT NOT NULL REFERENCES stat_attributes(id),
stage_key TEXT NOT NULL,
weight REAL NOT NULL DEFAULT 1,
soft_cap REAL,
hard_cap REAL,
curve_type TEXT NOT NULL DEFAULT 'none' CHECK(curve_type IN ('none','power','smoothstep')),
curve_parameter REAL,
weapon_category TEXT NOT NULL DEFAULT '*',
UNIQUE(revision_id,stat_attribute_id,stage_key,weapon_category),
FOREIGN KEY(revision_id,stage_key) REFERENCES calculation_stages(revision_id,stage_key) ON DELETE CASCADE,
CHECK(soft_cap IS NULL OR soft_cap>=0),
CHECK(hard_cap IS NULL OR hard_cap>=0),
CHECK(soft_cap IS NULL OR hard_cap IS NULL OR hard_cap>=soft_cap)
);
CREATE TABLE IF NOT EXISTS calculation_calibration_cases (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'draft' CHECK(status IN ('draft','ready')),
loadout_snapshot_json TEXT NOT NULL,
selected_weapon INTEGER NOT NULL DEFAULT 0 CHECK(selected_weapon IN (0,1)),
game_version TEXT,
observed_at TEXT,
notes TEXT,
body_hit REAL,
critical_body_hit REAL,
headshot REAL,
critical_headshot REAL,
magazine_seconds REAL,
reload_seconds REAL,
encounter_dps REAL,
predicted_json TEXT,
calculation_revision_id TEXT REFERENCES calculation_revisions(id),
created_by TEXT NOT NULL REFERENCES users(id),
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
CHECK(body_hit IS NULL OR body_hit>=0),
CHECK(critical_body_hit IS NULL OR critical_body_hit>=0),
CHECK(headshot IS NULL OR headshot>=0),
CHECK(critical_headshot IS NULL OR critical_headshot>=0),
CHECK(magazine_seconds IS NULL OR magazine_seconds>0),
CHECK(reload_seconds IS NULL OR reload_seconds>0),
CHECK(encounter_dps IS NULL OR encounter_dps>=0)
);
CREATE INDEX IF NOT EXISTS calculation_calibration_status_idx
ON calculation_calibration_cases(status,updated_at DESC);
INSERT OR IGNORE INTO calculation_revisions(id,version,name,status,notes,created_at,published_at)
VALUES('calculation-revision-1',1,'Initial combat model','active','Typed baseline combat stages','2026-07-26T00:00:00.000Z','2026-07-26T00:00:00.000Z');
INSERT OR IGNORE INTO calculation_stages(id,revision_id,stage_key,label,operator,display_order,locked_position)
VALUES('calculation-stage-base-1','calculation-revision-1','base_damage','Base damage','base_damage',1,'first');
INSERT OR IGNORE INTO calculation_stages(id,revision_id,stage_key,label,operator,display_order)
VALUES('calculation-stage-flat-1','calculation-revision-1','flat_damage','Flat damage','flat_add',2);
INSERT OR IGNORE INTO calculation_stages(id,revision_id,stage_key,label,operator,display_order)
VALUES('calculation-stage-additive-1','calculation-revision-1','additive_damage','Additive damage','additive_percent',3);
INSERT OR IGNORE INTO calculation_stages(id,revision_id,stage_key,label,operator,display_order)
VALUES('calculation-stage-multiplicative-1','calculation-revision-1','multiplicative_damage','Multiplicative damage','multiplicative_percent',4);
INSERT OR IGNORE INTO calculation_stages(id,revision_id,stage_key,label,operator,display_order)
VALUES('calculation-stage-critical-chance-1','calculation-revision-1','critical_chance','Critical chance','expected_critical',5);
INSERT OR IGNORE INTO calculation_stages(id,revision_id,stage_key,label,operator,display_order)
VALUES('calculation-stage-critical-1','calculation-revision-1','critical_damage','Critical damage','expected_critical',6);
INSERT OR IGNORE INTO calculation_stages(id,revision_id,stage_key,label,operator,display_order)
VALUES('calculation-stage-headshot-chance-1','calculation-revision-1','headshot_chance','Headshot chance','expected_headshot',7);
INSERT OR IGNORE INTO calculation_stages(id,revision_id,stage_key,label,operator,display_order)
VALUES('calculation-stage-headshot-1','calculation-revision-1','headshot_damage','Headshot damage','expected_headshot',8);
INSERT OR IGNORE INTO calculation_stages(id,revision_id,stage_key,label,operator,display_order,locked_position)
VALUES('calculation-stage-cycle-1','calculation-revision-1','firing_cycle','Firing cycle','firing_cycle',9,'last');
CREATE TABLE IF NOT EXISTS game_data_icons (
entity_type TEXT NOT NULL CHECK(entity_type IN ('talent', 'brand_set', 'os_protocol', 'skill_chip', 'specialization', 'specialization_ability', 'specialization_node')),
entity_id TEXT NOT NULL,