feat: persist authored mechanic revisions
This commit is contained in:
@@ -926,6 +926,266 @@ CREATE TABLE IF NOT EXISTS conditional_effect_outcomes (
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS conditional_effect_outcomes_rule_idx
|
||||
ON conditional_effect_outcomes(rule_id,variant_key,outcome_key,sort_order);
|
||||
-- Phase 2 mechanic persistence. Revision rows are append-only by application
|
||||
-- convention. No database triggers enforce immutability.
|
||||
CREATE TABLE IF NOT EXISTS mechanic_authored_sources (
|
||||
id TEXT PRIMARY KEY,
|
||||
owner_type TEXT NOT NULL CHECK(owner_type IN (
|
||||
'talent','os_protocol_talent','skill_chip_talent','specialization_tree_rank'
|
||||
)),
|
||||
owner_id TEXT NOT NULL CHECK(length(trim(owner_id))>0),
|
||||
template TEXT NOT NULL CHECK(length(trim(template))>0),
|
||||
authored_contract_version INTEGER NOT NULL CHECK(authored_contract_version>0),
|
||||
source_fingerprint TEXT NOT NULL CHECK(
|
||||
length(source_fingerprint)=64
|
||||
AND lower(source_fingerprint)=source_fingerprint
|
||||
AND source_fingerprint NOT GLOB '*[^0-9a-f]*'
|
||||
),
|
||||
talent_scope TEXT CHECK(talent_scope IS NULL OR talent_scope IN ('standard','exotic')),
|
||||
talent_has_tier_ii INTEGER CHECK(talent_has_tier_ii IS NULL OR talent_has_tier_ii IN (0,1)),
|
||||
specialization_rank_description TEXT,
|
||||
specialization_effect_id TEXT,
|
||||
specialization_effect_template TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
UNIQUE(owner_type,owner_id),
|
||||
CHECK(
|
||||
(owner_type='talent'
|
||||
AND talent_scope IS NOT NULL
|
||||
AND specialization_rank_description IS NULL
|
||||
AND specialization_effect_id IS NULL
|
||||
AND specialization_effect_template IS NULL)
|
||||
OR
|
||||
(owner_type='specialization_tree_rank'
|
||||
AND talent_scope IS NULL
|
||||
AND talent_has_tier_ii IS NULL
|
||||
AND specialization_rank_description IS NOT NULL
|
||||
AND length(trim(specialization_rank_description))>0
|
||||
AND specialization_effect_id IS NOT NULL
|
||||
AND length(trim(specialization_effect_id))>0
|
||||
AND specialization_effect_template IS NOT NULL
|
||||
AND length(trim(specialization_effect_template))>0)
|
||||
OR
|
||||
(owner_type IN ('os_protocol_talent','skill_chip_talent')
|
||||
AND talent_scope IS NULL
|
||||
AND talent_has_tier_ii IS NULL
|
||||
AND specialization_rank_description IS NULL
|
||||
AND specialization_effect_id IS NULL
|
||||
AND specialization_effect_template IS NULL)
|
||||
)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS mechanic_authored_sources_owner_idx
|
||||
ON mechanic_authored_sources(owner_type,owner_id);
|
||||
CREATE TABLE IF NOT EXISTS mechanic_authored_variants (
|
||||
source_id TEXT NOT NULL REFERENCES mechanic_authored_sources(id) ON DELETE CASCADE,
|
||||
variant_key TEXT NOT NULL CHECK(length(trim(variant_key))>0),
|
||||
materialized TEXT NOT NULL CHECK(length(materialized)>0),
|
||||
sort_order INTEGER NOT NULL CHECK(sort_order>=0),
|
||||
PRIMARY KEY(source_id,variant_key),
|
||||
UNIQUE(source_id,sort_order)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS mechanic_authored_variants_source_order_idx
|
||||
ON mechanic_authored_variants(source_id,sort_order);
|
||||
CREATE TABLE IF NOT EXISTS mechanic_authored_slots (
|
||||
source_id TEXT NOT NULL,
|
||||
variant_key TEXT NOT NULL,
|
||||
slot_key TEXT NOT NULL CHECK(length(trim(slot_key))>0),
|
||||
display_value TEXT NOT NULL CHECK(
|
||||
display_value=trim(display_value)
|
||||
AND display_value GLOB '*[0-9]*'
|
||||
AND display_value NOT GLOB '*[^0-9.+-]*'
|
||||
AND substr(display_value,2) NOT GLOB '*[+-]*'
|
||||
AND length(display_value)-length(replace(display_value,'.',''))<=1
|
||||
AND substr(display_value,-1)<>'.'
|
||||
AND display_value NOT IN ('+','-','.','+.','-.')
|
||||
),
|
||||
sort_order INTEGER NOT NULL CHECK(sort_order>=0),
|
||||
unit TEXT,
|
||||
stat_attribute_id TEXT REFERENCES stat_attributes(id),
|
||||
PRIMARY KEY(source_id,variant_key,slot_key),
|
||||
UNIQUE(source_id,variant_key,sort_order),
|
||||
FOREIGN KEY(source_id,variant_key)
|
||||
REFERENCES mechanic_authored_variants(source_id,variant_key) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS mechanic_authored_slots_variant_order_idx
|
||||
ON mechanic_authored_slots(source_id,variant_key,sort_order);
|
||||
CREATE INDEX IF NOT EXISTS mechanic_authored_slots_attribute_idx
|
||||
ON mechanic_authored_slots(stat_attribute_id)
|
||||
WHERE stat_attribute_id IS NOT NULL;
|
||||
CREATE TABLE IF NOT EXISTS mechanic_model_revisions (
|
||||
id TEXT PRIMARY KEY,
|
||||
source_id TEXT NOT NULL REFERENCES mechanic_authored_sources(id) ON DELETE CASCADE,
|
||||
revision_number INTEGER NOT NULL CHECK(revision_number>0),
|
||||
source_fingerprint TEXT NOT NULL CHECK(
|
||||
length(source_fingerprint)=64
|
||||
AND lower(source_fingerprint)=source_fingerprint
|
||||
AND source_fingerprint NOT GLOB '*[^0-9a-f]*'
|
||||
),
|
||||
mechanic_contract_version INTEGER NOT NULL CHECK(mechanic_contract_version>0),
|
||||
program_hash TEXT NOT NULL CHECK(
|
||||
length(program_hash)=64
|
||||
AND lower(program_hash)=program_hash
|
||||
AND program_hash NOT GLOB '*[^0-9a-f]*'
|
||||
),
|
||||
simulation_readiness TEXT NOT NULL CHECK(simulation_readiness IN ('ready','requires_assumptions','captured_only')),
|
||||
scenario_inputs_json TEXT NOT NULL DEFAULT '[]' CHECK(json_valid(scenario_inputs_json)),
|
||||
source_variants_json TEXT NOT NULL DEFAULT '[]' CHECK(json_valid(source_variants_json)),
|
||||
program_name TEXT NOT NULL CHECK(length(trim(program_name))>0),
|
||||
origin TEXT NOT NULL CHECK(length(trim(origin))>0),
|
||||
actor_id TEXT REFERENCES users(id) ON DELETE SET NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
UNIQUE(source_id,revision_number),
|
||||
UNIQUE(source_id,id)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS mechanic_model_revisions_source_created_idx
|
||||
ON mechanic_model_revisions(source_id,revision_number DESC,created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS mechanic_model_revisions_fingerprint_idx
|
||||
ON mechanic_model_revisions(source_id,source_fingerprint);
|
||||
CREATE TABLE IF NOT EXISTS mechanic_model_revision_pools (
|
||||
id TEXT PRIMARY KEY,
|
||||
revision_id TEXT NOT NULL REFERENCES mechanic_model_revisions(id) ON DELETE CASCADE,
|
||||
pool_key TEXT NOT NULL CHECK(length(trim(pool_key))>0),
|
||||
label TEXT NOT NULL CHECK(length(trim(label))>0),
|
||||
state_kind TEXT NOT NULL CHECK(state_kind IN ('flag','count','stack','duration','scalar','target_mark','resource')),
|
||||
state_scope TEXT NOT NULL CHECK(state_scope IN ('agent','target')),
|
||||
initial_value REAL NOT NULL,
|
||||
minimum_value REAL,
|
||||
maximum_value REAL,
|
||||
best_case_value REAL,
|
||||
duration_seconds REAL CHECK(duration_seconds IS NULL OR duration_seconds>0),
|
||||
duration_variants_json TEXT CHECK(duration_variants_json IS NULL OR json_valid(duration_variants_json)),
|
||||
sort_order INTEGER NOT NULL CHECK(sort_order>=0),
|
||||
UNIQUE(revision_id,id),
|
||||
UNIQUE(revision_id,pool_key),
|
||||
UNIQUE(revision_id,sort_order),
|
||||
CHECK(minimum_value IS NULL OR maximum_value IS NULL OR maximum_value>=minimum_value),
|
||||
CHECK(best_case_value IS NULL OR minimum_value IS NULL OR best_case_value>=minimum_value),
|
||||
CHECK(best_case_value IS NULL OR maximum_value IS NULL OR best_case_value<=maximum_value),
|
||||
CHECK(NOT (duration_seconds IS NOT NULL AND duration_variants_json IS NOT NULL))
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS mechanic_model_revision_pools_revision_order_idx
|
||||
ON mechanic_model_revision_pools(revision_id,sort_order);
|
||||
CREATE TABLE IF NOT EXISTS mechanic_model_revision_clauses (
|
||||
id TEXT PRIMARY KEY,
|
||||
revision_id TEXT NOT NULL REFERENCES mechanic_model_revisions(id) ON DELETE CASCADE,
|
||||
clause_key TEXT NOT NULL CHECK(length(trim(clause_key))>0),
|
||||
label TEXT,
|
||||
event_kind TEXT NOT NULL CHECK(event_kind IN (
|
||||
'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','tick'
|
||||
)),
|
||||
recipe_key TEXT,
|
||||
conditions_json TEXT CHECK(conditions_json IS NULL OR json_valid(conditions_json)),
|
||||
reducer_json TEXT NOT NULL DEFAULT '{"kind":"none"}' CHECK(json_valid(reducer_json)),
|
||||
weight_expression_json TEXT CHECK(weight_expression_json IS NULL OR json_valid(weight_expression_json)),
|
||||
mutations_json TEXT NOT NULL DEFAULT '[]' CHECK(json_valid(mutations_json)),
|
||||
cooldown_seconds REAL CHECK(cooldown_seconds IS NULL OR cooldown_seconds>0),
|
||||
proc_chance_percent REAL CHECK(proc_chance_percent IS NULL OR (proc_chance_percent>=0 AND proc_chance_percent<=100)),
|
||||
requires_assigned_ability_active INTEGER CHECK(requires_assigned_ability_active IS NULL OR requires_assigned_ability_active IN (0,1)),
|
||||
sort_order INTEGER NOT NULL CHECK(sort_order>=0),
|
||||
UNIQUE(revision_id,id),
|
||||
UNIQUE(revision_id,clause_key),
|
||||
UNIQUE(revision_id,sort_order)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS mechanic_model_revision_clauses_revision_order_idx
|
||||
ON mechanic_model_revision_clauses(revision_id,sort_order);
|
||||
CREATE INDEX IF NOT EXISTS mechanic_model_revision_clauses_event_idx
|
||||
ON mechanic_model_revision_clauses(event_kind,revision_id,sort_order);
|
||||
CREATE TABLE IF NOT EXISTS mechanic_model_revision_outcomes (
|
||||
id TEXT PRIMARY KEY,
|
||||
revision_id TEXT NOT NULL REFERENCES mechanic_model_revisions(id) ON DELETE CASCADE,
|
||||
clause_id TEXT NOT NULL,
|
||||
state_pool_id TEXT,
|
||||
effect_key TEXT NOT NULL CHECK(length(trim(effect_key))>0),
|
||||
variant_key TEXT NOT NULL CHECK(length(trim(variant_key))>0),
|
||||
label TEXT NOT NULL CHECK(length(trim(label))>0),
|
||||
outcome_kind TEXT NOT NULL CHECK(outcome_kind IN ('attribute_modifier','direct_damage','restoration','resource_cost','weapon_reload','marker')),
|
||||
effect_scope TEXT NOT NULL CHECK(effect_scope IN ('self','equipped_weapon','inactive_weapon','target','allies','self_and_allies','assigned_ability','all_skills')),
|
||||
value REAL NOT NULL,
|
||||
value_expression_json TEXT CHECK(value_expression_json IS NULL OR json_valid(value_expression_json)),
|
||||
value_basis TEXT NOT NULL CHECK(value_basis IN ('flat','max_health_percent','current_health_percent','weapon_base_percent')),
|
||||
resource_kind TEXT CHECK(resource_kind IS NULL OR resource_kind IN ('health','armor','ammo','signature_ability_charge')),
|
||||
delivery_mode TEXT NOT NULL DEFAULT 'instant' CHECK(delivery_mode IN ('instant','continuous')),
|
||||
delivery_duration_seconds REAL CHECK(delivery_duration_seconds IS NULL OR delivery_duration_seconds>0),
|
||||
application_phase TEXT NOT NULL CHECK(application_phase IN ('on_event','after_event','on_enter','on_exit','while_active')),
|
||||
combat_timing TEXT CHECK(combat_timing IS NULL OR combat_timing IN ('pre_event','current')),
|
||||
stat_attribute_id TEXT REFERENCES stat_attributes(id),
|
||||
per_stack INTEGER CHECK(per_stack IS NULL OR per_stack IN (0,1)),
|
||||
min_state_value REAL CHECK(min_state_value IS NULL OR min_state_value>=0),
|
||||
max_state_value REAL CHECK(max_state_value IS NULL OR max_state_value>=0),
|
||||
targeting_json TEXT CHECK(targeting_json IS NULL OR json_valid(targeting_json)),
|
||||
probability_percent REAL CHECK(probability_percent IS NULL OR (probability_percent>=0 AND probability_percent<=100)),
|
||||
sort_order INTEGER NOT NULL CHECK(sort_order>=0),
|
||||
UNIQUE(revision_id,id),
|
||||
UNIQUE(revision_id,clause_id,variant_key,effect_key),
|
||||
UNIQUE(revision_id,clause_id,variant_key,sort_order),
|
||||
CHECK(max_state_value IS NULL OR min_state_value IS NULL OR max_state_value>=min_state_value),
|
||||
CHECK(
|
||||
(delivery_mode='instant' AND delivery_duration_seconds IS NULL)
|
||||
OR (delivery_mode='continuous' AND outcome_kind='restoration' AND delivery_duration_seconds IS NOT NULL)
|
||||
),
|
||||
CHECK(
|
||||
(outcome_kind='attribute_modifier' AND stat_attribute_id IS NOT NULL AND resource_kind IS NULL)
|
||||
OR (outcome_kind IN ('restoration','resource_cost') AND stat_attribute_id IS NULL AND resource_kind IS NOT NULL)
|
||||
OR (outcome_kind IN ('direct_damage','weapon_reload','marker') AND stat_attribute_id IS NULL AND resource_kind IS NULL)
|
||||
),
|
||||
FOREIGN KEY(revision_id,clause_id)
|
||||
REFERENCES mechanic_model_revision_clauses(revision_id,id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(revision_id,state_pool_id)
|
||||
REFERENCES mechanic_model_revision_pools(revision_id,id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS mechanic_model_revision_outcomes_clause_order_idx
|
||||
ON mechanic_model_revision_outcomes(revision_id,clause_id,variant_key,sort_order);
|
||||
CREATE INDEX IF NOT EXISTS mechanic_model_revision_outcomes_pool_idx
|
||||
ON mechanic_model_revision_outcomes(revision_id,state_pool_id)
|
||||
WHERE state_pool_id IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS mechanic_model_revision_outcomes_attribute_idx
|
||||
ON mechanic_model_revision_outcomes(stat_attribute_id)
|
||||
WHERE stat_attribute_id IS NOT NULL;
|
||||
CREATE TABLE IF NOT EXISTS mechanic_model_selections (
|
||||
source_id TEXT PRIMARY KEY REFERENCES mechanic_authored_sources(id) ON DELETE CASCADE,
|
||||
revision_id TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
FOREIGN KEY(source_id,revision_id)
|
||||
REFERENCES mechanic_model_revisions(source_id,id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS mechanic_model_selections_revision_idx
|
||||
ON mechanic_model_selections(revision_id,source_id);
|
||||
CREATE TABLE IF NOT EXISTS mechanic_public_variants (
|
||||
source_id TEXT NOT NULL,
|
||||
variant_key TEXT NOT NULL,
|
||||
selected_revision_id TEXT,
|
||||
current_source_fingerprint TEXT NOT NULL CHECK(
|
||||
length(current_source_fingerprint)=64
|
||||
AND lower(current_source_fingerprint)=current_source_fingerprint
|
||||
AND current_source_fingerprint NOT GLOB '*[^0-9a-f]*'
|
||||
),
|
||||
authored_contract_version INTEGER NOT NULL CHECK(authored_contract_version>0),
|
||||
mechanic_contract_version INTEGER CHECK(mechanic_contract_version IS NULL OR mechanic_contract_version>0),
|
||||
status TEXT NOT NULL CHECK(status IN ('modeled','not_modeled')),
|
||||
behavior_text TEXT NOT NULL CHECK(length(behavior_text)>0),
|
||||
display_source TEXT NOT NULL CHECK(display_source IN ('model','authored')),
|
||||
updated_at TEXT NOT NULL,
|
||||
PRIMARY KEY(source_id,variant_key),
|
||||
CHECK(
|
||||
(display_source='model' AND status='modeled' AND selected_revision_id IS NOT NULL)
|
||||
OR (display_source='authored' AND status='not_modeled')
|
||||
),
|
||||
FOREIGN KEY(source_id,variant_key)
|
||||
REFERENCES mechanic_authored_variants(source_id,variant_key) ON DELETE CASCADE,
|
||||
FOREIGN KEY(source_id,selected_revision_id)
|
||||
REFERENCES mechanic_model_revisions(source_id,id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS mechanic_public_variants_status_idx
|
||||
ON mechanic_public_variants(status,source_id,variant_key);
|
||||
CREATE INDEX IF NOT EXISTS mechanic_public_variants_revision_idx
|
||||
ON mechanic_public_variants(selected_revision_id)
|
||||
WHERE selected_revision_id IS NOT NULL;
|
||||
CREATE TABLE IF NOT EXISTS talent_tier_effects (
|
||||
id TEXT PRIMARY KEY,
|
||||
talent_tier_id TEXT NOT NULL REFERENCES talent_tiers(id) ON DELETE CASCADE,
|
||||
|
||||
Reference in New Issue
Block a user