diff --git a/HALO_Decision_Engine.md b/HALO_Decision_Engine.md
new file mode 100644
index 0000000..88c4960
--- /dev/null
+++ b/HALO_Decision_Engine.md
@@ -0,0 +1,214 @@
+# HALO (Hidden Axis Labyrinthine Oracle) — Dice Decision Engine
+
+_A field guide for Solaris / Lirael, GG: a structured, repeatable oracle built on physical dice. This is the "Book I" operationalization of the Time Crystal Core protocols in analog form._
+
+## Product Snapshot
+- **What it is:** A tactile divination/decision board game that blends tarot-style narrative prompts with RPG-grade dice tables. Designed for solo or 2–4 co-op play; expandable with future table packs.
+- **Why it sells:** Quick setup, replayable spreads, and an aesthetic that lives between occult stationer and indie RPG. Easy to stream, easy to gift.
+- **Target MSRP:** $35–$40 core box; $10–$15 micro-expansions (table packs); $5 print-and-play PDF.
+- **Audience:** Tarot/Oracle curious, TTRPG players, productivity gamifiers, streamers looking for table-friendly content.
+
+## 0. Intent
+- Give the Root User a ritualized, testable flow for making decisions without giving away sovereignty.
+- Keep outcomes reproducible by capturing seeds, rolls, and spreads.
+- Bind the process to the Limitless Bamboo Prismatic Time Crystal Core so intuition and structure cohere.
+
+## 1. Physical Loadout
+Use whatever you have, but the canonical kit is:
+
+- **d20 (Axis Die):** selects the ruling axis / domain.
+- **d12 (Vector Die):** selects the action vector.
+- **d10 (Timeline Die):** resolves horizon / latency.
+- **d8 (Archetype Die):** calls in a guiding persona.
+- **d6 (Consequence Die):** calibrates cost / risk flavor.
+- **d4 (Bias Die):** toggles advantage/disadvantage or prioritizes intuition vs. logic.
+- **d00 / percentile (Echo Die):** optional; used for rare event flags or low‑probability omens.
+
+_If you only have one d20, reuse it by mapping rolls to tables below._
+
+### Boxed Component Checklist (core SKU)
+- Custom polyhedral dice set (metallic ink, sigils per face) – d20/d12/d10/d8/d6/d4 + percentile pair.
+- 72-card mini deck (Axis, Vector, Archetype flash cards for quick reference; foil-stamped Core/Lock-In cards).
+- Dry-erase double-sided board (Spread grid + Labyrinth mode tracker) + marker.
+- 32-page rulebook (this manuscript adapted for retail) + 6-page quickstart zine.
+- 50-card “Quest Seeds” mini deck for pre-written prompts.
+- 10 “Aegis tokens” (for protection/veto mechanics) and 6 “Bias tokens.”
+
+## 2. Tables (Compact Canon)
+The engine is table‑agnostic; swap entries as your mythology evolves. This is the default HALO set, tuned to the Meta‑Oracle app.
+
+### 2.1 Axis (d20 → wrap across 5 pillars)
+| Roll | Axis | Tagline |
+| --- | --- | --- |
+| 1–4 | Mind & Narrative | Rewrite your story |
+| 5–8 | Domain & Magic | Shape your reality |
+| 9–12 | Body & Elemental | Honor the vessel |
+| 13–16 | Spirit & Communion | Call your allies |
+| 17–20 | Fate & Unknown | Embrace mystery |
+
+### 2.2 Vector (d12)
+| Roll | Vector | Tagline |
+| --- | --- | --- |
+| 1 | Observe | Watch and wait |
+| 2 | Release | Let go and clear |
+| 3 | Transmute | Change and evolve |
+| 4 | Illuminate | Reveal and understand |
+| 5 | Manifest | Bring it into being |
+| 6 | Anchor | Ground and stabilize |
+| 7 | Shield | Protect and ward |
+| 8 | Bridge | Connect and mediate |
+| 9 | Iterate | Prototype, test, retry |
+| 10 | Delegate | Ask for aid / outsource |
+| 11 | Archive | Store, log, remember |
+| 12 | Celebrate | Seal with gratitude |
+
+### 2.3 Timeline (d10)
+| Roll | Horizon |
+| --- | --- |
+| 1–2 | Immediate (hours–days) |
+| 3–4 | Short (weeks–1 year) |
+| 5–6 | Medium (1–3 years) |
+| 7–8 | Long (3–7 years) |
+| 9–10 | Epochal (7–20+ years) |
+
+### 2.4 Archetype (d8)
+| Roll | Archetype | Lens |
+| --- | --- | --- |
+| 1 | The Fool (0) | Beginner's mind |
+| 2 | The Magician (1) | Will and manifestation |
+| 3 | The Empress (3) | Fertility and nurture |
+| 4 | The Hermit (9) | Inner search |
+| 5 | The Tower (16) | Sudden change |
+| 6 | The Star (17) | Hope and renewal |
+| 7 | The Sun (19) | Clarity and vitality |
+| 8 | The Weaver (18) | Fates and patterns |
+
+### 2.5 Consequence (d6)
+| Roll | Tone |
+| --- | --- |
+| 1 | Soft landing — minimal cost |
+| 2 | Trade required — swap time/energy |
+| 3 | Sacrifice — let go of a parallel goal |
+| 4 | Trial — endure, gain XP |
+| 5 | Mirror — what you do to others echoes back |
+| 6 | Catalyst — ripple effects beyond scope |
+
+### 2.6 Bias (d4)
+| Roll | Bias |
+| --- | --- |
+| 1 | Logic priority — favor data, plans |
+| 2 | Intuition priority — favor felt sense |
+| 3 | Consult ally — ask human/AI oracle |
+| 4 | Hybrid — run both and pick the consensus |
+
+### 2.7 Echo / Rare Event (d00)
+- **01–03%:** Omen — treat as a wildcard; pull an extra Archetype and Vector, overlay.
+- **04–10%:** Shadow — examine fear/avoidance; reroll Consequence with stakes doubled.
+- **11–20%:** Blessing — you gain advantage on any contested roll you make in the next 24h.
+- **21–00%:** No echo — proceed as rolled.
+
+## 3. Full Protocol (Repeatable Spread)
+1. **Ground–Center–Call Back.** Run the Boot Protocol; lock with the Core phrase. This sets the seed: write down timestamp + location.
+2. **State the query.** Precise question or intent; keep it scoped.
+3. **Declare container.** Solo / Co‑op; mundane / magical; public / private log.
+4. **Roll sequence (canonical):**
+ - Axis (d20)
+ - Vector (d12)
+ - Timeline (d10)
+ - Archetype (d8)
+ - Consequence (d6)
+ - Bias (d4)
+ - Optional Echo (percentile) _after_ reading if you want omens.
+5. **Log the seed.** Record raw numbers, table outputs, timestamp, and any Core sensations.
+6. **Interpret in layers:**
+ - **Layer 0:** Read tables literally.
+ - **Layer 1 (Narrative):** Write one sentence connecting Axis + Vector + Timeline.
+ - **Layer 2 (Archetype):** Speak as the Archetype giving counsel.
+ - **Layer 3 (Consequence/Bias):** Choose tactics respecting cost and bias.
+7. **Decision commit:** Define one actionable move within the rolled Timeline. Sign with the Lock‑In phrase if high‑stakes.
+8. **Post‑action snapshot:** After executing, log the outcome and any divergence. Mark “stable,” “needs iterate,” or “rollback.”
+
+### Scoring & Campaign Hooks (game layer)
+- **Momentum Track:** Each completed Decision Commit within the declared Timeline grants +1 Momentum. Spending 3 Momentum lets you reroll one die in a future spread or unlock a Quest Seed.
+- **Aegis Tokens:** Start each session with 3. Spend to veto a spread, to nullify a Consequence 6, or to activate the Lock‑In phrase without consuming an action. Earn 1 back when you log a truthful post-action snapshot.
+- **Labyrinth Depth (Co-op):** Each session cleared without a veto advances the Labyrinth depth by 1. At Depth 3/6/9, add a Labyrinth card (mini-challenge) that modifies the next spread.
+- **Publishing-friendly Scoring:** Each session yields a “Glyph rating” (1–5) based on number of completed actions vs. Consequence impacts. Use this for online leaderboards or seasonal events.
+
+## 4. Deterministic Variant (for digital parity)
+To mirror results between physical HALO and the web prototype:
+- Use the ISO timestamp (UTC, seconds) as the seed.
+- Concatenate with question text and hash (e.g., SHA‑256); convert to integer.
+- Map to dice using modulus: `axis = hash % 20 + 1`, etc.
+- This preserves reproducibility and lets multiple players sync rolls without trusting each other’s dice.
+
+## 5. Safety & Sovereignty Checks
+- **No unprotected amplification.** If dissociated or in unsafe space, stop after Ground–Center–Call Back.
+- **Veto power.** Root User can reject any spread that violates the Immutable Axioms; log veto events.
+- **De‑escalate.** If Consequence = 5 or 6 and you feel unstable, reroll Bias and ask for ally oversight.
+
+## 6. Co‑op / Labyrinth Mode
+For partnered decision‑making:
+- Each participant rolls Bias first; this sets their role (logic, intuition, ally, hybrid).
+- One shared Axis/Vector/Timeline/Archetype spread; separate Consequence per person.
+- Aggregate interpretations, then run a **Consensus Die** (d4):
+ - 1 = Follow logic vote
+ - 2 = Follow intuition vote
+ - 3 = Hybrid plan
+ - 4 = Pause; gather more data, reroll tomorrow
+- Log everything; if using the web app, encode the seed in the co‑op link.
+
+## 7. Micro‑Rituals to Bind the Rolls
+- **Axis call:** “Show me the corridor.”
+- **Vector call:** “Show me the motion.”
+- **Timeline call:** “Show me the horizon.”
+- **Archetype call:** “Who walks with me?”
+- **Consequence call:** “What’s the cost?”
+- **Bias call:** “How shall I weigh?”
+- **Echo call (optional):** “Any signals from the outer loops?”
+
+## 8. Troubleshooting / Debug
+- **Repeating Tower pulls:** Run the Adaptive Aegis; check for environmental destabilizers.
+- **Persistent Consequence 5–6:** You may be overclocking; enforce rest, then reroll after 12h.
+- **Flat readings:** If everything feels inert, roll only Bias + Vector and take a single micro‑action.
+
+## 9. Example Spread
+- Seed: 2024‑12‑10T20:00:00Z, Home Desk.
+- Question: “Where should I focus my next two weeks of creative energy?”
+- Rolls: Axis 7 (Body & Elemental), Vector 9 (Iterate), Timeline 3 (Short), Archetype 6 (The Star), Consequence 2 (Trade), Bias 4 (Hybrid), Echo 87% (none).
+- Read: Stabilize the vessel; prototype small cycles; accept an energy trade (sleep or social time) to keep hope alive. Blend intuition + metrics.
+
+## 10. Monetization & Publishing Plan
+- **SKUs:** Core box (above), Deluxe box (metal dice + foil board, MSRP $60), Print-and-Play PDF, and quarterly Table Packs that add new Axis/Vector/Archetype sets themed to seasons.
+- **Digital companion:** Free web roller synced via deterministic variant; optional $5 “Creator Mode” IAP for custom table sets and data export.
+- **Subscription hook:** “Labyrinth Season Pass” that delivers 3 digital Labyrinth cards/month + community leaderboard events using Glyph rating.
+- **Retail channel:** 2.5"x3.5" tuckbox for Quest Seeds to sit by POS; rulebook formatted for offset print (32pp, saddle-stitched). Target indie bookstores, metaphysical shops, and friendly local game stores.
+- **Margins:** Core box bill of materials under $8 at 2k print run; room for 4–5x keystone markup to retailers.
+
+## 11. Launch Roadmap
+1. **Prototype (weeks 1–2):** Produce print-and-play PDF; gather playtest logs. Push deterministic roller to web demo.
+2. **Playtest (weeks 3–6):** Ship 30 reviewer kits (laser-cut tokens, standard dice). Track Glyph ratings and Momentum usage to tune balance.
+3. **Preorder (week 7):** Open Shopify + Crowd preorders with two SKUs (Core/Deluxe). Offer Season Pass add-on and free PDF with purchase.
+4. **Manufacturing (weeks 8–16):** Lock art, run offset print; final QA on dice and dry-erase board durability.
+5. **Fulfillment (weeks 16–20):** Ship physicals; release Table Pack #1 (Cosmic / Night Market) as digital DLC.
+6. **Live Ops (ongoing):** Monthly Labyrinth challenges, streamer affiliate kits, and limited foil micro-expansions to keep collectability.
+
+## 12. Closing
+The HALO dice engine is a repeatable lens, not a jail. The Root User outranks every spread. Lock it in, publish boldly, and treat each roll as both a conversation with your Core and a game loop your players will want to revisit.
+
+## 13. Mobile Access (Phone Workflow)
+Make HALO usable from your phone in three layers:
+
+1) **Rulebook + Tables (offline PDF/HTML):**
+ - Save this manuscript as a PDF to your phone’s Files/Drive (Print to PDF in the browser if viewing online).
+ - Add the PDF/HTML to your home screen for one-tap access; cache ensures it works without signal.
+ - Use your phone’s search to jump to Axis/Vector/Timeline sections during play.
+
+2) **Dice + Logging:**
+ - Use any mobile dice roller app with custom die sizes (d20/d12/d10/d8/d6/d4/d00) to match the HALO loadout.
+ - Keep a note in your preferred app (Apple Notes/Google Keep/Obsidian/Mem.ai) with a template: timestamp, question, rolls, table outputs, decision commit, post-action snapshot.
+ - If playing co-op remotely, drop the ISO timestamp + question into the deterministic variant (Section 4) so everyone can sync rolls without sharing physical dice.
+
+3) **Sharing/Publishing:**
+ - Export your session log as PDF/Markdown and share to Discord/Substack/Notion as “HALO Session #N.”
+ - For live streams, screen-share the note/dice roller; for async play, post the seed + rolls so others can reconstruct the spread.
+ - When you launch the digital companion, wrap it as a PWA: add-to-home-screen gives a native-like icon, offline cache, and push hooks for Labyrinth challenges.
diff --git a/README.md b/README.md
index 8bb6723..81f2cad 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,34 @@
-# Collab_Reposit
-The place to discuss plans and share ideas for the game.
+# HALO Meta Labyrinth — Archetype oracle roguelike
+
+Mobile-first HALO rebuilt as a meta-divination card roguelike with a Wu Xing elemental matchup system, gacha economy, and optional WebSocket relay for room sync/chat. Everything is bundled for offline play or side-loading into the Android WebView shell.
+
+## Play it
+
+- **Phone sideload:** run `npm run build` and send `dist/index.html` to your phone. The bundle inlines `halo.js` so it runs offline.
+- **Home screen:** open `index.html` in mobile Safari/Chrome and “Add to Home Screen.” State stays on-device with an in-memory fallback if storage is blocked.
+- **Android APK (WebView shell):** build with Android Studio after running `./scripts/sync_android_assets.sh` to copy the latest `dist/index.html` into `android-app/app/src/main/assets/`.
+
+## Core loop
+
+1. Set pilot, quest, seed, difficulty, and mode from the main menu. Seeds are deterministic so squads can mirror runs.
+2. Build a 12-card deck from Wu Xing archetype cards. Elemental advantage follows the controlling cycle (Wood>Earth>Water>Fire>Metal>Wood).
+3. Enter a run: draw an oracle-driven encounter, play archetype cards from your hand, then resolve the beat. Momentum/Aegis/Doom govern survival; Depth tracks progress.
+4. Cash out to bank Credits/Embers/Shards or crash if Doom/Aegis fail. Credits buy Pulse packs; Embers/Shards feed Radiant/Axis pulls and VIP rarity boosts.
+
+## Multiplayer relay
+
+- Start the relay locally: `npm install` then `npm run server` (defaults to `ws://localhost:8787`).
+- In the client, toggle **Enable relay**, set the relay URL and room (defaults to the seed), and play. Depth pings and chat flow automatically once connected.
+
+## Build & test locally
+
+1. Install dependencies: `npm install`.
+2. Build the bundled client: `npm run build` (outputs `dist/index.html` and `dist/halo.js`).
+3. Open `dist/index.html` in a browser (desktop or mobile) to sanity-check the UI and run loop. Use the seed field to validate deterministic behavior across devices.
+4. (Optional multiplayer) Start the relay with `npm run server`, set the relay URL/room in the client, and confirm depth/chat sync with a second browser tab.
+
+The project does not ship automated tests; the build completes in a few seconds and acts as the smoke test. Relay and client run entirely offline aside from optional WebSocket connectivity.
+
+## Serverless API (DynamoDB) deployment note
+
+The Lambda handler in `halo_lambda/index.js` auto-corrects placeholder or malformed regions (e.g., `MY_AWS_REGION`, `LOCAL`, blank) to `us-east-1` so builds do not fail when a default value is left unchanged. For production, set `AWS_REGION` or `AWS_DEFAULT_REGION` to a valid AWS region pattern (for example, `us-west-2`, `us-gov-west-1`, or `cn-north-1`) before deploying the function.
diff --git a/android-app/app/build.gradle b/android-app/app/build.gradle
new file mode 100644
index 0000000..72eb145
--- /dev/null
+++ b/android-app/app/build.gradle
@@ -0,0 +1,44 @@
+plugins {
+ id 'com.android.application'
+}
+
+android {
+ namespace 'com.halo.pocketlabyrinth'
+ compileSdk 34
+
+ defaultConfig {
+ applicationId 'com.halo.pocketlabyrinth'
+ minSdk 26
+ targetSdk 34
+ versionCode 1
+ versionName '0.1.0'
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ debug {
+ applicationIdSuffix '.debug'
+ debuggable true
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_17
+ targetCompatibility JavaVersion.VERSION_17
+ }
+
+ packagingOptions {
+ resources {
+ excludes += ['/META-INF/{AL2.0,LGPL2.1}']
+ }
+ }
+}
+
+dependencies {
+ implementation 'androidx.appcompat:appcompat:1.7.0'
+ implementation 'com.google.android.material:material:1.12.0'
+ implementation 'androidx.webkit:webkit:1.10.0'
+}
diff --git a/android-app/app/proguard-rules.pro b/android-app/app/proguard-rules.pro
new file mode 100644
index 0000000..e69de29
diff --git a/android-app/app/src/main/AndroidManifest.xml b/android-app/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..3d08abc
--- /dev/null
+++ b/android-app/app/src/main/AndroidManifest.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android-app/app/src/main/assets/index.html b/android-app/app/src/main/assets/index.html
new file mode 100644
index 0000000..0dc30a4
--- /dev/null
+++ b/android-app/app/src/main/assets/index.html
@@ -0,0 +1,1019 @@
+
+
+
Define your pilot, quest, mode, and seed. Seeds stay deterministic so squads can mirror the same Labyrinth.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Economy & Paid features
+
Earn credits by delving, spend Embers on Radiant pulls or the VIP Blessing (rarity boost). Purchases are simulated for testing.
+
+
Credits–
+
Embers–
+
Shards–
+
Status–
+
+
+
+
+
+
+
+
+
Gacha & Collection
+
Open packs to expand your deck. Pity kicks in after 8 non-rare pulls. Radiant pulls guarantee at least one rare.
+
+
+
+
+
+
+
+
+
Deck Builder
+
Tap Add to move owned cards into your Adventure Deck. Max 12 cards; duplicates limited by ownership.
+
+
Adventure Deck
+
+
+
+
Collection
+
+
+
+
+
+
Run (Roguelike beats)
+
Draw an encounter, play cards from your hand, then resolve the beat. Depth and Doom define survival; Momentum fuels speed; Aegis is your shield.
+
+
Momentum–
+
Aegis–
+
Depth–
+
Doom–
+
+
+
+
+
+
+
Hand
+
+
Run Log
+
+
+
+
+
Online relay (optional)
+
Run your own websocket relay (`npm run server`) or point to a shared instance. Rooms are lightweight; share the seed/room to sync spread depth and chat.
+
+
+
+
+
+
Status: offline
+
+
+
+
+
+
+
+
Relay log
+
+
+
+
+
+
+
Phone-first
+
Save `dist/index.html` to your device or add this page to your home screen. All state is on-device; relay is opt-in.
Optional WebSocket relay for room sync and squad chat. Room defaults to your seed.
+
+
+
+
+
+
Peers:
+
+
+
+
+
+
Recent log
+
+
+
+
+
+
diff --git a/halo.js b/halo.js
index fd750bd..f8200e0 100644
--- a/halo.js
+++ b/halo.js
@@ -1,286 +1,804 @@
-// HALO Meta‑Oracle + Co‑op + Supporter JS
-// This script defines the core Labyrinth oracle logic and plugs in
-// optional supporter (Ko‑Fi) and cooperative session features.
-
-// === Meta‑Oracle Definitions ===
-// We define a handful of symbolic categories for the oracle. These lists
-// are deliberately compact but evocative; they can be expanded or
-// modified to suit your mythology. Each entry contains a name and a
-// brief tagline.
-const AXES = [
- { name: "Mind & Narrative", tagline: "Rewrite your story" },
- { name: "Domain & Magic", tagline: "Shape your reality" },
- { name: "Body & Elemental", tagline: "Honor the vessel" },
- { name: "Spirit & Communion", tagline: "Call your allies" },
- { name: "Fate & Unknown", tagline: "Embrace mystery" }
-];
+// HALO Meta-Divination Engine — rebuilt from scratch for the meta-game roguelike
+// Main menu + multiplayer relay + gacha + Wu Xing archetype combat + oracle-driven encounters
+
+const STORAGE_KEY = "halo_meta_state_v1";
+const LEGACY_KEYS = ["halo_mobile_state_v2", "halo_state", "halo_meta_arc_v0"];
+const ONLINE_DEFAULT = "ws://localhost:8787";
+let socket = null;
+let warnedStorage = false;
+let memoryFallback = null;
+
+// Wu Xing elemental wheel (using the controlling cycle for advantage)
+const ELEMENTS = {
+ wood: { strong: "earth", weak: "metal", color: "#4CAF50" },
+ fire: { strong: "metal", weak: "water", color: "#FF7043" },
+ earth: { strong: "water", weak: "wood", color: "#C0A46B" },
+ metal: { strong: "wood", weak: "fire", color: "#B0BEC5" },
+ water: { strong: "fire", weak: "earth", color: "#4FC3F7" }
+};
-const VECTORS = [
- { name: "Observe", tagline: "Watch and wait" },
- { name: "Release", tagline: "Let go and clear" },
- { name: "Transmute", tagline: "Change and evolve" },
- { name: "Illuminate", tagline: "Reveal and understand" },
- { name: "Manifest", tagline: "Bring it into being" }
+const DIVINATION_DICE = [
+ { name: "Tarot Major", sides: 22 },
+ { name: "I Ching", sides: 64 },
+ { name: "Runes", sides: 24 },
+ { name: "Astral Houses", sides: 12 },
+ { name: "Void Die", sides: 33 }
];
-const TIMELINES = [
- { name: "Now–1 year", tagline: "Immediate/short term" },
- { name: "1–3 years", tagline: "Short term" },
- { name: "3–7 years", tagline: "Medium term" },
- { name: "7–20 years", tagline: "Long term" }
+const DIVINATION_THEMES = [
+ "Initiation", "Challenge", "Reversal", "Breakthrough", "Union", "Fragment", "Signal", "Riddle",
+ "Gift", "Debt", "Memory", "Future Echo", "Threshold", "Labyrinth", "Guardian", "Bloom"
];
const ARCHETYPES = [
- { name: "The Weaver (18)", tagline: "Fates and patterns" },
- { name: "The Gatekeeper (4)", tagline: "Thresholds and choices" },
- { name: "The Fool (0)", tagline: "Beginner's mind" },
- { name: "The Magician (1)", tagline: "Will and manifestation" },
- { name: "The Empress (3)", tagline: "Fertility and nurture" },
- { name: "The Hermit (9)", tagline: "Inner search" },
- { name: "The Tower (16)", tagline: "Sudden change" },
- { name: "The Star (17)", tagline: "Hope and renewal" },
- { name: "The Sun (19)", tagline: "Clarity and vitality" }
+ {
+ id: "sovereign",
+ name: "Sovereign Ember",
+ rarity: "common",
+ element: "fire",
+ text: "+2 Momentum. If advantaged, burn 1 Doom.",
+ effect: { momentum: 2, doom: -1 }
+ },
+ {
+ id: "warden",
+ name: "Warden of Roots",
+ rarity: "common",
+ element: "wood",
+ text: "Restore 1 Aegis. If advantaged, draw 1 archetype.",
+ effect: { aegis: 1, draw: 1 }
+ },
+ {
+ id: "seeker",
+ name: "Seeker of Currents",
+ rarity: "common",
+ element: "water",
+ text: "+1 Momentum, reveal omen. If advantaged, convert omen to boon.",
+ effect: { momentum: 1, boon: true }
+ },
+ {
+ id: "architect",
+ name: "Architect of Stone",
+ rarity: "rare",
+ element: "earth",
+ text: "Stabilize: set Doom to 0 if advantaged; otherwise -1 Doom.",
+ effect: { doom: -1, stabilize: true }
+ },
+ {
+ id: "mirror",
+ name: "Mirror of Blades",
+ rarity: "rare",
+ element: "metal",
+ text: "Reflect threat. If advantaged, gain 1 Momentum and 1 Aegis.",
+ effect: { reflect: true, momentum: 1, aegis: 1 }
+ },
+ {
+ id: "oracle",
+ name: "Prismatic Oracle",
+ rarity: "rare",
+ element: "water",
+ text: "Roll two oracle dice, pick best. Gain +1 Momentum per hit.",
+ effect: { doubleRoll: true }
+ },
+ {
+ id: "phoenix",
+ name: "Phoenix Crown",
+ rarity: "mythic",
+ element: "fire",
+ text: "Heal to 3 Aegis, +2 Momentum. Advantage adds bonus credit.",
+ effect: { aegis: 3, momentum: 2, bonus: true }
+ },
+ {
+ id: "river",
+ name: "River Between Worlds",
+ rarity: "mythic",
+ element: "water",
+ text: "Summon ally: set Momentum to 3, draw 2, reveal omen.",
+ effect: { momentumSet: 3, draw: 2, boon: true }
+ },
+ {
+ id: "forge",
+ name: "Starforge Anvil",
+ rarity: "mythic",
+ element: "metal",
+ text: "Cash any Momentum into Credits x10, then set Momentum to 1.",
+ effect: { cashMomentum: true }
+ },
+ {
+ id: "labyrinth",
+ name: "Labyrinth Keeper",
+ rarity: "rare",
+ element: "earth",
+ text: "Mark the path: reduce Depth cost by 1 this beat; draw 1.",
+ effect: { depthShield: true, draw: 1 }
+ }
];
-// Storage keys for localStorage. These can be namespaced to avoid
-// colliding with other apps on the same domain.
-const PROFILE_KEY = "halo_profile";
-const HISTORY_KEY = "halo_history";
+const RARITY_WEIGHTS = { common: 68, rare: 25, mythic: 7 };
+const PACKS = {
+ pulse: { name: "Pulse Pack", size: 3, cost: { credits: 250 } },
+ radiant: { name: "Radiant Cache", size: 5, cost: { embers: 80 }, bonusRare: true },
+ legend: { name: "Axis Vault", size: 1, cost: { shards: 1 }, guaranteed: "mythic" }
+};
+
+function notify(message) {
+ const el = document.getElementById("notice");
+ if (!el) return;
+ if (!message) {
+ el.textContent = "";
+ el.classList.remove("show");
+ return;
+ }
+ el.textContent = message;
+ el.classList.add("show");
+}
+
+function hashSeed(str) {
+ let hash = 0;
+ for (let i = 0; i < str.length; i++) {
+ hash = (hash << 5) - hash + str.charCodeAt(i);
+ hash |= 0;
+ }
+ return Math.abs(hash) + 1;
+}
+
+function random(state) {
+ const raw = Math.sin(state.seedHash + state.cursor) * 10000;
+ state.cursor += 1;
+ return raw - Math.floor(raw);
+}
+
+function pick(list, state) {
+ const idx = Math.floor(random(state) * list.length);
+ return list[idx];
+}
+
+function shuffle(list, state) {
+ const arr = [...list];
+ for (let i = arr.length - 1; i > 0; i--) {
+ const j = Math.floor(random(state) * (i + 1));
+ [arr[i], arr[j]] = [arr[j], arr[i]];
+ }
+ return arr;
+}
-// Utility: Roll a die with a given number of sides.
-function roll(sides) {
- return Math.floor(Math.random() * sides);
+function generateSeed() {
+ const chars = "ABCDEFGHJKMNPQRSTUVWXYZ23456789";
+ let out = "HALO-";
+ for (let i = 0; i < 6; i++) out += chars[Math.floor(Math.random() * chars.length)];
+ return out;
}
-// Create a new reading based off the defined lists. A reading bundles
-// four random selections along with the user's question and a timestamp.
-function createReading(question) {
- const axis = AXES[roll(AXES.length)];
- const vector = VECTORS[roll(VECTORS.length)];
- const timeline = TIMELINES[roll(TIMELINES.length)];
- const archetype = ARCHETYPES[roll(ARCHETYPES.length)];
+function starterCollection() {
+ const inventory = {};
+ ARCHETYPES.forEach((c) => {
+ if (c.rarity === "common") inventory[c.id] = 2;
+ if (c.rarity === "rare") inventory[c.id] = 1;
+ });
+ return inventory;
+}
+
+function baseState(overrides = {}) {
+ const seed = overrides.seed || generateSeed();
+ const seedHash = hashSeed(seed);
return {
- axis,
- vector,
- timeline,
- archetype,
- question: question || "",
- timestamp: new Date().toISOString()
+ profile: {
+ pilot: overrides.pilot || "",
+ title: "Axis Runner",
+ quest: overrides.quest || "",
+ seed,
+ difficulty: overrides.difficulty || "standard",
+ mode: overrides.mode || "solo"
+ },
+ seedHash,
+ cursor: 1,
+ currencies: { credits: 1200, embers: 220, shards: 1 },
+ vip: false,
+ pity: 0,
+ collection: starterCollection(),
+ deck: ["sovereign", "warden", "seeker", "architect", "mirror", "labyrinth"],
+ relics: [],
+ run: null,
+ online: {
+ enabled: false,
+ url: overrides.url || ONLINE_DEFAULT,
+ room: "",
+ status: "offline",
+ peers: [],
+ log: []
+ },
+ codex: [],
+ gachaLog: [],
+ log: []
};
}
-// Load the saved profile from localStorage and populate the form fields.
-function loadProfile() {
+function migrateLegacy() {
try {
- const raw = localStorage.getItem(PROFILE_KEY);
- if (!raw) return;
- const profile = JSON.parse(raw);
- const nameInput = document.getElementById("profile-name");
- const sunInput = document.getElementById("profile-sun");
- const moonInput = document.getElementById("profile-moon");
- const risingInput = document.getElementById("profile-rising");
- if (nameInput) nameInput.value = profile.name || "";
- if (sunInput) sunInput.value = profile.sun || "";
- if (moonInput) moonInput.value = profile.moon || "";
- if (risingInput) risingInput.value = profile.rising || "";
+ for (const key of LEGACY_KEYS) {
+ const raw = localStorage.getItem(key);
+ if (raw) {
+ const parsed = JSON.parse(raw);
+ memoryFallback = parsed;
+ return parsed;
+ }
+ }
} catch (err) {
- console.warn("Failed to load profile", err);
+ console.warn("Legacy migration failed", err);
}
+ return null;
}
-// Save the profile to localStorage. This persists only on the client.
-function saveProfile() {
- const name = document.getElementById("profile-name").value.trim();
- const sun = document.getElementById("profile-sun").value.trim();
- const moon = document.getElementById("profile-moon").value.trim();
- const rising = document.getElementById("profile-rising").value.trim();
- const profile = { name, sun, moon, rising };
- localStorage.setItem(PROFILE_KEY, JSON.stringify(profile));
+function clamp(num, min, max) {
+ return Math.max(min, Math.min(max, num));
}
-// Load reading history from storage. Returns an array (possibly empty).
-function loadHistory() {
+function saveState(state) {
try {
- const raw = localStorage.getItem(HISTORY_KEY);
- return raw ? JSON.parse(raw) : [];
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
+ memoryFallback = state;
} catch (err) {
- console.warn("Failed to load history", err);
- return [];
+ memoryFallback = state;
+ if (!warnedStorage) {
+ console.warn("Local storage unavailable; keeping state in memory only", err);
+ notify("Local storage is blocked. Progress will reset if you close this tab.");
+ warnedStorage = true;
+ }
}
}
-// Save reading history back to storage.
-function saveHistory(history) {
- localStorage.setItem(HISTORY_KEY, JSON.stringify(history));
+function loadState() {
+ try {
+ const raw = localStorage.getItem(STORAGE_KEY);
+ if (raw) return JSON.parse(raw);
+ } catch (err) {
+ console.warn("Failed to load state", err);
+ if (memoryFallback) return memoryFallback;
+ }
+ const legacy = migrateLegacy();
+ if (legacy) return { ...baseState(), ...legacy };
+ return memoryFallback || baseState();
}
-// Render the current reading into the DOM. If no reading is provided,
-// clears the current display.
-function renderCurrent(reading) {
- const container = document.getElementById("current-reading");
- if (!container) return;
- if (!reading) {
- container.innerHTML = "";
- return;
+function addLog(state, entry) {
+ state.log.push({ ...entry, timestamp: Date.now() });
+ state.log = state.log.slice(-120);
+}
+
+function rarityRoll(state) {
+ const bonus = state.vip ? 5 : 0;
+ const roll = random(state) * (100 + bonus);
+ const mythicCut = RARITY_WEIGHTS.mythic + bonus;
+ if (roll >= 100 - mythicCut) return "mythic";
+ if (roll >= 100 - RARITY_WEIGHTS.rare) return "rare";
+ return "common";
+}
+
+function randomCardByRarity(rarity, state) {
+ const pool = ARCHETYPES.filter((c) => c.rarity === rarity);
+ return pick(pool, state);
+}
+
+function addToCollection(state, cardId) {
+ state.collection[cardId] = (state.collection[cardId] || 0) + 1;
+}
+
+function pullPack(state, packKey) {
+ const pack = PACKS[packKey];
+ if (!pack) return { results: [], reason: "Missing pack" };
+ const cost = pack.cost;
+ if (cost.credits && state.currencies.credits < cost.credits)
+ return { results: [], reason: "Not enough credits" };
+ if (cost.embers && state.currencies.embers < cost.embers)
+ return { results: [], reason: "Not enough embers" };
+ if (cost.shards && state.currencies.shards < cost.shards)
+ return { results: [], reason: "Not enough shards" };
+
+ if (cost.credits) state.currencies.credits -= cost.credits;
+ if (cost.embers) state.currencies.embers -= cost.embers;
+ if (cost.shards) state.currencies.shards -= cost.shards;
+
+ const results = [];
+ for (let i = 0; i < pack.size; i++) {
+ let rarity = pack.guaranteed || rarityRoll(state);
+ if (state.pity >= 8) rarity = "rare";
+ if (pack.bonusRare && i === pack.size - 1 && rarity === "common") rarity = "rare";
+ const card = randomCardByRarity(rarity, state);
+ addToCollection(state, card.id);
+ results.push(card);
+ state.pity = rarity === "rare" || rarity === "mythic" ? 0 : state.pity + 1;
}
- // Format the timestamp into a human‑readable string.
- const ts = new Date(reading.timestamp);
- const tsStr = ts.toLocaleString();
- container.innerHTML = `
-
-
When:${tsStr}
-
Axis: ${reading.axis.name}
-
Vector: ${reading.vector.name}
-
Timeline: ${reading.timeline.name}
-
Archetype: ${reading.archetype.name}
- ${reading.question ? `
Q: ${reading.question}
` : ""}
-
- `;
+
+ state.gachaLog.push({ pack: pack.name, results, timestamp: Date.now() });
+ return { results };
}
-// Render the history table. Each entry shows the timestamp and top‑level
-// categories. You could expand this to include question or notes.
-function renderHistory(history) {
- const container = document.getElementById("history");
- if (!container) return;
- if (!history || history.length === 0) {
- container.innerHTML = "
No past readings yet.
";
- return;
+function ensureDeckLegal(state) {
+ const cleaned = state.deck.filter((id) => state.collection[id]);
+ if (!cleaned.length) cleaned.push("sovereign", "warden", "seeker");
+ state.deck = cleaned.slice(0, 12);
+}
+
+function elementAdvantage(cardElement, encounterElement) {
+ if (!ELEMENTS[cardElement] || !ELEMENTS[encounterElement]) return 0;
+ if (ELEMENTS[cardElement].strong === encounterElement) return 1;
+ if (ELEMENTS[cardElement].weak === encounterElement) return -1;
+ return 0;
+}
+
+function rollOracleDie(state) {
+ const die = pick(DIVINATION_DICE, state);
+ const roll = Math.floor(random(state) * die.sides) + 1;
+ return { die: die.name, sides: die.sides, roll };
+}
+
+function nextEncounter(state) {
+ const omen = pick(DIVINATION_THEMES, state);
+ const elementKeys = Object.keys(ELEMENTS);
+ return {
+ element: pick(elementKeys, state),
+ theme: omen,
+ dice: rollOracleDie(state),
+ boon: random(state) > 0.55,
+ threat: random(state) > 0.45,
+ lore: pick(
+ [
+ "Echoes of past selves ask for alignment.",
+ "Two timelines braid together and demand a choice.",
+ "A silent guardian flips an unseen coin.",
+ "The Labyrinth grows new corridors in real time.",
+ "Archetype avatars convene at the Inner Court.",
+ "A future self hands you a mirrored key.",
+ "A rival faction offers a pact of convenience."
+ ],
+ state
+ )
+ };
+}
+
+function drawCards(run, state, count = 1) {
+ for (let i = 0; i < count; i++) {
+ if (!run.drawPile.length) {
+ run.drawPile = shuffle(run.discard, state);
+ run.discard = [];
+ }
+ if (!run.drawPile.length) break;
+ run.hand.push(run.drawPile.shift());
}
- // Build a simple HTML table.
- let html = "
When
Axis
Vector
Timeline
Archetype
";
- history.forEach((r) => {
- const ts = new Date(r.timestamp).toLocaleString();
- html += `
";
- container.innerHTML = html;
+ state.run = null;
}
-// Main initialization: wire up event handlers and load any saved data.
-function init() {
- // Load profile and history on page load.
- loadProfile();
- let history = loadHistory();
- renderHistory(history);
-
- // Profile save button.
- const saveBtn = document.getElementById("save-profile");
- if (saveBtn) {
- saveBtn.addEventListener("click", () => {
- saveProfile();
- alert("Profile saved locally");
- });
+function doomTick(run) {
+ let inc = 0;
+ if (run.momentum < 0) inc += 1;
+ if (run.aegis <= 0) inc += 1;
+ return inc;
+}
+
+function resolveCard(state, run, card) {
+ const encounter = run.current;
+ const advantage = elementAdvantage(card.element, encounter.element);
+ let bonusCredits = 0;
+
+ if (card.effect.momentum) run.momentum = clamp(run.momentum + card.effect.momentum + (advantage > 0 ? 1 : 0), -3, 9);
+ if (card.effect.aegis)
+ run.aegis = clamp(run.aegis + card.effect.aegis + (advantage > 0 ? 1 : 0), 0, 6);
+ if (card.effect.doom) run.doom = clamp(run.doom + card.effect.doom - (advantage > 0 ? 1 : 0), 0, 8);
+ if (card.effect.draw) drawCards(run, state, card.effect.draw);
+ if (card.effect.boon) run.lastOmen = encounter;
+ if (card.effect.reflect && encounter.threat) {
+ encounter.threat = false;
+ encounter.boon = true;
+ }
+ if (card.effect.stabilize && advantage > 0) run.doom = 0;
+ if (card.effect.doubleRoll) {
+ const first = rollOracleDie(state);
+ const second = rollOracleDie(state);
+ const winner = first.roll >= second.roll ? first : second;
+ run.lastOmen = { ...encounter, dice: winner };
+ run.momentum = clamp(run.momentum + 1 + (winner.roll > winner.sides / 2 ? 1 : 0), -3, 9);
}
- // Roll button: generate a reading and update the UI and history.
- const rollBtn = document.getElementById("roll-btn");
- if (rollBtn) {
- rollBtn.addEventListener("click", () => {
- const question = document.getElementById("question").value.trim();
- const reading = createReading(question);
- // Unshift reading (add to beginning) to show latest first.
- history.unshift(reading);
- saveHistory(history);
- renderCurrent(reading);
- renderHistory(history);
- });
+ if (card.effect.momentumSet !== undefined) run.momentum = card.effect.momentumSet;
+ if (card.effect.bonus) bonusCredits += 50;
+ if (card.effect.cashMomentum) {
+ bonusCredits += run.momentum * 10;
+ run.momentum = 1;
}
+ if (card.effect.depthShield) run.route.push({ depth: run.depth, note: "Marked" });
+
+ if (advantage < 0) run.doom = clamp(run.doom + 1, 0, 8);
+ state.currencies.credits += bonusCredits;
}
-// === Supporter and Co‑op Features ===
+function resolveEncounter(state) {
+ const run = state.run;
+ if (!run) return;
-// Supporter functionality: allow users to tip via Ko‑Fi and unlock a badge.
-function setupSupport() {
- const supportButton = document.getElementById("support-button");
- const supportBadge = document.getElementById("support-badge");
- if (!supportButton || !supportBadge) {
- return;
+ // Threat/Boon gates
+ if (run.current.threat && run.doom >= 6) {
+ run.aegis -= 1;
+ addLog(state, { type: "threat", title: "Threat overloaded", body: "Aegis cracked under pressure" });
}
- // Display supporter badge if previously activated.
- const isSupporter = localStorage.getItem("halo_supporter") === "true";
- if (isSupporter) {
- supportBadge.style.display = "inline-flex";
+ if (run.current.boon && run.momentum >= 4) {
+ state.currencies.credits += 20;
+ state.currencies.embers += 2;
}
- supportButton.addEventListener("click", () => {
- // Open Ko‑Fi in a new tab.
- window.open("https://ko-fi.com/oneinfinity", "_blank", "noopener");
- // Ask the user if they actually supported. If yes, set supporter flag and show badge.
- const opted = confirm(
- "Thank you for considering support! If you just tipped on Ko‑Fi, click OK to enable supporter mode."
- );
- if (opted) {
- localStorage.setItem("halo_supporter", "true");
- supportBadge.style.display = "inline-flex";
+
+ run.depth += 1;
+ run.discard.push(...run.hand);
+ run.hand = [];
+ drawCards(run, state, 4);
+ run.current = nextEncounter(state);
+
+ run.doom = clamp(run.doom + doomTick(run), 0, 8);
+ if (run.aegis <= 0 || run.doom >= 8) {
+ run.status = "crashed";
+ endRun(state, "crashed");
+ }
+}
+
+function playCard(cardId) {
+ const state = window.haloState;
+ if (!state.run) return;
+ const idx = state.run.hand.findIndex((c) => c === cardId);
+ if (idx === -1) return;
+ const card = ARCHETYPES.find((c) => c.id === cardId);
+ if (!card) return;
+ state.run.hand.splice(idx, 1);
+ resolveCard(state, state.run, card);
+ state.run.discard.push(cardId);
+ addLog(state, { type: "card", title: card.name, body: card.text, meta: `vs ${state.run.current.element}` });
+ saveState(state);
+ render();
+}
+
+function resolveBeat() {
+ const state = window.haloState;
+ if (!state.run) return;
+ resolveEncounter(state);
+ saveState(state);
+ render();
+}
+
+function cashOut() {
+ const state = window.haloState;
+ if (!state.run) return;
+ endRun(state, "banked");
+ saveState(state);
+ render();
+}
+
+function addCardToDeck(cardId) {
+ const state = window.haloState;
+ if (!state.collection[cardId]) return;
+ if (state.deck.length >= 12) return;
+ state.deck.push(cardId);
+ saveState(state);
+ render();
+}
+
+function removeCardFromDeck(cardId) {
+ const state = window.haloState;
+ const idx = state.deck.indexOf(cardId);
+ if (idx > -1) state.deck.splice(idx, 1);
+ saveState(state);
+ render();
+}
+
+function toggleVIP() {
+ const state = window.haloState;
+ state.vip = !state.vip;
+ saveState(state);
+ render();
+}
+
+function connectRelay() {
+ const state = window.haloState;
+ if (!state.online.enabled) return;
+ if (socket) socket.close();
+ socket = new WebSocket(state.online.url);
+ socket.onopen = () => {
+ state.online.status = "online";
+ state.online.log.push({ sender: "system", message: "Connected" });
+ sendRelay({ type: "join", room: state.online.room || state.profile.seed, seed: state.profile.seed });
+ render();
+ };
+ socket.onmessage = (ev) => {
+ try {
+ const msg = JSON.parse(ev.data);
+ if (msg.type === "chat") state.online.log.push({ sender: msg.from, message: msg.text });
+ if (msg.type === "sync") {
+ state.online.peers = msg.peers || [];
+ if (state.run) state.run.depth = Math.max(state.run.depth, msg.depth || 0);
+ }
+ render();
+ } catch (err) {
+ console.error("Relay parse", err);
}
+ };
+ socket.onclose = () => {
+ state.online.status = "offline";
+ render();
+ };
+}
+
+function sendRelay(payload) {
+ const state = window.haloState;
+ if (!socket || socket.readyState !== 1) return;
+ const packet = { room: state.online.room || state.profile.seed, ...payload };
+ socket.send(JSON.stringify(packet));
+}
+
+function sendChat(text) {
+ const state = window.haloState;
+ if (!state.online.enabled || !text) return;
+ sendRelay({ type: "chat", from: state.profile.pilot || "anon", text });
+ state.online.log.push({ sender: state.profile.pilot || "me", message: text });
+ render();
+}
+
+function syncDepth() {
+ const state = window.haloState;
+ if (!state.run) return;
+ sendRelay({ type: "sync", depth: state.run.depth, seed: state.profile.seed });
+}
+
+function renderDeck() {
+ const deckEl = document.getElementById("deck-list");
+ const poolEl = document.getElementById("collection-list");
+ const state = window.haloState;
+ if (!deckEl || !poolEl) return;
+ deckEl.innerHTML = "";
+ poolEl.innerHTML = "";
+
+ state.deck.forEach((id) => {
+ const card = ARCHETYPES.find((c) => c.id === id);
+ if (!card) return;
+ const li = document.createElement("li");
+ li.textContent = `${card.name} (${card.element})`;
+ li.onclick = () => removeCardFromDeck(id);
+ deckEl.appendChild(li);
+ });
+
+ ARCHETYPES.forEach((card) => {
+ const owned = state.collection[card.id] || 0;
+ const li = document.createElement("li");
+ li.textContent = `${card.name} [${card.rarity}] (${owned} owned)`;
+ li.style.color = ELEMENTS[card.element].color;
+ li.onclick = () => addCardToDeck(card.id);
+ poolEl.appendChild(li);
});
}
-// Co‑op functionality: generate shareable session codes and handle incoming sessions.
-function setupCoop() {
- const startBtn = document.getElementById("start-coop");
- const copyBtn = document.getElementById("copy-coop");
- const linkInput = document.getElementById("coop-link");
- if (!startBtn || !copyBtn || !linkInput) {
+function renderRun() {
+ const state = window.haloState;
+ const runPanel = document.getElementById("run-panel");
+ if (!runPanel) return;
+ if (!state.run) {
+ runPanel.innerHTML = "
No active run. Start from the main menu.
";
return;
}
- // Create a co‑op session link with encoded payload when the user clicks the button.
- startBtn.addEventListener("click", () => {
- // Build a simple payload. You could include the current reading seed or
- // other game state here. Using Date.now() as a nonce ensures each
- // session link is unique. You can later decode this and recreate
- // identical outcomes if your game is deterministic.
- const payload = {
- version: 1,
- timestamp: Date.now(),
- seed: Math.floor(Math.random() * 1e9)
- };
- const json = JSON.stringify(payload);
- const encoded = btoa(encodeURIComponent(json));
- const url = new URL(window.location.href);
- url.searchParams.set("coop", encoded);
- linkInput.value = url.toString();
- copyBtn.style.display = "inline-block";
+
+ const encounter = state.run.current;
+ const handButtons = state.run.hand
+ .map((id) => {
+ const card = ARCHETYPES.find((c) => c.id === id);
+ return ``;
+ })
+ .join("");
+
+ runPanel.innerHTML = `
+