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..4f89f5c 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,46 @@
-# Collab_Reposit
-The place to discuss plans and share ideas for the game.
+# HALO Pocket Labyrinth — Online Card Roguelike
+
+Mobile-first HALO built out as a roguelike card crawler with gacha, paid VIP boost, and an optional WebSocket relay for multiplayer sync and chat.
+
+## Play it
+
+* **Phone sideload:** run `npm run build` and send `dist/index.html` to your device (AirDrop, email, cloud drive, or a local file server). The build inlines `halo.js` so it runs offline without extra assets.
+* **Home screen:** open `index.html` in mobile Safari/Chrome and add it to your home screen for a lightweight PWA shell. State is on-device (localStorage), with an in-memory fallback if storage is blocked.
+
+## Android APK (WebView shell)
+
+1. Run `npm run build` to refresh `dist/index.html`.
+2. Sync the HTML into the Android project: `./scripts/sync_android_assets.sh`.
+3. Open the `android-app` folder in Android Studio (Giraffe+), let it download the Android Gradle Plugin, and build **app → assembleDebug**.
+4. Install `app-debug.apk` on your device. The shell runs offline and keeps relay/WebSocket support when you point it at your server.
+
+## Core loop
+
+1. Define pilot, quest, seed, difficulty, and mode. Seeds are deterministic—share them so squads can mirror the same Labyrinth.
+2. Build a 12-card Adventure Deck from your owned collection.
+3. Enter a run, draw encounters, play cards from your hand, then tap **Resolve Beat**. Momentum/Aegis/Depth/Doom drive survival; rewards convert to Credits.
+4. Cash out to bank rewards or crash when Doom/Aegis fail. Credits buy Pulse packs; Embers buy Radiant pulls and the VIP Blessing (rarity boost). Purchases are simulated for testing only.
+
+## Online relay
+
+An optional, ultra-light relay server lets multiple pilots sync depth/seed metadata and chat while playing the same run.
+
+* 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 (use your seed or a custom code), and hit **Start / Resume Run**. Sync + chat messages flow automatically once connected.
+
+## Files
+
+* `index.html` – mobile UI shell.
+* `halo.js` – HALO oracle, roguelike loop, gacha/deck logic, and relay client.
+* `scripts/build.js` – inlines `halo.js` into `dist/index.html`.
+* `scripts/server.js` – minimal WebSocket relay for multiplayer metadata/chat.
+
+All content stays in this repo for easy sideloading. No external CDNs or assets are required.
+
+## Serverless API (DynamoDB) deployment note
+
+The Lambda handler in `halo_lambda/index.js` now auto-corrects placeholder
+regions (e.g., `MY_AWS_REGION`) 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 your real AWS region (for example, `us-west-2`) 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.
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.
+
+
+
+
+
+
+
+
diff --git a/halo.js b/halo.js
index fd750bd..c592d70 100644
--- a/halo.js
+++ b/halo.js
@@ -1,12 +1,11 @@
-// 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.
+// HALO Pocket Labyrinth: Online roguelike card crawler with gacha and relay-ready co-op
+
+const STORAGE_KEY = "halo_mobile_state_v2";
+const ONLINE_DEFAULT = "ws://localhost:8787";
+let socket = null;
+let warnedStorage = false;
+let memoryFallback = null;
+
const AXES = [
{ name: "Mind & Narrative", tagline: "Rewrite your story" },
{ name: "Domain & Magic", tagline: "Shape your reality" },
@@ -30,257 +29,783 @@ const TIMELINES = [
{ name: "7–20 years", tagline: "Long term" }
];
-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" }
+const RARITY_WEIGHTS = {
+ common: 70,
+ rare: 25,
+ mythic: 5
+};
+
+const PACKS = {
+ starter: { name: "Pulse Pack", size: 3, cost: { credits: 250 } },
+ radiant: { name: "Radiant Pull", size: 5, cost: { embers: 80 }, bonusRare: true }
+};
+
+const CARD_POOL = [
+ {
+ id: "rush",
+ name: "Momentum Rush",
+ rarity: "common",
+ axis: "Mind",
+ text: "+1 Momentum. Draw 1."
+ },
+ {
+ id: "ward",
+ name: "Aegis Ward",
+ rarity: "common",
+ axis: "Body",
+ text: "Restore 1 Aegis. If a threat is present, prevent 1 Doom."
+ },
+ {
+ id: "spark",
+ name: "Prismatic Spark",
+ rarity: "common",
+ axis: "Spirit",
+ text: "Gain 1 Momentum and reveal the boon on this beat if any."
+ },
+ {
+ id: "mirror",
+ name: "Mirror Veil",
+ rarity: "rare",
+ axis: "Mind",
+ text: "If a threat exists, turn it into a boon. Otherwise +1 Aegis."
+ },
+ {
+ id: "gate",
+ name: "Gatekeeper's Key",
+ rarity: "rare",
+ axis: "Domain",
+ text: "Reduce Doom by 1 and bank current Momentum as Credits."
+ },
+ {
+ id: "star",
+ name: "Starfall Surge",
+ rarity: "rare",
+ axis: "Fate",
+ text: "+2 Momentum, then lose 1 Aegis."
+ },
+ {
+ id: "tower",
+ name: "Tower Break",
+ rarity: "rare",
+ axis: "Fate",
+ text: "Clear hand, draw 3 fresh cards, Doom cannot increase this beat."
+ },
+ {
+ id: "time",
+ name: "Time Lattice",
+ rarity: "mythic",
+ axis: "Spirit",
+ text: "Set Doom to 0 or Depth-1 (whichever is lower). Gain +1 Aegis."
+ },
+ {
+ id: "empress",
+ name: "Empress Bloom",
+ rarity: "mythic",
+ axis: "Body",
+ text: "Heal to 3 Aegis, add +2 Momentum, then bank 1 Ember."
+ },
+ {
+ id: "magus",
+ name: "Magus Rewrite",
+ rarity: "mythic",
+ axis: "Mind",
+ text: "Replay the last boon you saw and draw 2 cards."
+ }
];
-// 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";
+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");
+}
-// Utility: Roll a die with a given number of sides.
-function roll(sides) {
- return Math.floor(Math.random() * sides);
+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;
}
-// 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 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;
+}
+
+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;
+}
+
+function starterCollection() {
+ const inventory = {};
+ CARD_POOL.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()
+ player: overrides.player || "",
+ quest: overrides.quest || "",
+ mode: overrides.mode || "solo",
+ difficulty: overrides.difficulty || "standard",
+ seed,
+ seedHash,
+ cursor: 1,
+ currencies: { credits: 800, embers: 160, shards: 0 },
+ pity: 0,
+ vip: false,
+ collection: starterCollection(),
+ deck: ["rush", "rush", "ward", "spark", "mirror", "gate", "star", "tower"],
+ run: null,
+ online: {
+ enabled: false,
+ url: overrides.url || ONLINE_DEFAULT,
+ room: "",
+ status: "offline",
+ peers: [],
+ log: []
+ },
+ gachaLog: [],
+ log: []
};
}
-// Load the saved profile from localStorage and populate the form fields.
-function loadProfile() {
+function clamp(num, min, max) {
+ return Math.max(min, Math.min(max, num));
+}
+
+function saveState(state) {
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 || "";
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
+ memoryFallback = state;
} catch (err) {
- console.warn("Failed to load profile", err);
+ 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 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));
-}
-
-// Load reading history from storage. Returns an array (possibly empty).
-function loadHistory() {
+function loadState() {
try {
- const raw = localStorage.getItem(HISTORY_KEY);
- return raw ? JSON.parse(raw) : [];
+ const raw = localStorage.getItem(STORAGE_KEY);
+ if (raw) return JSON.parse(raw);
} catch (err) {
- console.warn("Failed to load history", err);
- return [];
+ if (!warnedStorage) {
+ console.warn("Failed to load state", err);
+ notify("Cannot access local storage. Using in-memory state for this session.");
+ warnedStorage = true;
+ }
+ if (memoryFallback) return memoryFallback;
}
+ return memoryFallback;
}
-// Save reading history back to storage.
-function saveHistory(history) {
- localStorage.setItem(HISTORY_KEY, JSON.stringify(history));
+function addLog(state, entry) {
+ state.log.push({ ...entry, timestamp: Date.now() });
+ state.log = state.log.slice(-80);
}
-// 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 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 = CARD_POOL.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.credits) state.currencies.credits -= cost.credits;
+ if (cost.embers) state.currencies.embers -= cost.embers;
+
+ const results = [];
+ for (let i = 0; i < pack.size; i++) {
+ let rarity = rarityRoll(state);
+ if (state.pity >= 8) rarity = "rare";
+ if (pack.bonusRare && i === pack.size - 1) rarity = rarity === "common" ? "rare" : rarity;
+ 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}
` : ""}
-
- `;
-}
-
-// 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;
+
+ state.gachaLog.push({
+ pack: pack.name,
+ results,
+ timestamp: Date.now()
+ });
+
+ return { results };
+}
+
+function ensureDeckLegal(state) {
+ const cleaned = state.deck.filter((id) => state.collection[id]);
+ if (!cleaned.length) cleaned.push("rush", "rush", "ward", "spark");
+ state.deck = cleaned.slice(0, 12);
+}
+
+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 += `
';
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";
+ results.innerHTML = `
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
+
-
- Stored only in this browser using localStorage.
-
-
-
-
2. Ask HALO
-
-
-
-
-
-
-
-
3. Past Readings (this device)
-
- Local Spiritual Blockchain prototype – readings are chained with simple hashes.
-
-
-
-
-
-
-
4. Co‑op Session
-
Share your Labyrinth session with a friend.
-
-
-
-
+
+
+
+
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.