-- 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, bio_markdown TEXT NOT NULL DEFAULT '', is_public INTEGER NOT NULL DEFAULT 1 CHECK(is_public IN (0, 1)), bio_updated_at TEXT, 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 profile_moderation ( user_id TEXT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, bio_hidden INTEGER NOT NULL DEFAULT 0 CHECK(bio_hidden IN (0, 1)), reason TEXT, moderated_at TEXT, moderated_by TEXT REFERENCES users(id) ); 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')), review_origin TEXT NOT NULL DEFAULT 'legacy' CHECK(review_origin IN ('automated', 'user_report', 'legacy')), 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, actions_json 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 TABLE IF NOT EXISTS oversight_notification_preferences ( user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, oversight_category TEXT NOT NULL CHECK(oversight_category IN ('data_changes', 'avatars')), frequency TEXT NOT NULL DEFAULT 'immediate' CHECK(frequency IN ('immediate', '15_minutes', '1_hour', 'daily')), updated_at TEXT NOT NULL, PRIMARY KEY(user_id, oversight_category) ); CREATE TABLE IF NOT EXISTS oversight_notification_queue ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, oversight_category TEXT NOT NULL CHECK(oversight_category IN ('data_changes', 'avatars')), review_item_id TEXT NOT NULL, title TEXT NOT NULL, summary TEXT NOT NULL, link TEXT NOT NULL, frequency TEXT NOT NULL CHECK(frequency IN ('15_minutes', '1_hour', 'daily')), created_at TEXT NOT NULL, deliver_after TEXT NOT NULL, delivered_at TEXT ); CREATE TABLE IF NOT EXISTS automated_system_messages ( id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, description TEXT NOT NULL, event_key TEXT NOT NULL CHECK(event_key IN ('account.created')), enabled INTEGER NOT NULL DEFAULT 1 CHECK(enabled IN (0,1)), send_delay_minutes INTEGER NOT NULL DEFAULT 0 CHECK(send_delay_minutes BETWEEN 0 AND 10080), source TEXT NOT NULL, title TEXT NOT NULL, body TEXT NOT NULL, link TEXT, actions_json TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, updated_by TEXT REFERENCES users(id) ); CREATE TABLE IF NOT EXISTS automated_system_message_deliveries ( id TEXT PRIMARY KEY, message_id TEXT NOT NULL REFERENCES automated_system_messages(id) ON DELETE CASCADE, user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, event_key TEXT NOT NULL, event_id TEXT NOT NULL, scheduled_for TEXT NOT NULL, delivered_at TEXT, notification_id TEXT REFERENCES notifications(id) ON DELETE SET NULL, created_at TEXT NOT NULL, UNIQUE(message_id,user_id,event_key,event_id) ); CREATE INDEX IF NOT EXISTS automated_system_message_deliveries_due_idx ON automated_system_message_deliveries(delivered_at,scheduled_for); INSERT OR IGNORE INTO automated_system_messages(id,name,description,event_key,enabled,send_delay_minutes,source,title,body,link,actions_json,created_at,updated_at) VALUES('welcome-agent','Welcome Agent','Introduces newly registered agents to SHD.build community channels and contribution workflows','account.created',1,0,'ISAC COMMUNITY NETWORK','Welcome to SHD.build','Welcome, Agent. SHD.build is your community intelligence hub for The Division Resurgence. Explore field news, search the game database, discover community builds and guides, or configure your own loadout in the Builder. ISAC also lets every registered agent inspect game data and enable Contributor Mode to suggest additions and corrections for Oversight review. Join the SHD Discord to meet other agents, share intelligence, and help the network grow.','/account','[{"label":"EXPLORE NEWS","href":"/news"},{"label":"QUERY DATABASE","href":"/database"},{"label":"VIEW COMMUNITY BUILDS","href":"/builds"},{"label":"READ FIELD GUIDES","href":"/guides"},{"label":"OPEN BUILDER","href":"/builder"},{"label":"CONTRIBUTE THROUGH ISAC","href":"/isac/contributor-guidelines"},{"label":"JOIN SHD DISCORD","href":"https://discord.gg/dhpd33VwQK"}]','2026-07-26T00:00:00.000Z','2026-07-26T00:00:00.000Z'); CREATE INDEX IF NOT EXISTS oversight_notification_queue_due_idx ON oversight_notification_queue(delivered_at, deliver_after); CREATE INDEX IF NOT EXISTS oversight_notification_queue_user_category_idx ON oversight_notification_queue(user_id, oversight_category, delivered_at); 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 'private', summary TEXT, guide_body TEXT, source_build_id TEXT REFERENCES builds(id), loadout_json TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL); CREATE INDEX IF NOT EXISTS builds_visibility_updated_idx ON builds(visibility, updated_at DESC); 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 comment_threads ( id TEXT PRIMARY KEY, target_type TEXT NOT NULL CHECK(target_type IN ('build', 'guide', 'profile')), target_id TEXT NOT NULL, created_at TEXT NOT NULL, UNIQUE(target_type, target_id) ); CREATE TABLE IF NOT EXISTS comments ( id TEXT PRIMARY KEY, thread_id TEXT NOT NULL REFERENCES comment_threads(id) ON DELETE CASCADE, author_id TEXT NOT NULL REFERENCES users(id), parent_id TEXT REFERENCES comments(id), body_markdown TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'pending_review' CHECK(status IN ('pending_review', 'visible', 'removed')), created_at TEXT NOT NULL, updated_at TEXT NOT NULL, removed_at TEXT, removed_by TEXT REFERENCES users(id), removal_reason TEXT ); CREATE INDEX IF NOT EXISTS comments_thread_status_created_idx ON comments(thread_id, status, created_at DESC); CREATE INDEX IF NOT EXISTS comments_author_status_created_idx ON comments(author_id, status, created_at DESC); CREATE TABLE IF NOT EXISTS community_moderation_settings ( id TEXT PRIMARY KEY CHECK(id='primary'), automated_enabled INTEGER NOT NULL DEFAULT 0 CHECK(automated_enabled IN (0,1)), auto_publish_clean INTEGER NOT NULL DEFAULT 1 CHECK(auto_publish_clean IN (0,1)), hold_on_provider_error INTEGER NOT NULL DEFAULT 1 CHECK(hold_on_provider_error IN (0,1)), scan_comments INTEGER NOT NULL DEFAULT 1 CHECK(scan_comments IN (0,1)), scan_profiles INTEGER NOT NULL DEFAULT 1 CHECK(scan_profiles IN (0,1)), scan_guides INTEGER NOT NULL DEFAULT 1 CHECK(scan_guides IN (0,1)), scan_avatars INTEGER NOT NULL DEFAULT 1 CHECK(scan_avatars IN (0,1)), provider_model TEXT NOT NULL DEFAULT 'omni-moderation-latest' CHECK(length(trim(provider_model))>0), detect_pii INTEGER NOT NULL DEFAULT 1 CHECK(detect_pii IN (0,1)), max_links INTEGER NOT NULL DEFAULT 3 CHECK(max_links BETWEEN 0 AND 100), duplicate_window_seconds INTEGER NOT NULL DEFAULT 300 CHECK(duplicate_window_seconds BETWEEN 0 AND 86400), policy_version INTEGER NOT NULL DEFAULT 1 CHECK(policy_version>=1), updated_at TEXT NOT NULL, updated_by TEXT REFERENCES users(id) ); INSERT OR IGNORE INTO community_moderation_settings( id,automated_enabled,auto_publish_clean,hold_on_provider_error, scan_comments,scan_profiles,scan_guides,scan_avatars,provider_model,detect_pii, max_links,duplicate_window_seconds,policy_version,updated_at ) VALUES( 'primary',0,1,1,1,1,1,1,'omni-moderation-latest',1,3,300,1, '2026-07-27T00:00:00.000Z' ); CREATE TABLE IF NOT EXISTS community_moderation_thresholds ( category TEXT PRIMARY KEY CHECK(length(trim(category))>0), review_threshold REAL NOT NULL CHECK(review_threshold BETWEEN 0 AND 1), priority_threshold REAL NOT NULL CHECK(priority_threshold BETWEEN 0 AND 1 AND priority_threshold>=review_threshold), enabled INTEGER NOT NULL DEFAULT 1 CHECK(enabled IN (0,1)), updated_at TEXT NOT NULL, updated_by TEXT REFERENCES users(id) ); INSERT OR IGNORE INTO community_moderation_thresholds(category,review_threshold,priority_threshold,enabled,updated_at) VALUES('harassment',0.30,0.70,1,'2026-07-27T00:00:00.000Z'); INSERT OR IGNORE INTO community_moderation_thresholds(category,review_threshold,priority_threshold,enabled,updated_at) VALUES('harassment/threatening',0.10,0.40,1,'2026-07-27T00:00:00.000Z'); INSERT OR IGNORE INTO community_moderation_thresholds(category,review_threshold,priority_threshold,enabled,updated_at) VALUES('hate',0.20,0.60,1,'2026-07-27T00:00:00.000Z'); INSERT OR IGNORE INTO community_moderation_thresholds(category,review_threshold,priority_threshold,enabled,updated_at) VALUES('hate/threatening',0.10,0.40,1,'2026-07-27T00:00:00.000Z'); INSERT OR IGNORE INTO community_moderation_thresholds(category,review_threshold,priority_threshold,enabled,updated_at) VALUES('illicit',0.30,0.70,1,'2026-07-27T00:00:00.000Z'); INSERT OR IGNORE INTO community_moderation_thresholds(category,review_threshold,priority_threshold,enabled,updated_at) VALUES('illicit/violent',0.10,0.40,1,'2026-07-27T00:00:00.000Z'); INSERT OR IGNORE INTO community_moderation_thresholds(category,review_threshold,priority_threshold,enabled,updated_at) VALUES('self-harm',0.15,0.50,1,'2026-07-27T00:00:00.000Z'); INSERT OR IGNORE INTO community_moderation_thresholds(category,review_threshold,priority_threshold,enabled,updated_at) VALUES('self-harm/intent',0.10,0.35,1,'2026-07-27T00:00:00.000Z'); INSERT OR IGNORE INTO community_moderation_thresholds(category,review_threshold,priority_threshold,enabled,updated_at) VALUES('self-harm/instructions',0.10,0.35,1,'2026-07-27T00:00:00.000Z'); INSERT OR IGNORE INTO community_moderation_thresholds(category,review_threshold,priority_threshold,enabled,updated_at) VALUES('sexual',0.25,0.65,1,'2026-07-27T00:00:00.000Z'); INSERT OR IGNORE INTO community_moderation_thresholds(category,review_threshold,priority_threshold,enabled,updated_at) VALUES('sexual/minors',0.05,0.20,1,'2026-07-27T00:00:00.000Z'); INSERT OR IGNORE INTO community_moderation_thresholds(category,review_threshold,priority_threshold,enabled,updated_at) VALUES('violence',0.35,0.75,1,'2026-07-27T00:00:00.000Z'); INSERT OR IGNORE INTO community_moderation_thresholds(category,review_threshold,priority_threshold,enabled,updated_at) VALUES('violence/graphic',0.20,0.55,1,'2026-07-27T00:00:00.000Z'); CREATE TABLE IF NOT EXISTS community_moderation_terms ( id TEXT PRIMARY KEY, list_kind TEXT NOT NULL CHECK(list_kind IN ('allow','ban')), match_mode TEXT NOT NULL CHECK(match_mode IN ('word','phrase')), value TEXT NOT NULL CHECK(length(trim(value))>0), normalized_value TEXT NOT NULL CHECK(length(trim(normalized_value))>0), scopes_json TEXT NOT NULL DEFAULT '["comment","profile_bio","guide"]', enabled INTEGER NOT NULL DEFAULT 1 CHECK(enabled IN (0,1)), note TEXT, created_at TEXT NOT NULL, created_by TEXT NOT NULL REFERENCES users(id), updated_at TEXT NOT NULL, updated_by TEXT NOT NULL REFERENCES users(id), UNIQUE(list_kind,match_mode,normalized_value,scopes_json) ); CREATE INDEX IF NOT EXISTS community_moderation_terms_enabled_kind_idx ON community_moderation_terms(enabled,list_kind,normalized_value); CREATE TABLE IF NOT EXISTS moderated_content_revisions ( id TEXT PRIMARY KEY, content_type TEXT NOT NULL CHECK(content_type IN ('profile_bio','guide')), content_id TEXT NOT NULL, submitted_by TEXT NOT NULL REFERENCES users(id), body_markdown TEXT NOT NULL, content_hash TEXT NOT NULL CHECK(length(content_hash)>0), created_at TEXT NOT NULL, UNIQUE(content_type,content_id,content_hash) ); CREATE INDEX IF NOT EXISTS moderated_content_revisions_content_created_idx ON moderated_content_revisions(content_type,content_id,created_at DESC); CREATE TABLE IF NOT EXISTS content_moderation_scans ( id TEXT PRIMARY KEY, content_type TEXT NOT NULL CHECK(content_type IN ('comment','profile_bio','guide','avatar')), content_id TEXT NOT NULL, author_id TEXT NOT NULL REFERENCES users(id), content_hash TEXT NOT NULL CHECK(length(content_hash)>0), revision_id TEXT REFERENCES moderated_content_revisions(id), policy_version INTEGER NOT NULL CHECK(policy_version>=1), provider TEXT NOT NULL CHECK(length(trim(provider))>0), model TEXT NOT NULL CHECK(length(trim(model))>0), provider_request_id TEXT, status TEXT NOT NULL CHECK(status IN ('approved','review','priority_review','error')), risk_level TEXT NOT NULL CHECK(risk_level IN ('low','medium','high')), reason TEXT NOT NULL CHECK(length(trim(reason))>0), scores_json TEXT NOT NULL DEFAULT '{}', flags_json TEXT NOT NULL DEFAULT '[]', rule_matches_json TEXT NOT NULL DEFAULT '[]', created_at TEXT NOT NULL, completed_at TEXT, error_code TEXT, error_message TEXT, UNIQUE(content_type,content_id,content_hash,policy_version,provider,model), CHECK( (content_type IN ('comment','avatar') AND revision_id IS NULL) OR (content_type IN ('profile_bio','guide') AND revision_id IS NOT NULL) ), CHECK( (status='error' AND error_code IS NOT NULL) OR (status<>'error' AND error_code IS NULL AND error_message IS NULL) ) ); CREATE INDEX IF NOT EXISTS content_moderation_scans_pending_queue_idx ON content_moderation_scans(status,created_at) WHERE status IN ('review','priority_review'); CREATE INDEX IF NOT EXISTS content_moderation_scans_content_idx ON content_moderation_scans(content_type,content_id,created_at DESC); CREATE TABLE IF NOT EXISTS content_moderation_reviews ( id TEXT PRIMARY KEY, scan_id TEXT NOT NULL REFERENCES content_moderation_scans(id), reviewer_id TEXT NOT NULL REFERENCES users(id), decision TEXT NOT NULL CHECK(decision IN ('approved','rejected','superseded')), reason TEXT NOT NULL CHECK(length(trim(reason))>0), created_at TEXT NOT NULL ); CREATE INDEX IF NOT EXISTS content_moderation_reviews_scan_created_idx ON content_moderation_reviews(scan_id,created_at DESC); CREATE INDEX IF NOT EXISTS content_moderation_reviews_reviewer_created_idx ON content_moderation_reviews(reviewer_id,created_at DESC); CREATE TABLE IF NOT EXISTS user_content_reports ( id TEXT PRIMARY KEY, content_type TEXT NOT NULL CHECK(content_type IN ('comment','profile_bio','guide','avatar')), content_id TEXT NOT NULL, reporter_id TEXT NOT NULL REFERENCES users(id), category TEXT NOT NULL CHECK(category IN ( 'spam','harassment','hate','sexual','violence','self_harm','privacy','other' )), details TEXT, status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending','resolved','dismissed')), created_at TEXT NOT NULL, reviewed_at TEXT, reviewed_by TEXT REFERENCES users(id), resolution_reason TEXT, CHECK( (status='pending' AND reviewed_at IS NULL AND reviewed_by IS NULL AND resolution_reason IS NULL) OR (status IN ('resolved','dismissed') AND reviewed_at IS NOT NULL AND reviewed_by IS NOT NULL AND resolution_reason IS NOT NULL AND length(trim(resolution_reason))>0) ) ); CREATE INDEX IF NOT EXISTS user_content_reports_pending_queue_idx ON user_content_reports(created_at) WHERE status='pending'; CREATE INDEX IF NOT EXISTS user_content_reports_content_idx ON user_content_reports(content_type,content_id,created_at DESC); CREATE UNIQUE INDEX IF NOT EXISTS user_content_reports_one_pending_idx ON user_content_reports(reporter_id,content_type,content_id) WHERE status='pending'; 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-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-community-comments-moderate','community.comments.moderate','Review and moderate public build, guide, and agent-profile comments'); INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-community-profiles-moderate','community.profiles.moderate','Review and hide public agent-profile biographies'); 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 permissions(id,name,description) VALUES('permission-ai-mechanic-generate','ai.mechanic.generate','Use the OpenAI-assisted mechanic drafting workflow'); INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-ai-calculation-generate','ai.calculation.generate','Use the OpenAI-assisted combat calculation revision drafting workflow'); 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'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-moderator','permission-community-comments-moderate'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-moderator','permission-community-profiles-moderate'); 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','calculations','attributes','talents','brands','gear','weapons','weaponmods','gearmods','protocols','skillchips','specializations')), 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, description TEXT, 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_unit_nocase_idx ON stat_attributes(name COLLATE NOCASE, unit); 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 conditional_effect_rules ( id TEXT PRIMARY KEY, source_type TEXT NOT NULL CHECK(source_type IN ( 'talent','os_protocol_talent','skill_chip_talent','specialization_tree_rank' )), source_id TEXT NOT NULL, configured INTEGER NOT NULL DEFAULT 0 CHECK(configured IN (0,1)), script_version INTEGER NOT NULL DEFAULT 1 CHECK(script_version>0), simulation_readiness TEXT NOT NULL DEFAULT 'ready' CHECK(simulation_readiness IN ( 'ready','requires_assumptions','captured_only' )), scenario_inputs_json TEXT NOT NULL DEFAULT '[]', created_at TEXT NOT NULL, updated_at TEXT NOT NULL, UNIQUE(source_type,source_id) ); CREATE TABLE IF NOT EXISTS conditional_effect_state_pools ( id TEXT PRIMARY KEY, rule_id TEXT NOT NULL REFERENCES conditional_effect_rules(id) ON DELETE CASCADE, state_key TEXT NOT NULL, label TEXT NOT NULL, state_kind TEXT NOT NULL DEFAULT 'stack' CHECK(state_kind IN ( 'flag','count','stack','duration','scalar','target_mark','resource' )), state_scope TEXT NOT NULL DEFAULT 'agent' CHECK(state_scope IN ('agent','target')), initial_value REAL NOT NULL DEFAULT 0, minimum_value REAL, maximum_value REAL NOT NULL DEFAULT 1 CHECK(maximum_value>0), best_case_value REAL, duration_seconds REAL CHECK(duration_seconds IS NULL OR duration_seconds>0), expiry_policy TEXT NOT NULL DEFAULT 'clear_all' CHECK(expiry_policy IN ('clear_all','decrement_one','none')), refresh_policy TEXT NOT NULL DEFAULT 'refresh' CHECK(refresh_policy IN ('refresh','extend','ignore')), parameters_json TEXT NOT NULL DEFAULT '{}', sort_order INTEGER NOT NULL CHECK(sort_order>=1), created_at TEXT NOT NULL, updated_at TEXT NOT NULL, UNIQUE(rule_id,state_key), UNIQUE(rule_id,sort_order), CHECK(minimum_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 best_case_value<=maximum_value) ); CREATE INDEX IF NOT EXISTS conditional_effect_state_pools_rule_idx ON conditional_effect_state_pools(rule_id,sort_order); CREATE TABLE IF NOT EXISTS conditional_effect_clauses ( id TEXT PRIMARY KEY, rule_id TEXT NOT NULL REFERENCES conditional_effect_rules(id) ON DELETE CASCADE, clause_key TEXT NOT NULL, label TEXT NOT NULL, event_kind TEXT NOT NULL, recipe_key TEXT, recipe_parameters_json TEXT NOT NULL DEFAULT '{}', conditions_json TEXT NOT NULL DEFAULT '{"kind":"true"}', reducer_json TEXT NOT NULL DEFAULT '{"kind":"none"}', weight_expression_json TEXT, emission_kind TEXT NOT NULL DEFAULT 'each_match' CHECK(emission_kind IN ( 'each_match','threshold_crossed','while_true','interval_elapsed','value_changed' )), state_pool_id TEXT REFERENCES conditional_effect_state_pools(id) ON DELETE SET NULL, mutations_json TEXT NOT NULL DEFAULT '[]', cooldown_seconds REAL CHECK(cooldown_seconds IS NULL OR cooldown_seconds>0), output_branch TEXT NOT NULL DEFAULT 'active', sort_order INTEGER NOT NULL CHECK(sort_order>=1), active INTEGER NOT NULL DEFAULT 1 CHECK(active IN (0,1)), created_at TEXT NOT NULL, updated_at TEXT NOT NULL, UNIQUE(rule_id,clause_key), UNIQUE(rule_id,sort_order) ); CREATE INDEX IF NOT EXISTS conditional_effect_clauses_event_idx ON conditional_effect_clauses(event_kind,rule_id); CREATE TABLE IF NOT EXISTS conditional_effect_outcomes ( id TEXT PRIMARY KEY, rule_id TEXT NOT NULL REFERENCES conditional_effect_rules(id) ON DELETE CASCADE, clause_id TEXT REFERENCES conditional_effect_clauses(id) ON DELETE CASCADE, state_pool_id TEXT REFERENCES conditional_effect_state_pools(id) ON DELETE SET NULL, effect_key TEXT NOT NULL, variant_key TEXT NOT NULL DEFAULT 'base', outcome_key TEXT NOT NULL DEFAULT 'active', application_kind TEXT NOT NULL DEFAULT 'active_modifier' CHECK(application_kind IN ( 'active_modifier','direct_damage','restore_health','restore_armor','restore_ammo','resource_cost','marker' )), effect_scope TEXT NOT NULL DEFAULT 'self' CHECK(effect_scope IN ( 'self','equipped_weapon','target','allies','self_and_allies','assigned_ability','all_skills' )), stat_attribute_id TEXT REFERENCES stat_attributes(id), value REAL NOT NULL, value_expression_json TEXT, value_basis TEXT NOT NULL DEFAULT 'flat' CHECK(value_basis IN ( 'flat','max_health_percent','current_health_percent','weapon_base_percent' )), value_scaling TEXT NOT NULL DEFAULT 'per_state_unit' CHECK(value_scaling IN ('flat','per_state_unit')), unit TEXT, damage_basis TEXT CHECK(damage_basis IS NULL OR damage_basis IN ('flat','weapon_base_percent')), application_phase TEXT NOT NULL DEFAULT 'while_active' CHECK(application_phase IN ( 'while_active','on_event','after_event','on_enter','on_exit' )), combat_timing TEXT NOT NULL DEFAULT 'current' CHECK(combat_timing IN ('pre_event','current')), resource_kind TEXT CHECK(resource_kind IS NULL OR resource_kind IN ('health','armor','ammo')), min_state_value REAL, max_state_value REAL, description_slot INTEGER CHECK(description_slot IS NULL OR description_slot>=1), sort_order INTEGER NOT NULL CHECK(sort_order>=1), created_at TEXT NOT NULL, updated_at TEXT NOT NULL, UNIQUE(rule_id,variant_key,outcome_key,sort_order), CHECK(min_state_value IS NULL OR min_state_value>=0), CHECK(max_state_value IS NULL OR max_state_value>=0), CHECK(min_state_value IS NULL OR max_state_value IS NULL OR max_state_value>=min_state_value), CHECK( (application_kind='active_modifier' AND stat_attribute_id IS NOT NULL AND damage_basis IS NULL) OR (application_kind='direct_damage' AND damage_basis IS NOT NULL) OR application_kind IN ('restore_health','restore_armor','restore_ammo','resource_cost','marker') ) ); CREATE INDEX IF NOT EXISTS conditional_effect_outcomes_rule_idx ON conditional_effect_outcomes(rule_id,variant_key,outcome_key,sort_order); 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_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, specialization_ability_id TEXT NOT NULL REFERENCES specialization_abilities(id), 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 INDEX IF NOT EXISTS skill_chip_sets_ability_idx ON skill_chip_sets(specialization_ability_id,active); 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_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 weapon_mods ( id TEXT PRIMARY KEY, name TEXT NOT NULL, slot_type TEXT NOT NULL CHECK(slot_type IN ('Optics','Muzzle','Underbarrel','Magazine','Barrel','Upper Receiver','Lower Receiver','Trigger')), exotic_weapon_id TEXT REFERENCES weapons(id), 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 weapon_mods_name_nocase_idx ON weapon_mods(name COLLATE NOCASE); CREATE TABLE IF NOT EXISTS weapon_mod_compatible_categories ( weapon_mod_id TEXT NOT NULL REFERENCES weapon_mods(id) ON DELETE CASCADE, weapon_category TEXT NOT NULL CHECK(weapon_category IN ('shotgun','rifle','mmr','ar','lmg','smg')), PRIMARY KEY(weapon_mod_id,weapon_category) ); CREATE TABLE IF NOT EXISTS weapon_mod_slots ( weapon_id TEXT NOT NULL REFERENCES weapons(id) ON DELETE CASCADE, tier INTEGER NOT NULL CHECK(tier BETWEEN 0 AND 2), slot_type TEXT NOT NULL CHECK(slot_type IN ('Optics','Muzzle','Underbarrel','Magazine','Barrel','Upper Receiver','Lower Receiver','Trigger')), PRIMARY KEY(weapon_id,tier,slot_type) ); CREATE TABLE IF NOT EXISTS weapon_mod_quality_values ( weapon_mod_id TEXT NOT NULL REFERENCES weapon_mods(id) ON DELETE CASCADE, quality TEXT NOT NULL CHECK(quality IN ('Standard','Specialized','Superior','High-end')), primary_level_1_min REAL NOT NULL, primary_level_1_max REAL NOT NULL, primary_level_20_min REAL NOT NULL, primary_level_20_max REAL NOT NULL, PRIMARY KEY(weapon_mod_id,quality), CHECK(primary_level_1_max>=primary_level_1_min), CHECK(primary_level_20_max>=primary_level_20_min), CHECK(primary_level_20_min>=primary_level_1_min), CHECK(primary_level_20_max>=primary_level_1_max) ); CREATE TABLE IF NOT EXISTS weapon_mod_secondary_options ( weapon_mod_id TEXT NOT NULL REFERENCES weapon_mods(id) ON DELETE CASCADE, stat_attribute_id TEXT NOT NULL REFERENCES stat_attributes(id), PRIMARY KEY(weapon_mod_id,stat_attribute_id) ); CREATE TABLE IF NOT EXISTS weapon_mod_secondary_quality_values ( weapon_mod_id TEXT NOT NULL, stat_attribute_id TEXT NOT NULL, quality TEXT NOT NULL CHECK(quality IN ('Standard','Specialized','Superior','High-end')), min_value REAL NOT NULL, max_value REAL NOT NULL, PRIMARY KEY(weapon_mod_id,stat_attribute_id,quality), FOREIGN KEY(weapon_mod_id,stat_attribute_id) REFERENCES weapon_mod_secondary_options(weapon_mod_id,stat_attribute_id) ON DELETE CASCADE, CHECK(max_value>=min_value) ); CREATE TABLE IF NOT EXISTS weapon_mod_exotic_attributes ( id TEXT PRIMARY KEY, weapon_mod_id TEXT NOT NULL REFERENCES weapon_mods(id) ON DELETE CASCADE, stat_attribute_id TEXT NOT NULL REFERENCES stat_attributes(id), level_1_value REAL NOT NULL, level_20_value REAL NOT NULL, sort_order INTEGER NOT NULL CHECK(sort_order BETWEEN 1 AND 12), UNIQUE(weapon_mod_id,stat_attribute_id), UNIQUE(weapon_mod_id,sort_order) ); CREATE TABLE IF NOT EXISTS weapon_mod_effects ( id TEXT PRIMARY KEY, weapon_mod_id TEXT NOT NULL REFERENCES weapon_mods(id) ON DELETE CASCADE, stat_attribute_id TEXT NOT NULL REFERENCES stat_attributes(id), sort_order INTEGER NOT NULL CHECK(sort_order BETWEEN 1 AND 2), UNIQUE(weapon_mod_id,stat_attribute_id), UNIQUE(weapon_mod_id,sort_order) ); CREATE TABLE IF NOT EXISTS weapon_mod_effect_quality_values ( effect_id TEXT NOT NULL REFERENCES weapon_mod_effects(id) ON DELETE CASCADE, quality TEXT NOT NULL CHECK(quality IN ('Standard','Specialized','Superior','High-end')), value REAL NOT NULL, PRIMARY KEY(effect_id,quality) ); CREATE TABLE IF NOT EXISTS gear_mods ( 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 gear_mods_name_nocase_idx ON gear_mods(name COLLATE NOCASE); CREATE TABLE IF NOT EXISTS gear_mod_quality_values ( gear_mod_id TEXT NOT NULL REFERENCES gear_mods(id) ON DELETE CASCADE, quality TEXT NOT NULL CHECK(quality IN ('Standard','Specialized','Superior','High-end')), primary_level_1_min REAL NOT NULL, primary_level_1_max REAL NOT NULL, primary_level_20_min REAL NOT NULL, primary_level_20_max REAL NOT NULL, PRIMARY KEY(gear_mod_id,quality), CHECK(primary_level_1_max>=primary_level_1_min), CHECK(primary_level_20_max>=primary_level_20_min), CHECK(primary_level_20_min>=primary_level_1_min), CHECK(primary_level_20_max>=primary_level_1_max) ); CREATE TABLE IF NOT EXISTS gear_mod_secondary_options ( gear_mod_id TEXT NOT NULL REFERENCES gear_mods(id) ON DELETE CASCADE, stat_attribute_id TEXT NOT NULL REFERENCES stat_attributes(id), PRIMARY KEY(gear_mod_id,stat_attribute_id) ); CREATE TABLE IF NOT EXISTS gear_mod_secondary_quality_values ( gear_mod_id TEXT NOT NULL, stat_attribute_id TEXT NOT NULL, quality TEXT NOT NULL CHECK(quality IN ('Standard','Specialized','Superior','High-end')), min_value REAL NOT NULL, max_value REAL NOT NULL, PRIMARY KEY(gear_mod_id,stat_attribute_id,quality), FOREIGN KEY(gear_mod_id,stat_attribute_id) REFERENCES gear_mod_secondary_options(gear_mod_id,stat_attribute_id) ON DELETE CASCADE, CHECK(max_value>=min_value) ); CREATE TABLE IF NOT EXISTS specialization_progression_config ( id TEXT PRIMARY KEY CHECK(id='default'), xp_points INTEGER NOT NULL CHECK(xp_points>=0), shd_cache_points INTEGER NOT NULL CHECK(shd_cache_points>=0), updated_at TEXT NOT NULL ); INSERT OR IGNORE INTO specialization_progression_config(id,xp_points,shd_cache_points,updated_at) VALUES('default',30,19,'2026-07-26T00:00:00.000Z'); CREATE TABLE IF NOT EXISTS specialization_rank_cost_profiles ( rank INTEGER PRIMARY KEY CHECK(rank BETWEEN 1 AND 3), point_cost INTEGER NOT NULL CHECK(point_cost>0), credit_cost INTEGER NOT NULL CHECK(credit_cost>=0), updated_at TEXT NOT NULL ); INSERT OR IGNORE INTO specialization_rank_cost_profiles(rank,point_cost,credit_cost,updated_at) VALUES(1,1,2333,'2026-07-26T00:00:00.000Z'); INSERT OR IGNORE INTO specialization_rank_cost_profiles(rank,point_cost,credit_cost,updated_at) VALUES(2,2,10000,'2026-07-26T00:00:00.000Z'); INSERT OR IGNORE INTO specialization_rank_cost_profiles(rank,point_cost,credit_cost,updated_at) VALUES(3,3,90000,'2026-07-26T00:00:00.000Z'); CREATE TABLE IF NOT EXISTS upgrade_resources ( id TEXT PRIMARY KEY, name TEXT NOT NULL, resource_type TEXT NOT NULL CHECK(resource_type IN ('Currency','Component','Progression')), quality TEXT CHECK(quality IS NULL OR quality IN ('Standard','Specialized','Superior','High-end')), 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 upgrade_resources_name_nocase_idx ON upgrade_resources(name COLLATE NOCASE); INSERT OR IGNORE INTO upgrade_resources(id,name,resource_type,quality,created_at,updated_at) VALUES('resource-credits','Credits','Currency',NULL,'2026-07-26T00:00:00.000Z','2026-07-26T00:00:00.000Z'); INSERT OR IGNORE INTO upgrade_resources(id,name,resource_type,quality,created_at,updated_at) VALUES('resource-electronics-alpha','Electronics Alpha Component','Component','Specialized','2026-07-26T00:00:00.000Z','2026-07-26T00:00:00.000Z'); INSERT OR IGNORE INTO upgrade_resources(id,name,resource_type,quality,created_at,updated_at) VALUES('resource-electronics-beta','Electronics Beta Component','Component','Specialized','2026-07-26T00:00:00.000Z','2026-07-26T00:00:00.000Z'); INSERT OR IGNORE INTO upgrade_resources(id,name,resource_type,quality,created_at,updated_at) VALUES('resource-electronics-gamma','Electronics Gamma Component','Component','Specialized','2026-07-26T00:00:00.000Z','2026-07-26T00:00:00.000Z'); INSERT OR IGNORE INTO upgrade_resources(id,name,resource_type,quality,created_at,updated_at) VALUES('resource-electronics-omega','Electronics Omega Component','Component','High-end','2026-07-26T00:00:00.000Z','2026-07-26T00:00:00.000Z'); INSERT OR IGNORE INTO upgrade_resources(id,name,resource_type,quality,created_at,updated_at) VALUES('resource-shd-tech-cache','SHD Tech Cache','Progression','Superior','2026-07-26T00:00:00.000Z','2026-07-26T00:00:00.000Z'); CREATE TABLE IF NOT EXISTS specializations ( id TEXT PRIMARY KEY, name TEXT NOT NULL, description TEXT, 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 specializations_name_nocase_idx ON specializations(name COLLATE NOCASE); CREATE TABLE IF NOT EXISTS specialization_focuses ( id TEXT PRIMARY KEY, specialization_id TEXT NOT NULL REFERENCES specializations(id) ON DELETE CASCADE, name TEXT NOT NULL, description TEXT, primary_attribute TEXT NOT NULL CHECK(primary_attribute IN ('Firepower','Toughness','Engineering')), display_order INTEGER NOT NULL DEFAULT 0 CHECK(display_order>=0), builder_ready INTEGER NOT NULL DEFAULT 0 CHECK(builder_ready IN (0,1)), 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_focuses_specialization_idx ON specialization_focuses(specialization_id,display_order,name); CREATE TABLE IF NOT EXISTS specialization_abilities ( id TEXT PRIMARY KEY, specialization_id TEXT NOT NULL REFERENCES specializations(id) ON DELETE CASCADE, name TEXT NOT NULL, description TEXT NOT NULL, duration_seconds REAL CHECK(duration_seconds IS NULL OR duration_seconds>0), cooldown_seconds REAL CHECK(cooldown_seconds IS NULL OR cooldown_seconds>0), 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 TABLE IF NOT EXISTS specialization_ability_attacks ( ability_id TEXT PRIMARY KEY REFERENCES specialization_abilities(id) ON DELETE CASCADE, base_damage REAL NOT NULL CHECK(base_damage>=0), damage_type TEXT NOT NULL CHECK(damage_type IN ('Blast','Pierce','Shredding')), delivery TEXT NOT NULL CHECK(delivery IN ('direct','periodic')), hits_per_activation INTEGER NOT NULL DEFAULT 1 CHECK(hits_per_activation>0), tick_interval_seconds REAL CHECK(tick_interval_seconds IS NULL OR tick_interval_seconds>0), damage_duration_seconds REAL CHECK(damage_duration_seconds IS NULL OR damage_duration_seconds>0), cast_seconds REAL NOT NULL DEFAULT 0 CHECK(cast_seconds>=0), target_cap INTEGER NOT NULL DEFAULT 1 CHECK(target_cap>0), charge_count INTEGER NOT NULL DEFAULT 1 CHECK(charge_count>0), charge_policy TEXT NOT NULL DEFAULT 'none' CHECK(charge_policy IN ('none','independent','full_set')), updated_at TEXT NOT NULL, CHECK( (delivery='direct' AND tick_interval_seconds IS NULL AND damage_duration_seconds IS NULL) OR (delivery='periodic' AND tick_interval_seconds IS NOT NULL AND damage_duration_seconds IS NOT NULL) ), CHECK(charge_policy<>'none' OR charge_count=1) ); CREATE INDEX IF NOT EXISTS specialization_abilities_specialization_idx ON specialization_abilities(specialization_id,name); CREATE TABLE IF NOT EXISTS specialization_ability_effects ( id TEXT PRIMARY KEY, ability_id TEXT NOT NULL REFERENCES specialization_abilities(id) ON DELETE CASCADE, effect_order INTEGER NOT NULL CHECK(effect_order>=1), stat_attribute_id TEXT REFERENCES stat_attributes(id), label TEXT NOT NULL, value REAL, unit TEXT, UNIQUE(ability_id,effect_order) ); CREATE TABLE IF NOT EXISTS specialization_focus_abilities ( focus_id TEXT NOT NULL REFERENCES specialization_focuses(id) ON DELETE CASCADE, ability_slot INTEGER NOT NULL CHECK(ability_slot BETWEEN 1 AND 3), ability_id TEXT NOT NULL REFERENCES specialization_abilities(id), PRIMARY KEY(focus_id,ability_slot), UNIQUE(focus_id,ability_id) ); CREATE TABLE IF NOT EXISTS specialization_tree_nodes ( id TEXT PRIMARY KEY, focus_id TEXT NOT NULL REFERENCES specialization_focuses(id) ON DELETE CASCADE, name TEXT NOT NULL, description_template TEXT NOT NULL, row_number INTEGER NOT NULL CHECK(row_number>=1), column_number INTEGER NOT NULL CHECK(column_number>=1), max_rank INTEGER NOT NULL CHECK(max_rank BETWEEN 1 AND 3), active INTEGER NOT NULL DEFAULT 1 CHECK(active IN (0,1)), created_at TEXT NOT NULL, updated_at TEXT NOT NULL, UNIQUE(focus_id,row_number,column_number), UNIQUE(focus_id,name) ); CREATE INDEX IF NOT EXISTS specialization_tree_nodes_focus_idx ON specialization_tree_nodes(focus_id,row_number,column_number); CREATE TABLE IF NOT EXISTS specialization_tree_node_ranks ( node_id TEXT NOT NULL REFERENCES specialization_tree_nodes(id) ON DELETE CASCADE, rank INTEGER NOT NULL CHECK(rank BETWEEN 1 AND 3), description TEXT NOT NULL, PRIMARY KEY(node_id,rank) ); CREATE TABLE IF NOT EXISTS specialization_tree_rank_effects ( id TEXT PRIMARY KEY, node_id TEXT NOT NULL, rank INTEGER NOT NULL, effect_order INTEGER NOT NULL CHECK(effect_order>=1), effect_type TEXT NOT NULL CHECK(effect_type IN ('Attribute','Ability','Custom')), stat_attribute_id TEXT REFERENCES stat_attributes(id), ability_id TEXT REFERENCES specialization_abilities(id), label TEXT NOT NULL, value REAL, unit TEXT, FOREIGN KEY(node_id,rank) REFERENCES specialization_tree_node_ranks(node_id,rank) ON DELETE CASCADE, UNIQUE(node_id,rank,effect_order), CHECK( (effect_type='Attribute' AND stat_attribute_id IS NOT NULL) OR (effect_type='Ability' AND ability_id IS NOT NULL) OR effect_type='Custom' ) ); CREATE TABLE IF NOT EXISTS specialization_tree_rank_costs ( node_id TEXT NOT NULL, rank INTEGER NOT NULL, resource_id TEXT NOT NULL REFERENCES upgrade_resources(id), quantity INTEGER NOT NULL CHECK(quantity>0), 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 skill_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 CHECK(stage_key IN ( 'flat_damage','engineering','skill_damage','critical_chance','critical_damage', 'multishot_chance','multishot_damage','final_damage', 'cooldown_recovery','duration' )), operator TEXT NOT NULL CHECK(operator IN ('add','multiply')), 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, UNIQUE(revision_id,stat_attribute_id,stage_key), 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, 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) ); CREATE TABLE IF NOT EXISTS managed_images ( id TEXT PRIMARY KEY, name TEXT NOT NULL, category TEXT NOT NULL, tags_json TEXT NOT NULL DEFAULT '[]', is_icon INTEGER NOT NULL DEFAULT 0 CHECK(is_icon IN (0,1)), force_white INTEGER NOT NULL DEFAULT 0 CHECK(force_white IN (0,1)), storage_key TEXT NOT NULL UNIQUE, original_filename TEXT NOT NULL, byte_size INTEGER NOT NULL CHECK(byte_size > 0), width INTEGER NOT NULL CHECK(width > 0), height INTEGER NOT NULL CHECK(height > 0), source_entity_type TEXT, source_entity_id TEXT, created_by TEXT NOT NULL REFERENCES users(id), created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE UNIQUE INDEX IF NOT EXISTS managed_images_name_nocase_idx ON managed_images(name COLLATE NOCASE); CREATE INDEX IF NOT EXISTS managed_images_category_idx ON managed_images(category); CREATE UNIQUE INDEX IF NOT EXISTS managed_images_source_entity_idx ON managed_images(source_entity_type,source_entity_id) WHERE source_entity_type IS NOT NULL AND source_entity_id IS NOT NULL; CREATE TABLE IF NOT EXISTS external_service_status ( provider TEXT PRIMARY KEY, credits_total REAL, credits_subscription REAL, credits_payg REAL, credits_enterprise REAL, free_calls INTEGER, checked_at TEXT, next_check_at TEXT, last_charge REAL, last_charge_at TEXT, error_message TEXT, severity TEXT NOT NULL DEFAULT 'unknown' CHECK(severity IN ('unknown','ok','warning','critical')), episode_id TEXT, episode_started_at TEXT, recovered_at TEXT, updated_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS external_service_alert_deliveries ( id TEXT PRIMARY KEY, provider TEXT NOT NULL REFERENCES external_service_status(provider), episode_id TEXT NOT NULL, severity TEXT NOT NULL CHECK(severity IN ('warning','critical')), user_id TEXT NOT NULL REFERENCES users(id), notification_id TEXT REFERENCES notifications(id), created_at TEXT NOT NULL, delivered_at TEXT, UNIQUE(provider,episode_id,severity,user_id) ); CREATE INDEX IF NOT EXISTS external_service_alert_pending_idx ON external_service_alert_deliveries(delivered_at,created_at); CREATE TABLE IF NOT EXISTS site_launch_settings ( id TEXT PRIMARY KEY CHECK(id='primary'), coming_soon_enabled INTEGER NOT NULL DEFAULT 1 CHECK(coming_soon_enabled IN (0,1)), account_creation_enabled INTEGER NOT NULL DEFAULT 1 CHECK(account_creation_enabled IN (0,1)), news_enabled INTEGER NOT NULL DEFAULT 1 CHECK(news_enabled IN (0,1)), database_enabled INTEGER NOT NULL DEFAULT 1 CHECK(database_enabled IN (0,1)), builds_enabled INTEGER NOT NULL DEFAULT 1 CHECK(builds_enabled IN (0,1)), guides_enabled INTEGER NOT NULL DEFAULT 1 CHECK(guides_enabled IN (0,1)), builder_enabled INTEGER NOT NULL DEFAULT 1 CHECK(builder_enabled IN (0,1)), updated_at TEXT NOT NULL, updated_by TEXT REFERENCES users(id) ); INSERT OR IGNORE INTO site_launch_settings( id,coming_soon_enabled,account_creation_enabled,news_enabled,database_enabled, builds_enabled,guides_enabled,builder_enabled,updated_at ) VALUES('primary',1,1,1,1,1,1,1,'2026-07-27T00:00:00.000Z'); INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-site-settings-manage','site.settings.manage','Manage launch mode, account creation, and public section availability');