-- Initial embedded Turso schema. Every statement is safe to run repeatedly. CREATE TABLE IF NOT EXISTS users (id TEXT PRIMARY KEY, username TEXT NOT NULL UNIQUE, login_username TEXT, password_hash TEXT, discord_id TEXT UNIQUE, role TEXT NOT NULL DEFAULT 'user', created_at TEXT NOT NULL); CREATE TABLE IF NOT EXISTS user_profiles ( user_id TEXT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, display_name TEXT NOT NULL, updated_at TEXT NOT NULL ); INSERT OR IGNORE INTO user_profiles(user_id,display_name,updated_at) SELECT id,username,created_at FROM users; CREATE TABLE IF NOT EXISTS user_avatars ( user_id TEXT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, source TEXT NOT NULL CHECK(source IN ('preset', 'upload')), value TEXT NOT NULL, byte_size INTEGER CHECK(byte_size IS NULL OR byte_size >= 0), updated_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS avatar_reviews ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, avatar_updated_at TEXT NOT NULL, storage_key TEXT NOT NULL, byte_size INTEGER NOT NULL, status TEXT NOT NULL CHECK(status IN ('pending', 'approved', 'removed', 'withdrawn', 'superseded')), submitted_at TEXT NOT NULL, reviewed_at TEXT, reviewed_by TEXT REFERENCES users(id), review_note TEXT ); CREATE TABLE IF NOT EXISTS notifications ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, category TEXT NOT NULL CHECK(category IN ('isac', 'contributions', 'comments', 'builds', 'guides', 'system')), source TEXT NOT NULL, title TEXT NOT NULL, body TEXT NOT NULL, link TEXT, mandatory INTEGER NOT NULL DEFAULT 0 CHECK(mandatory IN (0, 1)), created_at TEXT NOT NULL, read_at TEXT, emailed_at TEXT ); CREATE INDEX IF NOT EXISTS notifications_user_created_idx ON notifications(user_id, created_at DESC); CREATE INDEX IF NOT EXISTS notifications_user_unread_idx ON notifications(user_id, read_at, created_at DESC); CREATE TABLE IF NOT EXISTS contact_messages ( id TEXT PRIMARY KEY, sender_user_id TEXT REFERENCES users(id) ON DELETE SET NULL, sender_name TEXT NOT NULL, sender_email TEXT NOT NULL, team TEXT NOT NULL CHECK(team IN ('oversight', 'administrators', 'dmca')), subject TEXT NOT NULL, body TEXT NOT NULL, dmca_json TEXT, created_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS contact_message_recipients ( message_id TEXT NOT NULL REFERENCES contact_messages(id) ON DELETE CASCADE, user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, PRIMARY KEY(message_id, user_id) ); CREATE INDEX IF NOT EXISTS contact_messages_created_idx ON contact_messages(created_at DESC); CREATE INDEX IF NOT EXISTS contact_message_recipients_user_idx ON contact_message_recipients(user_id, message_id); CREATE TABLE IF NOT EXISTS notification_preferences ( user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, category TEXT NOT NULL CHECK(category IN ('isac', 'contributions', 'comments', 'builds', 'guides', 'system')), in_app_enabled INTEGER NOT NULL DEFAULT 1 CHECK(in_app_enabled IN (0, 1)), browser_enabled INTEGER NOT NULL DEFAULT 0 CHECK(browser_enabled IN (0, 1)), email_frequency TEXT NOT NULL DEFAULT 'none' CHECK(email_frequency IN ('none', 'immediate', 'daily')), updated_at TEXT NOT NULL, PRIMARY KEY(user_id, category) ); CREATE INDEX IF NOT EXISTS avatar_reviews_status_submitted_idx ON avatar_reviews(status, submitted_at DESC); CREATE INDEX IF NOT EXISTS avatar_reviews_user_submitted_idx ON avatar_reviews(user_id, submitted_at DESC); CREATE TABLE IF NOT EXISTS builds (id TEXT PRIMARY KEY, owner_id TEXT NOT NULL REFERENCES users(id), title TEXT NOT NULL, slug TEXT NOT NULL UNIQUE, visibility TEXT NOT NULL DEFAULT 'draft', summary TEXT, guide_body TEXT, source_build_id TEXT REFERENCES builds(id), created_at TEXT NOT NULL, updated_at TEXT NOT NULL); CREATE TABLE IF NOT EXISTS build_items (id TEXT PRIMARY KEY, build_id TEXT NOT NULL REFERENCES builds(id) ON DELETE CASCADE, slot_id TEXT NOT NULL, item_definition_id TEXT NOT NULL, configuration_json TEXT NOT NULL); CREATE TABLE IF NOT EXISTS ratings (user_id TEXT NOT NULL REFERENCES users(id), build_id TEXT NOT NULL REFERENCES builds(id) ON DELETE CASCADE, value INTEGER NOT NULL CHECK(value BETWEEN 1 AND 5), PRIMARY KEY(user_id, build_id)); CREATE TABLE IF NOT EXISTS comments (id TEXT PRIMARY KEY, build_id TEXT NOT NULL REFERENCES builds(id) ON DELETE CASCADE, user_id TEXT NOT NULL REFERENCES users(id), body TEXT NOT NULL, created_at TEXT NOT NULL); CREATE TABLE IF NOT EXISTS favorites (user_id TEXT NOT NULL REFERENCES users(id), build_id TEXT NOT NULL REFERENCES builds(id) ON DELETE CASCADE, created_at TEXT NOT NULL, PRIMARY KEY(user_id, build_id)); CREATE TABLE IF NOT EXISTS equipment_types (id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, category TEXT NOT NULL); CREATE TABLE IF NOT EXISTS stat_definitions (id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, value_kind TEXT NOT NULL, min_value REAL, max_value REAL, unit TEXT, active INTEGER NOT NULL DEFAULT 1); CREATE TABLE IF NOT EXISTS stat_equipment_types (stat_id TEXT NOT NULL REFERENCES stat_definitions(id), equipment_type_id TEXT NOT NULL REFERENCES equipment_types(id), PRIMARY KEY(stat_id, equipment_type_id)); CREATE TABLE IF NOT EXISTS audit_log (id TEXT PRIMARY KEY, actor_id TEXT NOT NULL REFERENCES users(id), entity_type TEXT NOT NULL, entity_id TEXT NOT NULL, action TEXT NOT NULL, before_json TEXT, after_json TEXT, created_at TEXT NOT NULL); CREATE TABLE IF NOT EXISTS sessions (id TEXT PRIMARY KEY, user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, token_hash TEXT NOT NULL UNIQUE, expires_at TEXT NOT NULL, created_at TEXT NOT NULL); CREATE TABLE IF NOT EXISTS roles (id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, description TEXT); CREATE TABLE IF NOT EXISTS permissions (id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, description TEXT); CREATE TABLE IF NOT EXISTS user_roles (user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, role_id TEXT NOT NULL REFERENCES roles(id) ON DELETE CASCADE, PRIMARY KEY(user_id, role_id)); CREATE TABLE IF NOT EXISTS role_permissions (role_id TEXT NOT NULL REFERENCES roles(id) ON DELETE CASCADE, permission_id TEXT NOT NULL REFERENCES permissions(id) ON DELETE CASCADE, scope_json TEXT, PRIMARY KEY(role_id, permission_id)); CREATE TABLE IF NOT EXISTS user_permissions (user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, permission_id TEXT NOT NULL REFERENCES permissions(id) ON DELETE CASCADE, PRIMARY KEY(user_id, permission_id)); CREATE TABLE IF NOT EXISTS user_status ( user_id TEXT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, last_active_at TEXT, blocked_at TEXT, blocked_by TEXT REFERENCES users(id), banned_until TEXT, moderation_reason TEXT, updated_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS login_events ( id TEXT PRIMARY KEY, user_id TEXT REFERENCES users(id) ON DELETE SET NULL, provider TEXT NOT NULL CHECK(provider IN ('password', 'discord', 'magic_link', 'passkey')), succeeded INTEGER NOT NULL CHECK(succeeded IN (0, 1)), ip_address TEXT, user_agent TEXT, client_name TEXT, operating_system TEXT, device_type TEXT, location_country TEXT, location_region TEXT, location_city TEXT, failure_reason TEXT, risk_flags TEXT, credential_id TEXT, session_id TEXT REFERENCES sessions(id) ON DELETE SET NULL, session_ended_at TEXT, session_end_reason TEXT, sensitive_purged_at TEXT, created_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS user_emails ( user_id TEXT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, email TEXT NOT NULL COLLATE NOCASE UNIQUE, verified_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS magic_link_tokens ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, email TEXT NOT NULL COLLATE NOCASE, purpose TEXT NOT NULL CHECK(purpose IN ('login', 'verify_email')), token_hash TEXT NOT NULL UNIQUE, expires_at TEXT NOT NULL, consumed_at TEXT, created_at TEXT NOT NULL ); CREATE INDEX IF NOT EXISTS magic_link_tokens_expiry_idx ON magic_link_tokens(expires_at); CREATE TABLE IF NOT EXISTS passkeys ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, webauthn_user_id TEXT NOT NULL, public_key BLOB NOT NULL, counter INTEGER NOT NULL DEFAULT 0, device_type TEXT NOT NULL, backed_up INTEGER NOT NULL DEFAULT 0 CHECK(backed_up IN (0, 1)), transports TEXT, name TEXT NOT NULL, last_used_at TEXT, created_at TEXT NOT NULL ); CREATE INDEX IF NOT EXISTS passkeys_user_idx ON passkeys(user_id); CREATE TABLE IF NOT EXISTS webauthn_challenges ( id TEXT PRIMARY KEY, user_id TEXT REFERENCES users(id) ON DELETE CASCADE, flow TEXT NOT NULL CHECK(flow IN ('registration', 'authentication')), challenge TEXT NOT NULL, rp_id TEXT NOT NULL, origin TEXT NOT NULL, expires_at TEXT NOT NULL, created_at TEXT NOT NULL ); CREATE INDEX IF NOT EXISTS webauthn_challenges_expiry_idx ON webauthn_challenges(expires_at); CREATE TABLE IF NOT EXISTS deleted_accounts ( user_id TEXT PRIMARY KEY REFERENCES users(id), original_username TEXT NOT NULL, deleted_at TEXT NOT NULL, deleted_by TEXT NOT NULL REFERENCES users(id), reason TEXT ); CREATE INDEX IF NOT EXISTS login_events_user_created_idx ON login_events(user_id, created_at DESC); INSERT OR IGNORE INTO roles(id,name,description) VALUES('role-data-editor','Data Editor','Access to ISAC game-data management'); INSERT OR IGNORE INTO roles(id,name,description) VALUES('role-user-operator','User Operator','View and moderate SHD.build users'); INSERT OR IGNORE INTO roles(id,name,description) VALUES('role-access-manager','Access Manager','Manage user roles and direct permissions'); INSERT OR IGNORE INTO roles(id,name,description) VALUES('role-admin','Admin','Full administrative access to all current and future ISAC capabilities'); INSERT OR IGNORE INTO roles(id,name,description) VALUES('role-moderator','Community Oversight','Community oversight role reserved for build, guide, and comment tools'); INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-system-admin','system.admin','Unrestricted access to all administrative capabilities'); INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-isac-access','isac.access','Open the ISAC interface'); INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-data-manage','data.manage','Create and edit builder source data'); INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-data-view','data.view','View ISAC builder source data without changing live records'); INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-data-propose','data.propose','Suggest builder source-data additions and edits for approval'); INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-data-approve','data.approve','Review, approve, and reject proposed builder source-data changes'); INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-users-view','users.view','View users, activity, and content history'); INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-users-moderate','users.moderate','Block, ban, and reactivate users'); INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-users-roles','users.roles','Assign roles and direct permissions'); INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-users-delete','users.delete','Soft-delete user accounts while retaining authored content'); INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-users-identities','users.identities','View and manage linked external identities'); INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-audit-view','audit.view','View and search the immutable ISAC audit history'); INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-audit-revert','audit.revert','Attempt confirmed compensating reversals of supported ISAC audit events'); INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-metrics-view','metrics.view','View read-only application, runtime, and database health metrics'); INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-avatars-review','avatars.review','Review and remove custom user avatars'); 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'); INSERT OR IGNORE INTO roles(id,name,description) VALUES('role-data-viewer','Data Viewer','Read-only access to ISAC game-data registries'); INSERT OR IGNORE INTO roles(id,name,description) VALUES('role-data-contributor','Data Contributor','View game data and submit moderated change proposals'); INSERT OR IGNORE INTO roles(id,name,description) VALUES('role-data-approver','Data Oversight','Review, correct, and decide proposals before applying approved changes to live game data'); UPDATE roles SET name='Community Oversight',description='Community oversight role reserved for build, guide, and comment tools' WHERE id='role-moderator'; UPDATE roles SET name='Data Oversight',description='Review, correct, and decide proposals before applying approved changes to live game data' WHERE id='role-data-approver'; INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-data-viewer','permission-isac-access'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-data-viewer','permission-data-view'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-data-contributor','permission-isac-access'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-data-contributor','permission-data-view'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-data-contributor','permission-data-propose'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-data-approver','permission-isac-access'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-data-approver','permission-data-view'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-data-approver','permission-data-manage'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-data-approver','permission-data-approve'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-user-operator','permission-isac-access'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-user-operator','permission-users-view'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-user-operator','permission-users-moderate'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-user-operator','permission-avatars-review'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-access-manager','permission-isac-access'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-access-manager','permission-users-view'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-access-manager','permission-users-roles'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-access-manager','permission-users-identities'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-admin','permission-system-admin'); INSERT OR IGNORE INTO roles(id,name,description) VALUES('role-auditor','Auditor','Read-only access to ISAC audit history'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-auditor','permission-isac-access'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-auditor','permission-audit-view'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-auditor','permission-metrics-view'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-moderator','permission-isac-access'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-moderator','permission-avatars-review'); 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','protocols','skillchips')), action TEXT NOT NULL, entity_id TEXT, draft_entity_id TEXT, applied_entity_id TEXT, summary TEXT NOT NULL, payload_json TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending','approved','rejected','failed')), proposed_by TEXT NOT NULL REFERENCES users(id), reviewed_by TEXT REFERENCES users(id), review_note TEXT, created_at TEXT NOT NULL, reviewed_at TEXT, applied_at TEXT ); CREATE INDEX IF NOT EXISTS data_change_proposals_status_created_idx ON data_change_proposals(status,created_at DESC); CREATE INDEX IF NOT EXISTS data_change_proposals_module_status_idx ON data_change_proposals(module,status); CREATE TABLE IF NOT EXISTS data_change_proposal_dependencies ( proposal_id TEXT NOT NULL REFERENCES data_change_proposals(id) ON DELETE CASCADE, depends_on_proposal_id TEXT NOT NULL REFERENCES data_change_proposals(id) ON DELETE RESTRICT, PRIMARY KEY(proposal_id,depends_on_proposal_id), CHECK(proposal_id<>depends_on_proposal_id) ); CREATE INDEX IF NOT EXISTS data_change_proposal_dependencies_dependency_idx ON data_change_proposal_dependencies(depends_on_proposal_id); CREATE TABLE IF NOT EXISTS contributor_agreements ( user_id TEXT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, guidelines_version TEXT NOT NULL, accepted_at TEXT NOT NULL, enabled INTEGER NOT NULL DEFAULT 1 CHECK(enabled IN (0,1)), enabled_at TEXT NOT NULL, disabled_at TEXT ); CREATE TABLE IF NOT EXISTS discord_profiles ( user_id TEXT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, discord_id TEXT NOT NULL UNIQUE, username TEXT NOT NULL, display_name TEXT, avatar_hash TEXT, updated_at TEXT NOT NULL ); CREATE UNIQUE INDEX IF NOT EXISTS users_username_nocase_idx ON users(username COLLATE NOCASE); CREATE UNIQUE INDEX IF NOT EXISTS users_login_username_nocase_idx ON users(login_username COLLATE NOCASE) WHERE login_username IS NOT NULL; CREATE TABLE IF NOT EXISTS stat_attributes ( id TEXT PRIMARY KEY, name TEXT NOT NULL, type TEXT NOT NULL CHECK(type IN ('Offensive', 'Defensive')), category TEXT NOT NULL CHECK(category IN ('Weapon', 'Skill', 'Character')), calculation_type TEXT NOT NULL CHECK(calculation_type IN ('Additive', 'Multiplicative')), unit TEXT NOT NULL DEFAULT '', gear_min REAL, gear_max REAL, gear_reference_level INTEGER NOT NULL DEFAULT 50 CHECK(gear_reference_level BETWEEN 1 AND 50), weapon_min REAL, weapon_max REAL, weapon_reference_level INTEGER NOT NULL DEFAULT 50 CHECK(weapon_reference_level BETWEEN 1 AND 50), skill_chip_min REAL, skill_chip_max REAL, cap_value REAL, active INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, CHECK((gear_min IS NULL AND gear_max IS NULL) OR (gear_min IS NOT NULL AND gear_max IS NOT NULL AND gear_min <= gear_max)), CHECK((weapon_min IS NULL AND weapon_max IS NULL) OR (weapon_min IS NOT NULL AND weapon_max IS NOT NULL AND weapon_min <= weapon_max)), CHECK((skill_chip_min IS NULL AND skill_chip_max IS NULL) OR (skill_chip_min IS NOT NULL AND skill_chip_max IS NOT NULL AND skill_chip_min <= skill_chip_max)), CHECK(cap_value IS NULL OR cap_value >= 0) ); CREATE UNIQUE INDEX IF NOT EXISTS stat_attributes_name_nocase_idx ON stat_attributes(name COLLATE NOCASE); CREATE TABLE IF NOT EXISTS talents ( id TEXT PRIMARY KEY, name TEXT NOT NULL, category TEXT NOT NULL CHECK(category IN ('Weapon', 'Backpack', 'Body Armor', 'Vest')), cooldown_seconds REAL CHECK(cooldown_seconds IS NULL OR cooldown_seconds > 0), duration_seconds REAL CHECK(duration_seconds IS NULL OR duration_seconds > 0), max_stacks INTEGER CHECK(max_stacks IS NULL OR max_stacks > 0), active INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE UNIQUE INDEX IF NOT EXISTS talents_name_category_nocase_idx ON talents(name COLLATE NOCASE, category); CREATE TABLE IF NOT EXISTS talent_tiers ( id TEXT PRIMARY KEY, talent_id TEXT NOT NULL REFERENCES talents(id) ON DELETE CASCADE, tier TEXT NOT NULL CHECK(tier IN ('I', 'II')), description TEXT NOT NULL, trigger_text TEXT NOT NULL, effect_text TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, UNIQUE(talent_id, tier) ); CREATE TABLE IF NOT EXISTS talent_triggers ( id TEXT PRIMARY KEY, name TEXT NOT NULL, description TEXT NOT NULL, supports_uptime INTEGER NOT NULL DEFAULT 0 CHECK(supports_uptime IN (0, 1)), min_uptime REAL, max_uptime REAL, calculation_script TEXT, active INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, CHECK( (supports_uptime = 0 AND min_uptime IS NULL AND max_uptime IS NULL) OR (supports_uptime = 1 AND min_uptime IS NOT NULL AND max_uptime IS NOT NULL AND min_uptime >= 0 AND max_uptime <= 100 AND min_uptime <= max_uptime) ) ); CREATE UNIQUE INDEX IF NOT EXISTS talent_triggers_name_nocase_idx ON talent_triggers(name COLLATE NOCASE); CREATE TABLE IF NOT EXISTS talent_tier_triggers ( talent_tier_id TEXT NOT NULL REFERENCES talent_tiers(id) ON DELETE CASCADE, trigger_id TEXT NOT NULL REFERENCES talent_triggers(id), PRIMARY KEY(talent_tier_id, trigger_id) ); 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, stat_attribute_id TEXT NOT NULL REFERENCES stat_attributes(id), value REAL NOT NULL, created_at TEXT NOT NULL, UNIQUE(talent_tier_id, stat_attribute_id) ); CREATE TABLE IF NOT EXISTS talent_description_templates ( talent_id TEXT PRIMARY KEY REFERENCES talents(id) ON DELETE CASCADE, template TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS talent_weapon_scopes ( talent_id TEXT PRIMARY KEY REFERENCES talents(id) ON DELETE CASCADE, scope TEXT NOT NULL CHECK(scope IN ('Standard', 'Exotic')) ); INSERT OR IGNORE INTO talent_weapon_scopes(talent_id,scope) SELECT id,'Standard' FROM talents WHERE category='Weapon'; CREATE TABLE IF NOT EXISTS weapon_talent_slots ( talent_id TEXT PRIMARY KEY REFERENCES talents(id) ON DELETE CASCADE, slot INTEGER NOT NULL CHECK(slot IN (1, 2)) ); CREATE TABLE IF NOT EXISTS brand_sets ( id TEXT PRIMARY KEY, name TEXT NOT NULL, active INTEGER NOT NULL DEFAULT 1 CHECK(active IN (0, 1)), created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE UNIQUE INDEX IF NOT EXISTS brand_sets_name_nocase_idx ON brand_sets(name COLLATE NOCASE); CREATE TABLE IF NOT EXISTS brand_set_effects ( id TEXT PRIMARY KEY, brand_set_id TEXT NOT NULL REFERENCES brand_sets(id) ON DELETE CASCADE, pieces_required INTEGER NOT NULL CHECK(pieces_required IN (2, 3, 4)), stat_attribute_id TEXT NOT NULL REFERENCES stat_attributes(id), value REAL NOT NULL CHECK(value >= 0), created_at TEXT NOT NULL, updated_at TEXT NOT NULL, UNIQUE(brand_set_id, pieces_required) ); CREATE TABLE IF NOT EXISTS gear ( id TEXT PRIMARY KEY, name TEXT NOT NULL, slot TEXT NOT NULL CHECK(slot IN ('Holster', 'Gloves', 'Backpack', 'Knee Pads', 'Body Armor', 'Mask')), tier INTEGER NOT NULL CHECK(tier BETWEEN 0 AND 2), base_health_min REAL NOT NULL CHECK(base_health_min >= 0), base_health_max REAL NOT NULL CHECK(base_health_max >= base_health_min), base_armor_min REAL, base_armor_max REAL, range_reference_level INTEGER NOT NULL DEFAULT 50 CHECK(range_reference_level BETWEEN 1 AND 50), active INTEGER NOT NULL DEFAULT 1 CHECK(active IN (0, 1)), created_at TEXT NOT NULL, updated_at TEXT NOT NULL, CHECK( (slot IN ('Gloves', 'Knee Pads', 'Body Armor') AND base_armor_min IS NOT NULL AND base_armor_max IS NOT NULL AND base_armor_min >= 0 AND base_armor_max >= base_armor_min) OR (slot NOT IN ('Gloves', 'Knee Pads', 'Body Armor') AND base_armor_min IS NULL AND base_armor_max IS NULL) ) ); CREATE UNIQUE INDEX IF NOT EXISTS gear_name_nocase_idx ON gear(name COLLATE NOCASE); CREATE TABLE IF NOT EXISTS gear_brand_sets ( gear_id TEXT NOT NULL REFERENCES gear(id) ON DELETE CASCADE, brand_set_id TEXT NOT NULL REFERENCES brand_sets(id), PRIMARY KEY(gear_id, brand_set_id) ); CREATE TABLE IF NOT EXISTS gear_talents ( gear_id TEXT NOT NULL REFERENCES gear(id) ON DELETE CASCADE, talent_id TEXT NOT NULL REFERENCES talents(id), PRIMARY KEY(gear_id, talent_id) ); CREATE TABLE IF NOT EXISTS gear_tactical_augments ( id TEXT PRIMARY KEY, gear_slot TEXT NOT NULL CHECK(gear_slot IN ('Holster', 'Gloves', 'Backpack', 'Knee Pads', 'Body Armor', 'Mask')), augment_slot INTEGER NOT NULL CHECK(augment_slot BETWEEN 1 AND 4), effect_type TEXT NOT NULL CHECK(effect_type IN ('Custom', 'Base Percentage')), stat_attribute_id TEXT REFERENCES stat_attributes(id), value REAL NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, UNIQUE(gear_slot, augment_slot), CHECK( (gear_slot IN ('Backpack', 'Body Armor') AND augment_slot BETWEEN 1 AND 4) OR (gear_slot NOT IN ('Backpack', 'Body Armor') AND augment_slot BETWEEN 1 AND 3) ), CHECK( (augment_slot = 1 AND effect_type = 'Custom' AND stat_attribute_id IS NOT NULL) OR (augment_slot > 1 AND effect_type = 'Base Percentage' AND stat_attribute_id IS NULL AND value > 0) ) ); CREATE TABLE IF NOT EXISTS gear_tactical_augment_costs ( augment_id TEXT NOT NULL REFERENCES gear_tactical_augments(id) ON DELETE CASCADE, gear_tier INTEGER NOT NULL CHECK(gear_tier BETWEEN 0 AND 2), unlock_cost INTEGER NOT NULL CHECK(unlock_cost >= 0), PRIMARY KEY(augment_id, gear_tier) ); CREATE TABLE IF NOT EXISTS weapons ( id TEXT PRIMARY KEY, name TEXT NOT NULL, category TEXT NOT NULL CHECK(category IN ('Shotgun', 'Rifle', 'MMR', 'AR', 'LMG', 'SMG')), is_exotic INTEGER NOT NULL DEFAULT 0 CHECK(is_exotic IN (0, 1)), native_tier INTEGER NOT NULL CHECK(native_tier BETWEEN 0 AND 2), damage_type TEXT NOT NULL CHECK(damage_type IN ('Blast', 'Pierce', 'Shredding')), rpm REAL NOT NULL CHECK(rpm >= 0), magazine REAL NOT NULL CHECK(magazine >= 0), reload_speed REAL NOT NULL CHECK(reload_speed >= 0), accuracy REAL NOT NULL CHECK(accuracy >= 0), stability REAL NOT NULL CHECK(stability >= 0), exotic_talent_id TEXT REFERENCES talents(id), active INTEGER NOT NULL DEFAULT 1 CHECK(active IN (0, 1)), created_at TEXT NOT NULL, updated_at TEXT NOT NULL, CHECK( (is_exotic = 1 AND exotic_talent_id IS NOT NULL) OR (is_exotic = 0 AND exotic_talent_id IS NULL) ) ); CREATE UNIQUE INDEX IF NOT EXISTS weapons_name_nocase_idx ON weapons(name COLLATE NOCASE); CREATE TABLE IF NOT EXISTS core_attribute_tier_ranges ( tier INTEGER PRIMARY KEY CHECK(tier BETWEEN 0 AND 2), nominal_min REAL NOT NULL CHECK(nominal_min >= 0), nominal_max REAL NOT NULL CHECK(nominal_max >= nominal_min), reference_level INTEGER NOT NULL DEFAULT 50 CHECK(reference_level BETWEEN 1 AND 50), updated_at TEXT NOT NULL ); INSERT OR IGNORE INTO core_attribute_tier_ranges(tier,nominal_min,nominal_max,reference_level,updated_at) VALUES(0,30,78,50,CURRENT_TIMESTAMP); INSERT OR IGNORE INTO core_attribute_tier_ranges(tier,nominal_min,nominal_max,reference_level,updated_at) VALUES(1,36,94,50,CURRENT_TIMESTAMP); INSERT OR IGNORE INTO core_attribute_tier_ranges(tier,nominal_min,nominal_max,reference_level,updated_at) VALUES(2,44,112,50,CURRENT_TIMESTAMP); CREATE TABLE IF NOT EXISTS weapon_tier_ranges ( weapon_id TEXT NOT NULL REFERENCES weapons(id) ON DELETE CASCADE, tier INTEGER NOT NULL CHECK(tier BETWEEN 0 AND 2), damage_min REAL NOT NULL CHECK(damage_min >= 0), damage_max REAL NOT NULL CHECK(damage_max >= damage_min), firepower_min REAL NOT NULL CHECK(firepower_min >= 0), firepower_max REAL NOT NULL CHECK(firepower_max >= firepower_min), toughness_min REAL NOT NULL CHECK(toughness_min >= 0), toughness_max REAL NOT NULL CHECK(toughness_max >= toughness_min), engineering_min REAL NOT NULL CHECK(engineering_min >= 0), engineering_max REAL NOT NULL CHECK(engineering_max >= engineering_min), reference_level INTEGER NOT NULL DEFAULT 50 CHECK(reference_level BETWEEN 1 AND 50), PRIMARY KEY(weapon_id, tier) ); CREATE TABLE IF NOT EXISTS weapon_talent_options ( weapon_id TEXT NOT NULL REFERENCES weapons(id) ON DELETE CASCADE, talent_slot INTEGER NOT NULL CHECK(talent_slot IN (1, 2)), talent_id TEXT NOT NULL REFERENCES talents(id), PRIMARY KEY(weapon_id, talent_slot, talent_id) ); CREATE TABLE IF NOT EXISTS weapon_augmentation_effects ( id TEXT PRIMARY KEY, weapon_id TEXT NOT NULL REFERENCES weapons(id) ON DELETE CASCADE, stat_attribute_id TEXT NOT NULL REFERENCES stat_attributes(id), value REAL NOT NULL CHECK(value > 0), sort_order INTEGER NOT NULL CHECK(sort_order IN (1, 2)), UNIQUE(weapon_id, sort_order), UNIQUE(weapon_id, stat_attribute_id) ); CREATE TABLE IF NOT EXISTS weapon_base_attribute_bonuses ( id TEXT PRIMARY KEY, weapon_id TEXT NOT NULL REFERENCES weapons(id) ON DELETE CASCADE, stat_attribute_id TEXT NOT NULL REFERENCES stat_attributes(id), value REAL NOT NULL CHECK(value > 0), sort_order INTEGER NOT NULL CHECK(sort_order >= 1), UNIQUE(weapon_id, stat_attribute_id), UNIQUE(weapon_id, sort_order) ); CREATE TABLE IF NOT EXISTS os_protocols ( id TEXT PRIMARY KEY, name TEXT NOT NULL, quality TEXT NOT NULL CHECK(quality IN ('Standard', 'Specialized', 'Superior', 'High-end')), primary_attribute_type TEXT NOT NULL CHECK(primary_attribute_type IN ('Firepower', 'Toughness', 'Engineering')), active INTEGER NOT NULL DEFAULT 1 CHECK(active IN (0, 1)), created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE UNIQUE INDEX IF NOT EXISTS os_protocols_name_nocase_idx ON os_protocols(name COLLATE NOCASE); CREATE TABLE IF NOT EXISTS os_protocol_primary_effects ( id TEXT PRIMARY KEY, protocol_id TEXT NOT NULL UNIQUE REFERENCES os_protocols(id) ON DELETE CASCADE, stat_attribute_id TEXT NOT NULL REFERENCES stat_attributes(id), base_value REAL NOT NULL CHECK(base_value >= 0) ); CREATE TABLE IF NOT EXISTS os_protocol_secondary_profiles ( id TEXT PRIMARY KEY, quality TEXT NOT NULL CHECK(quality IN ('Standard', 'Specialized', 'Superior', 'High-end')), primary_attribute_type TEXT NOT NULL CHECK(primary_attribute_type IN ('Firepower', 'Toughness', 'Engineering')), created_at TEXT NOT NULL, updated_at TEXT NOT NULL, UNIQUE(quality, primary_attribute_type) ); CREATE TABLE IF NOT EXISTS os_protocol_secondary_effects ( id TEXT PRIMARY KEY, profile_id TEXT NOT NULL REFERENCES os_protocol_secondary_profiles(id) ON DELETE CASCADE, effect_role TEXT NOT NULL CHECK(effect_role IN ('Core', 'Damage', 'Health')), stat_attribute_id TEXT NOT NULL REFERENCES stat_attributes(id), value REAL NOT NULL CHECK(value >= 0), UNIQUE(profile_id, effect_role), UNIQUE(profile_id, stat_attribute_id) ); CREATE TABLE IF NOT EXISTS os_protocol_talents ( id TEXT PRIMARY KEY, protocol_id TEXT NOT NULL UNIQUE REFERENCES os_protocols(id) ON DELETE CASCADE, name TEXT NOT NULL, description_template TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE UNIQUE INDEX IF NOT EXISTS os_protocol_talents_name_nocase_idx ON os_protocol_talents(name COLLATE NOCASE); CREATE TABLE IF NOT EXISTS os_protocol_talent_triggers ( talent_id TEXT NOT NULL REFERENCES os_protocol_talents(id) ON DELETE CASCADE, trigger_id TEXT NOT NULL REFERENCES talent_triggers(id), PRIMARY KEY(talent_id, trigger_id) ); CREATE TABLE IF NOT EXISTS os_protocol_talent_effects ( id TEXT PRIMARY KEY, talent_id TEXT NOT NULL REFERENCES os_protocol_talents(id) ON DELETE CASCADE, stat_attribute_id TEXT NOT NULL REFERENCES stat_attributes(id), base_value REAL NOT NULL, sort_order INTEGER NOT NULL CHECK(sort_order BETWEEN 1 AND 12), UNIQUE(talent_id, stat_attribute_id), UNIQUE(talent_id, sort_order) ); CREATE TABLE IF NOT EXISTS skill_chip_sets ( id TEXT PRIMARY KEY, name TEXT NOT NULL, active INTEGER NOT NULL DEFAULT 1 CHECK(active IN (0, 1)), created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE UNIQUE INDEX IF NOT EXISTS skill_chip_sets_name_nocase_idx ON skill_chip_sets(name COLLATE NOCASE); CREATE TABLE IF NOT EXISTS skill_chip_two_piece_bonuses ( id TEXT PRIMARY KEY, skill_chip_set_id TEXT NOT NULL UNIQUE REFERENCES skill_chip_sets(id) ON DELETE CASCADE, stat_attribute_id TEXT NOT NULL REFERENCES stat_attributes(id), min_value REAL NOT NULL, max_value REAL NOT NULL, CHECK(max_value >= min_value) ); CREATE TABLE IF NOT EXISTS skill_chip_primary_options ( skill_chip_set_id TEXT NOT NULL REFERENCES skill_chip_sets(id) ON DELETE CASCADE, stat_attribute_id TEXT NOT NULL REFERENCES stat_attributes(id), PRIMARY KEY(skill_chip_set_id, stat_attribute_id) ); CREATE TABLE IF NOT EXISTS skill_chip_secondary_options ( skill_chip_set_id TEXT NOT NULL REFERENCES skill_chip_sets(id) ON DELETE CASCADE, stat_attribute_id TEXT NOT NULL REFERENCES stat_attributes(id), PRIMARY KEY(skill_chip_set_id, stat_attribute_id) ); CREATE TABLE IF NOT EXISTS skill_chip_talents ( id TEXT PRIMARY KEY, skill_chip_set_id TEXT NOT NULL UNIQUE REFERENCES skill_chip_sets(id) ON DELETE CASCADE, name TEXT NOT NULL, description_template TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE UNIQUE INDEX IF NOT EXISTS skill_chip_talents_name_nocase_idx ON skill_chip_talents(name COLLATE NOCASE); CREATE TABLE IF NOT EXISTS skill_chip_talent_triggers ( talent_id TEXT NOT NULL REFERENCES skill_chip_talents(id) ON DELETE CASCADE, trigger_id TEXT NOT NULL REFERENCES talent_triggers(id), PRIMARY KEY(talent_id, trigger_id) ); CREATE TABLE IF NOT EXISTS skill_chip_talent_effects ( id TEXT PRIMARY KEY, talent_id TEXT NOT NULL REFERENCES skill_chip_talents(id) ON DELETE CASCADE, stat_attribute_id TEXT NOT NULL REFERENCES stat_attributes(id), min_value REAL NOT NULL, max_value REAL NOT NULL, sort_order INTEGER NOT NULL CHECK(sort_order BETWEEN 1 AND 12), CHECK(max_value >= min_value), UNIQUE(talent_id, stat_attribute_id), UNIQUE(talent_id, sort_order) ); CREATE TABLE IF NOT EXISTS game_data_icons ( entity_type TEXT NOT NULL CHECK(entity_type IN ('talent', 'brand_set', 'os_protocol', 'skill_chip')), entity_id TEXT NOT NULL, storage_key TEXT NOT NULL UNIQUE, byte_size INTEGER NOT NULL CHECK(byte_size > 0), updated_at TEXT NOT NULL, PRIMARY KEY(entity_type, entity_id) );