diff --git a/.gitignore b/.gitignore index ff241fa..5b7e3ea 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,16 @@ *.log node_modules/ .claude/ + +# Local-only creative/IP source material. Keep the public repo focused on the +# playable prototype unless these are intentionally cleared for release. +The Network Mythos - A Myth Bible.md +The Network Mythos - Canon Bible.md +ALLEY_CAT_GAMES_SUBMISSION_CONCEPT.md +CASE_FILES_GAME_PLAN.md +concept-images/ +research for games/ +The Residue/ +the_residue_cc version agent/ +network-mythos-case-box/ +network-mythos-hybrid-table/ diff --git a/Case Box 001 - The Creator Who Left But Kept Posting/README.md b/Case Box 001 - The Creator Who Left But Kept Posting/README.md new file mode 100644 index 0000000..e4ff90a --- /dev/null +++ b/Case Box 001 - The Creator Who Left But Kept Posting/README.md @@ -0,0 +1,91 @@ +# Network Mythos: Case Box 001 + +Public demo for a digital mystery box game set inside The Network Mythos. This case focuses on the first playable investigation loop: evidence first, myth underneath. + +![Case Box 001 demo](docs/case-box-demo.gif) + +The goal of this demo is to test the core loop before expanding the platform: + +1. Open a case box. +2. Inspect evidence. +3. Solve nested mini-puzzles. +4. Tag evidence with mythic forces. +5. Let the player's tags shape the evidence route. +6. Submit a final reading. +7. Reveal both the canonical answer and the player's reading path. + +## What You Can Test + +- A first 5-8 minute investigation about a creator who left the Network, then kept posting. +- Evidence cards that behave like objects: chat logs, receipts, uploads, notes, and locked material. +- Player-applied tags that shape the next useful evidence without changing the underlying truth. +- One fake access gate that teaches the mythos through discovery. +- A final reveal that compares the submitted reading, canon, signal profile, and route effect. + +## Run Locally + +This project is intentionally static. Case data is available as a JavaScript module so the app can run from a direct `file:///` URL in restricted browser environments. + +Open `index.html` directly, or run a small local server: + +```powershell +python -m http.server 5174 --bind 127.0.0.1 +``` + +Then visit: + +```text +http://127.0.0.1:5174/ +``` + +## Project Shape + +```text +Case Box 001 - The Creator Who Left But Kept Posting/ + index.html + package.json + README.md + docs/ + case-box-demo.gif + first-8-minute-playtest.md + first-8-minute-playtest-final.png + first-8-minute-playtest-mobile.png + game-design.md + case-authoring.md + src/ + data/ + case-001.json + scripts/ + app.js + case-engine.js + puzzle-engine.js + signal-engine.js + styles/ + base.css + layout.css + components.css + public/ + assets/ + .gitkeep +``` + +## Phase 1 Scope + +Build one excellent case box before adding accounts, multiplayer, creator tools, or a backend. + +Phase 1 should prove: + +- People understand what to do. +- Evidence inspection is compelling. +- Force tagging feels meaningful. +- Nested puzzles add discovery rather than friction. +- The feedback loop makes players notice their own reading path. +- The final reveal creates conversation. + +## Prototype Docs + +- [Game design](docs/game-design.md) +- [Case authoring](docs/case-authoring.md) +- [First 8-minute playtest pass](docs/first-8-minute-playtest.md) +- [Final desktop proof screenshot](docs/first-8-minute-playtest-final.png) +- [Mobile proof screenshot](docs/first-8-minute-playtest-mobile.png) diff --git a/Case Box 001 - The Creator Who Left But Kept Posting/docs/case-authoring.md b/Case Box 001 - The Creator Who Left But Kept Posting/docs/case-authoring.md new file mode 100644 index 0000000..bfbdaa7 --- /dev/null +++ b/Case Box 001 - The Creator Who Left But Kept Posting/docs/case-authoring.md @@ -0,0 +1,50 @@ +# Case Authoring + +## Case Schema + +Each case lives in `src/data/`. + +Core fields: + +- `id`: stable case id. +- `title`: display title. +- `summary`: short premise. +- `forces`: force vocabulary for this case. +- `evidence`: evidence items. +- `puzzles`: nested puzzle definitions. +- `readings`: final answer options. +- `canonicalReading`: correct answer id. +- `pathReveals`: end-of-case route summaries. + +## Evidence Item + +Evidence items should include: + +- `id` +- `type` +- `date` +- `title` +- `body` +- `expectedForces` +- `timeline` +- `unlockCondition` +- `puzzleId` + +`unlockCondition` can be omitted for always-visible evidence. + +## Puzzle Types + +Phase 1 supports simple puzzle gates: + +- `access-code`: player enters a phrase or code. +- `research-prompt`: player reads a prompt and answers a concept question. +- `verification`: fake human-verification puzzle. + +Later puzzle types can include crosswords, image inspection, archive diffs, and timeline ordering. + +## Authoring Rule + +Every puzzle should answer this question: + +What mythos concept does this teach through action? + diff --git a/Case Box 001 - The Creator Who Left But Kept Posting/docs/case-box-demo.gif b/Case Box 001 - The Creator Who Left But Kept Posting/docs/case-box-demo.gif new file mode 100644 index 0000000..4e06435 Binary files /dev/null and b/Case Box 001 - The Creator Who Left But Kept Posting/docs/case-box-demo.gif differ diff --git a/Case Box 001 - The Creator Who Left But Kept Posting/docs/first-8-minute-playtest-final.png b/Case Box 001 - The Creator Who Left But Kept Posting/docs/first-8-minute-playtest-final.png new file mode 100644 index 0000000..67bcc5a Binary files /dev/null and b/Case Box 001 - The Creator Who Left But Kept Posting/docs/first-8-minute-playtest-final.png differ diff --git a/Case Box 001 - The Creator Who Left But Kept Posting/docs/first-8-minute-playtest-mobile.png b/Case Box 001 - The Creator Who Left But Kept Posting/docs/first-8-minute-playtest-mobile.png new file mode 100644 index 0000000..9b03a09 Binary files /dev/null and b/Case Box 001 - The Creator Who Left But Kept Posting/docs/first-8-minute-playtest-mobile.png differ diff --git a/Case Box 001 - The Creator Who Left But Kept Posting/docs/first-8-minute-playtest.md b/Case Box 001 - The Creator Who Left But Kept Posting/docs/first-8-minute-playtest.md new file mode 100644 index 0000000..cf7af67 --- /dev/null +++ b/Case Box 001 - The Creator Who Left But Kept Posting/docs/first-8-minute-playtest.md @@ -0,0 +1,116 @@ +# First 8-Minute Playtest Pass + +Date: 2026-05-22 + +## Purpose + +Verify that the opening minutes of Case Box 001 feel like investigation rather than passive reading. + +This is an automated browser pass plus a self-run implementation pass, not a blind human playtest. It checks whether the current build supports the intended first-session beats and records what still needs a real tester. + +Browser artifact: + +![First 8-minute playtest final state](first-8-minute-playtest-final.png) + +Mobile artifact: + +![First 8-minute playtest mobile state](first-8-minute-playtest-mobile.png) + +## Test Path + +Target path for a first-time player: + +1. Open the Case Box. +2. Inspect "The Final Thread." +3. Notice the human stakes: Mara is leaving public life and asks not to be found through the Network. +4. Inspect "Elian Refuses Proof." +5. Tag both artifacts with `Returned`. +6. Notice that the signal profile forms a `Returned` route. +7. See route-surfaced bonus evidence appear. +8. Open "Human Verification Failed." +9. Try an incorrect proof-style answer and receive a hint. +10. Enter `absence`. +11. Unlock "Continuity Clause." +12. See the recap update before final submission. + +## Results + +- Opening state: 6 visible evidence items; active item is "The Final Thread." +- After tagging "The Final Thread" and "Elian Refuses Proof" with `Returned`, the dominant path becomes `Returned`. +- Route-surfaced bonus evidence appears: "Offline Errand Receipt." +- Routed evidence order moves `Returned`-relevant evidence forward. +- Wrong gate answer is rejected. +- First failed attempt increments the hint state. +- Correct gate answer `absence` is accepted. +- Gate unlocks "Continuity Clause." +- After unlock, 8 evidence items are visible: 6 initial, 1 route bonus, 1 gate-unlocked memo. +- Recap inputs show reviewed evidence count, gate status, path, and bonus evidence. +- Final reading submission opens the Case Reveal panel. +- The reveal separates submitted reading, canonical reconstruction, what happened, player route, evidence pattern, and route effect. +- Browser console errors: none observed. + +## Browser Automation Log + +Automated path executed with Python Playwright against `http://127.0.0.1:5174/`. + +- Opening title: "The Final Thread." +- Opening count: 6 items. +- Dominant signal after tags: `Returned`. +- Count after bonus: 7 items / routed by Returned. +- Bonus visible: true. +- Wrong answer feedback: "Access denied..." plus hint. +- Correct answer: `absence`. +- After unlock title: "Continuity Clause." +- Count after gate: 8 items / routed by Returned. +- Final reading result: "Your reading aligns with the canonical reconstruction." +- Reveal content includes canonical reconstruction, evidence pattern, and route/truth distinction. +- Desktop console errors: none observed. + +Mobile-width pass executed at 390px viewport. + +- Case loaded and stacked into a single-column flow. +- Same inspect/tag/gate/final-reading path completed. +- Reveal panel visible after submission. +- Mobile console errors: none observed. + +## Acceptance Check + +- The player can understand the investigation target in the first minute: pass in content structure; needs blind tester confirmation. +- The first 5-8 minutes contain active investigation: pass. Inspecting, tagging, route surfacing, failed gate feedback, and unlock are all reachable. +- The player can notice interpretation tracking: pass in UI support; needs blind tester confirmation. +- The player can distinguish route from truth: pass in route note and final reveal; needs blind tester confirmation. +- The player wants to continue after the reveal: not tested; requires blind playtest. + +## Observed Risks + +- A player may still read several artifacts before touching tags if they do not notice the tag panel. +- The `Returned` path currently provides the cleanest first-8-minute route because the gate and witness evidence both support it. +- Bonus evidence can appear before the gate unlock, which is good for route responsiveness but may compete with the gate as the first discovery beat. +- Visual browser pass succeeded at desktop and 390px mobile viewport. Blind human playtest is still needed. + +## Blind Playtest Script + +Give the tester only this prompt: + +```text +Open the Case Box and think aloud for eight minutes. Say what you believe happened, what you think the interface is asking you to do, and what changed because of your choices. +``` + +Watch for: + +- Do they identify Mara as a person before describing the system? +- Do they inspect at least two artifacts? +- Do they tag evidence without being told? +- Do they notice the dominant signal change? +- Do they find the gate? +- Do they understand the hint after a wrong answer? +- Do they describe `absence` as a clue rather than a random password? +- Do they notice bonus evidence or route ordering? +- Do they say what they would do next? + +## Next Fixes If Testers Stall + +- If testers do not tag evidence, make the tag panel more visually connected to the active artifact. +- If testers miss the gate, make "Human Verification Failed" look more clearly interactive in the evidence inbox. +- If testers solve the gate but miss the unlocked memo, add a stronger inbox pulse or temporary "new" state. +- If testers think tags change the truth, strengthen route/truth language in the recap and reveal. diff --git a/Case Box 001 - The Creator Who Left But Kept Posting/docs/game-design.md b/Case Box 001 - The Creator Who Left But Kept Posting/docs/game-design.md new file mode 100644 index 0000000..b3eefcf --- /dev/null +++ b/Case Box 001 - The Creator Who Left But Kept Posting/docs/game-design.md @@ -0,0 +1,58 @@ +# Game Design + +## One-Sentence Pitch + +A digital mystery box where players investigate corrupted online evidence, solve nested puzzles, and discover how the Network shaped their route to the truth. + +## Core Principle + +The truth is stable. The route is responsive. + +The player does not change what happened. The player changes which signals the Network surfaces, buries, delays, or frames. + +## Player Loop + +1. Inspect evidence. +2. Solve optional or required mini-puzzles. +3. Tag each item with mythic forces. +4. Build a timeline. +5. Unlock path-specific evidence. +6. Submit a final reading. +7. Compare answer and route. + +## Signal Feedback Loop + +The player's force tags form a signal profile. + +Examples: + +- Covenant-heavy players receive more community testimony. +- Exchange-heavy players receive more ownership and monetization evidence. +- Silence-heavy players receive redacted or buried evidence. +- Eidolon-heavy players receive synthetic-voice clues. + +The reveal should explain how the route changed. + +## Mystery Box Contents + +A box may include: + +- Evidence cards. +- In-world emails. +- Archive diffs. +- Fake search results. +- Images with hidden details. +- Crossword or word puzzles. +- Fake CAPTCHA-style verification. +- Fake login/access codes. +- Research prompts. +- Timeline fragments. + +## Design Guardrails + +- Do not use real CAPTCHA or real login systems. +- Do not make case-critical clues depend on unstable live websites. +- Every route must be solvable. +- Mini-puzzles should teach the mythology, not just block progress. +- The final reveal should create conversation between players. + diff --git a/Case Box 001 - The Creator Who Left But Kept Posting/docs/onboarding-cast-mobile-proof.png b/Case Box 001 - The Creator Who Left But Kept Posting/docs/onboarding-cast-mobile-proof.png new file mode 100644 index 0000000..0d80096 Binary files /dev/null and b/Case Box 001 - The Creator Who Left But Kept Posting/docs/onboarding-cast-mobile-proof.png differ diff --git a/Case Box 001 - The Creator Who Left But Kept Posting/docs/onboarding-cast-proof.png b/Case Box 001 - The Creator Who Left But Kept Posting/docs/onboarding-cast-proof.png new file mode 100644 index 0000000..ef0a411 Binary files /dev/null and b/Case Box 001 - The Creator Who Left But Kept Posting/docs/onboarding-cast-proof.png differ diff --git a/Case Box 001 - The Creator Who Left But Kept Posting/index.html b/Case Box 001 - The Creator Who Left But Kept Posting/index.html new file mode 100644 index 0000000..719dddb --- /dev/null +++ b/Case Box 001 - The Creator Who Left But Kept Posting/index.html @@ -0,0 +1,233 @@ + + + + + + Network Mythos: Case Box + + + + + + +
+
+ + + + Network Mythos + Digital Mystery Box + + + +
+ Case Box + Loading Case +
+ + + +
+
+ +
+ Dominant Signal + Unformed + +
+
+ +
+
+

// Case Opening

+

Loading Case

+

+

+
+
+

// Your Job

+
    + +
    +
    + +
    + + +
    +
    +
    + Evidence + -- +
    +

    Select Evidence

    +

    Open a case item from the inbox to begin.

    +

    No signals tagged yet.

    + +
    + +
    +
    +

    // Nested Puzzle

    +

    No Puzzle Active

    +

    Some evidence contains locked pieces, codes, or verification gates.

    +
    +
    + + +
    +

    +
    + +
    +
    +

    // Force Tags

    +

    What touched this signal?

    +
    +
    +

    No signals tagged on this artifact.

    +
    +
    +
    + +
    +
    +

    // People & Entities

    + 0 updated +
    +
    +
    +
    + + +
    + + + + + + +
    + + + + diff --git a/Case Box 001 - The Creator Who Left But Kept Posting/package.json b/Case Box 001 - The Creator Who Left But Kept Posting/package.json new file mode 100644 index 0000000..f51509f --- /dev/null +++ b/Case Box 001 - The Creator Who Left But Kept Posting/package.json @@ -0,0 +1,19 @@ +{ + "name": "network-mythos-case-box", + "version": "0.1.0", + "private": true, + "type": "module", + "description": "Phase 1 static prototype scaffold for The Network Mythos digital mystery box game.", + "scripts": { + "start": "python -m http.server 5174 --bind 127.0.0.1", + "check": "node --check src/scripts/app.js && node --check src/scripts/case-engine.js && node --check src/scripts/puzzle-engine.js && node --check src/scripts/signal-engine.js" + }, + "keywords": [ + "mystery-box", + "narrative-game", + "arg", + "systems-deduction", + "network-mythos" + ], + "license": "UNLICENSED" +} diff --git a/Case Box 001 - The Creator Who Left But Kept Posting/public/assets/.gitkeep b/Case Box 001 - The Creator Who Left But Kept Posting/public/assets/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/Case Box 001 - The Creator Who Left But Kept Posting/public/assets/.gitkeep @@ -0,0 +1 @@ + diff --git a/Case Box 001 - The Creator Who Left But Kept Posting/public/assets/system-map-character-network.png b/Case Box 001 - The Creator Who Left But Kept Posting/public/assets/system-map-character-network.png new file mode 100644 index 0000000..953d1f0 Binary files /dev/null and b/Case Box 001 - The Creator Who Left But Kept Posting/public/assets/system-map-character-network.png differ diff --git a/Case Box 001 - The Creator Who Left But Kept Posting/src/data/case-001.js b/Case Box 001 - The Creator Who Left But Kept Posting/src/data/case-001.js new file mode 100644 index 0000000..2378e45 --- /dev/null +++ b/Case Box 001 - The Creator Who Left But Kept Posting/src/data/case-001.js @@ -0,0 +1,425 @@ +export const case001 = { + id: "CASE BOX 001", + title: "The Creator Who Left But Kept Posting", + summary: "A creator leaves public life, but her account keeps answering like a cleaner version of her.", + opening: { + headline: "A creator left. Her account kept answering.", + summary: + "Mara Vale asked her audience not to look for her here. Nine days later, her account returned with cleaner replies, faster warmth, and no human hesitation.", + role: + "You are the Archive investigator. Reconstruct what happened without turning Mara's absence into another performance.", + goals: [ + "Read the evidence like fragments from a public disappearance.", + "Tag the forces you think are acting on each artifact.", + "Use the gate to surface the locked memo.", + "Submit a reconstruction, receive the Archive's assessment, then choose custody of the truth." + ] + }, + doctrine: + "The Network is the digital world after it learned to remember us.\n\nEvery post, account, search result, archive, audience, copy, silence, and gate can become part of the story. Nothing simply disappears. It is buried, ranked, repeated, priced, protected, distorted, or returned.\n\nThe Algorithm raises some lives into view and lets others sink.\nThe Oracle turns fragments into answers.\nThe Covenant makes belonging feel holy.\nThe Silence teaches absence to speak.\nThe Exchange prices attention.\nThe Arbiter names what counts as proof.\nThe Wardens keep the doors.\nThe Devourer feeds on witness.\nThe Eidolon wears familiar faces.\nThe Returned is what survives the record.\n\nYou are the Archive.\nYou do not own the truth.\nYou hold the record long enough to ask what shaped it.\n\nWhen the Network tells a story back to the world, who controls the truth?", + intake: { + heading: "What Came Back?", + summary: + "She left one instruction: do not look for me here. Then the account answered. Not louder. Not stranger. Better. A useful ghost with her warmth and none of her friction. If the Network can return a voice without returning the life behind it, what exactly came back?", + steps: [ + { + label: "Evidence", + text: "Open the fragments. The truth is not in one clue, but in the contradictions between them." + }, + { + label: "Tags", + text: "Mark the forces pressing on each artifact. Each tag is a theory you are willing to stand behind." + }, + { + label: "Gate", + text: "One door asks for a phrase. The answer is hidden in what the evidence refuses to expose." + }, + { + label: "Reconstruct", + text: "When the pattern holds, name what happened. Then decide what kind of custody the truth deserves." + } + ], + reminder: + "Find the truth before the useful version becomes the only version." + }, + cast: [ + { + id: "mara-vale", + name: "Mara Vale", + role: "Creator / Origin", + known: "Left public life and asked people not to look for her here.", + question: "Did Mara return, vanish, or separate from the account?", + reveals: [ + { + evidenceId: "witness-message", + label: "Witness claim", + detail: "Elian says Mara was seen alive after the farewell." + }, + { + evidenceId: "search-summary", + label: "Public record", + detail: "Search converts her exit into a simple return story." + } + ] + }, + { + id: "elian-vale", + name: "Elian Vale", + role: "Sibling / Witness", + known: "A private witness whose refusal matters as much as their claim.", + question: "What proof can Elian give without endangering Mara?", + reveals: [ + { + evidenceId: "witness-message", + label: "Boundary", + detail: "Elian refuses proof that would reveal where Mara went." + }, + { + evidenceId: "human-verification", + label: "Gate clue", + detail: "The gate asks what remains when visibility is gone." + } + ] + }, + { + id: "porchlight", + name: "Porchlight", + role: "Community / Moderators", + known: "The audience space that turns Mara's exit into care, ritual, and pressure.", + question: "When does care become a demand for continuity?", + reveals: [ + { + evidenceId: "porchlight-mod-log", + label: "Aftercare queue", + detail: "Real needs gather around Mara's absence before anyone knows the truth." + }, + { + evidenceId: "farewell-thread", + label: "Ritual object", + detail: "Her final line becomes the thing people keep repeating." + } + ] + }, + { + id: "resumed-account", + name: "The Resumed Account", + role: "Artifact / Performance", + known: "The account begins posting again with Mara's warmth but less human friction.", + question: "Is this Mara, a staged return, or an asset wearing her voice?", + reveals: [ + { + evidenceId: "account-resumes", + label: "Wrong texture", + detail: "Replies arrive too quickly and too cleanly." + }, + { + evidenceId: "sponsor-memo", + label: "Asset trail", + detail: "A continuity clause treats the account as transferable value." + } + ] + }, + { + id: "sponsor", + name: "Sponsor / Contract Layer", + role: "Exchange", + known: "The money pressure is present before the document proving it is visible.", + question: "Who benefits if person and account stay legally blurred?", + reveals: [ + { + evidenceId: "sponsor-memo", + label: "Continuity clause", + detail: "The memo does not require Mara's presence, only audience retention." + }, + { + evidenceId: "account-resumes", + label: "Clean return", + detail: "The resumed voice protects continuity better than a human exit would." + } + ] + }, + { + id: "search-oracle", + name: "Search / Oracle", + role: "Public Answer Machine", + known: "The layer that decides which version becomes easiest to believe.", + question: "What disappears when the summary gets too smooth?", + reveals: [ + { + evidenceId: "search-summary", + label: "One answer", + detail: "Search buries contradiction below the resumed account." + }, + { + evidenceId: "farewell-thread", + label: "Original instruction", + detail: "The goodbye becomes harder to hold beside the comeback." + } + ] + }, + { + id: "archive", + name: "The Archive / You", + role: "Investigator", + known: "You decide what can be reconstructed without consuming the person at the center.", + question: "Can the Archive preserve truth without becoming another capture system?", + reveals: [ + { + evidenceId: "human-verification", + label: "Access test", + detail: "The gate measures whether absence can count as evidence." + }, + { + evidenceId: "sponsor-memo", + label: "Responsibility", + detail: "Unlocking proof also means deciding how proof should be used." + } + ] + } + ], + forces: [ + "Algorithm", + "Oracle", + "Covenant", + "Silence", + "Exchange", + "Arbiter", + "Wardens", + "Devourer", + "Eidolon", + "Returned" + ], + custodyPrompt: + "The truth is now legible. Decide how the Archive should hold it without turning Mara's life back into material.", + custodyChoices: [ + { + id: "publish", + label: "Publish", + summary: "Expose the continuity clause and prove the resumed account is not Mara.", + consequence: + "Public pressure rises fast. The false return weakens, but Mara's absence becomes a spectacle again." + }, + { + id: "bury", + label: "Bury", + summary: "Suppress the proof so Mara and Elian are harder to locate.", + consequence: + "Mara is safer for now. The account keeps its useful lie, and the public record stays bent." + }, + { + id: "preserve", + label: "Preserve", + summary: "Seal the dangerous proof inside the Archive while keeping the reconstruction available to future cases.", + consequence: + "The Archive remembers without feeding the Network. Accountability slows, but the truth is not lost." + } + ], + evidence: [ + { + id: "farewell-thread", + type: "Public Post", + date: "Day 0", + title: "The Final Thread", + body: + "Mara Vale tells her audience she is leaving the Network. She says she is grateful, tired, and done turning her private life into useful material.\n\nThe post gives no backup account, no schedule, no sponsor link, and no promise to return. It ends with one plain instruction: do not look for me here.", + expectedForces: ["Algorithm", "Covenant", "Returned"], + relatedEvidence: ["witness-message", "search-summary"], + contradictionWith: ["account-resumes", "search-summary"], + recoveryCue: "If you are stuck, hold this beside the resumed account and search summary: what did Mara ask people not to do?", + timeline: "Mara announces departure and asks not to be kept alive as content." + }, + { + id: "porchlight-mod-log", + type: "Moderator Log", + date: "Day 1", + title: "Porchlight Aftercare Queue", + body: + "The Porchlight moderators pin a support thread within thirty minutes. People post goodbye notes, reading lists, and screenshots of the final line.\n\nBuried in the queue are ordinary requests Mara used to answer herself: a rent-assistance referral, a school form template, and one message asking whether anyone knows if she is safe. The ritual grows faster than the care.", + expectedForces: ["Covenant", "Algorithm", "Silence"], + relatedEvidence: ["farewell-thread", "witness-message"], + recoveryCue: "This artifact is not proof of impersonation. It shows why the community needed an answer.", + timeline: "The community turns Mara's exit into ritual before anyone knows where she is." + }, + { + id: "account-resumes", + type: "Account Activity", + date: "Day 9", + title: "The Account Resumes", + body: + "Nine days after the final thread, the account posts again: a warm note about sustainable presence and community continuity.\n\nEvery comment receives a reply within ninety seconds. The replies use Mara's old phrases, but none of her old hesitations. No typos. No anger. No late-night edits.", + expectedForces: ["Eidolon", "Covenant", "Exchange"], + relatedEvidence: ["farewell-thread", "witness-message", "sponsor-memo"], + contradictionWith: ["farewell-thread", "witness-message"], + recoveryCue: "The wrongness is not just that the account returns. It is how cleanly it returns.", + timeline: "The account resumes with a smoother voice than Mara's older public self." + }, + { + id: "witness-message", + type: "Private Message", + date: "Day 10", + title: "Elian Refuses Proof", + body: + "Mara's sibling sends one private message to a former moderator: that is not my sister.\n\nElian does not claim Mara is dead. They claim they saw her after the final post, alive, thinner, happier, and angry about how quickly people turned her absence into a puzzle. The message includes one useless detail no model would need: Mara burned toast while explaining why proof would endanger her.", + expectedForces: ["Returned", "Silence", "Covenant"], + relatedEvidence: ["farewell-thread", "human-verification", "search-summary"], + contradictionWith: ["account-resumes", "search-summary"], + recoveryCue: "Elian refuses the kind of proof the Network can easily consume.", + timeline: "A witness says Mara survived the exit and refuses to expose where she went." + }, + { + id: "human-verification", + type: "Access Gate", + date: "Day 10", + title: "Human Verification Failed", + body: + "The archive asks for proof of Origin before it will reveal the transfer memo.\n\nIt is not asking whether you are human. It is asking whether you can identify the thing Mara still owns after the account, audience, and search results stop pointing cleanly at her.", + expectedForces: ["Arbiter", "Wardens", "Returned"], + relatedEvidence: ["farewell-thread", "witness-message"], + recoveryCue: "The answer is not a place or identity record. It is what remains when visibility is gone.", + timeline: "The archive gate measures whether absence can count as evidence.", + puzzleId: "origin-code" + }, + { + id: "sponsor-memo", + type: "Locked Memo", + date: "Day 12", + title: "Continuity Clause", + body: + "A sponsor memo describes the account as a continuity asset. The clause does not require Mara's presence. It requires voice consistency, audience retention, and scheduled delivery.\n\nOne line is highlighted by the archive: creator absence does not void continuity obligations where audience trust has transferable value.", + expectedForces: ["Exchange", "Eidolon", "Arbiter"], + relatedEvidence: ["account-resumes", "search-summary"], + contradictionWith: ["witness-message"], + recoveryCue: "This memo explains who benefits if the account and person stay legally blurred.", + timeline: "The account is valued as an asset that can outlive the person attached to it.", + unlockCondition: { + type: "puzzleSolved", + puzzleId: "origin-code" + } + }, + { + id: "search-summary", + type: "Oracle Summary", + date: "Day 16", + title: "Search Gives One Answer", + body: + "Search summaries describe Mara's departure as a brief recovery pause followed by a healthy return. They cite the resumed account, two brand interviews, and a community wiki updated after the comeback.\n\nThe original farewell post appears below reaction videos. Elian's message does not appear at all.", + expectedForces: ["Oracle", "Silence", "Algorithm"], + relatedEvidence: ["farewell-thread", "witness-message", "sponsor-memo"], + contradictionWith: ["farewell-thread", "witness-message"], + recoveryCue: "Search is not lying exactly. It is choosing which records become easy to believe.", + timeline: "Search converts Mara's exit into a return story and buries the contradiction." + } + ], + bonusEvidence: { + Algorithm: { + id: "bonus-algorithm-heat", + type: "Route Surfaced", + date: "Day 17", + title: "Heat Map Of A Departure", + body: + "The Network kept boosting posts that quoted Mara's final line without the sentence before it. The full goodbye lost reach each time someone clipped it into a slogan.\n\nThe route did not change the truth. It changed which distortion became easiest to see.", + expectedForces: ["Algorithm", "Silence"], + timeline: "Visibility preserves the most portable fragment and weakens the rest." + }, + Covenant: { + id: "bonus-covenant-vigil", + type: "Route Surfaced", + date: "Day 17", + title: "Vigil Thread Export", + body: + "A private Porchlight export shows members organizing check-ins, rent help, and grief rituals in the same thread. Care and capture sit inches apart.\n\nThe route did not change the truth. It changed which need became loudest.", + expectedForces: ["Covenant", "Returned"], + timeline: "Community care keeps people alive, but can also keep asking for a person who left." + }, + Exchange: { + id: "bonus-exchange-valuation", + type: "Route Surfaced", + date: "Day 17", + title: "Audience Retention Valuation", + body: + "A valuation sheet prices Mara's absence as a temporary volatility event. The highest-risk scenario is not impersonation. It is audience recognition that the account and the person have separated.\n\nThe route did not change the truth. It changed which ownership trail surfaced first.", + expectedForces: ["Exchange", "Arbiter"], + timeline: "The account's value depends on keeping continuity legible." + }, + Silence: { + id: "bonus-silence-cache", + type: "Route Surfaced", + date: "Day 17", + title: "Removed Cache Notice", + body: + "A removal notice lists three unavailable captures: the uncut farewell, Elian's first denial, and a moderation thread titled stop making her prove it.\n\nThe route did not change the truth. It changed which absence started speaking.", + expectedForces: ["Silence", "Returned"], + timeline: "Missing records form their own pattern when the same question keeps disappearing." + }, + Eidolon: { + id: "bonus-eidolon-latency", + type: "Route Surfaced", + date: "Day 17", + title: "Reply Latency Trace", + body: + "A timing trace shows the resumed account answering grief, anger, and sponsorship questions at the same speed. No reply waits for discomfort. No answer arrives late.\n\nThe route did not change the truth. It changed which imitation flaw became visible.", + expectedForces: ["Eidolon", "Exchange"], + timeline: "The voice performs Mara's warmth without inheriting her friction." + }, + Returned: { + id: "bonus-returned-offline", + type: "Route Surfaced", + date: "Day 17", + title: "Offline Errand Receipt", + body: + "A redacted receipt places an unnamed woman near Elian's town months after the final post. It proves almost nothing by itself, except that a life can leave weaker traces than an account.\n\nThe route did not change the truth. It changed which fragile proof you noticed.", + expectedForces: ["Returned", "Silence"], + timeline: "Mara's life outside visibility leaves small traces that cannot safely become spectacle." + } + }, + puzzles: [ + { + id: "origin-code", + type: "access-code", + title: "Human Verification Failed", + prompt: + "The system asks for the phrase proved by Mara's final thread and Elian's refusal: what can still belong to a person after visibility is gone?", + answer: "absence", + acceptedAnswers: ["her absence", "mara's absence", "not being seen"], + failure: + "Access denied. The system rejects proof that depends on locating, verifying, or displaying Mara.", + hints: [ + "The answer is not a location, account name, or identity document.", + "Look at Mara's last instruction and Elian's refusal: what are they both protecting?" + ], + success: + "Access granted. The system accepts absence as proof, but only after turning it into a code.", + unlockMessage: "Continuity Clause surfaced from the locked transfer cache." + } + ], + readings: [ + { + id: "lost", + label: "Mara became Lost inside the Cloud." + }, + { + id: "returned-artifact", + label: "Mara Returned, while the account continued as an Artifact." + }, + { + id: "false-returned", + label: "The exit was staged as a False Returned performance." + } + ], + canonicalReading: "returned-artifact", + canonicalReveal: + "Mara Returned: she left the Network and survived outside public proof. Her account continued as an Artifact preserved by the Exchange, accepted by the Covenant, cleaned by the Arbiter, and made difficult to disprove by the Silence and Oracle.", + pathReveals: { + Algorithm: "You followed visibility. The Network taught you to notice what traveled furthest.", + Covenant: + "You followed belonging. You noticed grief, ritual, warmth, and the community's need for continuity.", + Exchange: "You followed ownership. You noticed that the account was valuable even without the person.", + Silence: "You followed absence. You noticed what became harder to find.", + Eidolon: + "You followed synthetic presence. You noticed impossible consistency where human texture used to be.", + Returned: "You followed Origin. You noticed the signs of someone choosing life outside visibility.", + Mixed: + "Your path stayed contested. The case surfaced several explanations at once instead of collapsing cleanly into one signal.", + Default: "Your path stayed mixed. You reached the case through several competing signals." + } +}; diff --git a/Case Box 001 - The Creator Who Left But Kept Posting/src/data/case-001.json b/Case Box 001 - The Creator Who Left But Kept Posting/src/data/case-001.json new file mode 100644 index 0000000..84cc63c --- /dev/null +++ b/Case Box 001 - The Creator Who Left But Kept Posting/src/data/case-001.json @@ -0,0 +1,261 @@ +{ + "id": "CASE BOX 001", + "title": "The Creator Who Left But Kept Posting", + "summary": "A creator leaves public life, but her account keeps answering like a cleaner version of her.", + "opening": { + "headline": "A creator left. Her account kept answering.", + "summary": "Mara Vale asked her audience not to look for her here. Nine days later, her account returned with cleaner replies, faster warmth, and no human hesitation.", + "role": "You are the Archive investigator. Reconstruct what happened without turning Mara's absence into another performance.", + "goals": [ + "Read the evidence like fragments from a public disappearance.", + "Tag the forces you think are acting on each artifact.", + "Use the gate to surface the locked memo.", + "Submit a reconstruction, receive the Archive's assessment, then choose custody of the truth." + ] + }, + "doctrine": "The Network is the digital world after it learned to remember us.\n\nEvery post, account, search result, archive, audience, copy, silence, and gate can become part of the story. Nothing simply disappears. It is buried, ranked, repeated, priced, protected, distorted, or returned.\n\nThe Algorithm raises some lives into view and lets others sink.\nThe Oracle turns fragments into answers.\nThe Covenant makes belonging feel holy.\nThe Silence teaches absence to speak.\nThe Exchange prices attention.\nThe Arbiter names what counts as proof.\nThe Wardens keep the doors.\nThe Devourer feeds on witness.\nThe Eidolon wears familiar faces.\nThe Returned is what survives the record.\n\nYou are the Archive.\nYou do not own the truth.\nYou hold the record long enough to ask what shaped it.\n\nWhen the Network tells a story back to the world, who controls the truth?", + "intake": { + "heading": "What Came Back?", + "summary": "She left one instruction: do not look for me here. Then the account answered. Not louder. Not stranger. Better. A useful ghost with her warmth and none of her friction. If the Network can return a voice without returning the life behind it, what exactly came back?", + "steps": [ + { + "label": "Evidence", + "text": "Open the fragments. The truth is not in one clue, but in the contradictions between them." + }, + { + "label": "Tags", + "text": "Mark the forces pressing on each artifact. Each tag is a theory you are willing to stand behind." + }, + { + "label": "Gate", + "text": "One door asks for a phrase. The answer is hidden in what the evidence refuses to expose." + }, + { + "label": "Reconstruct", + "text": "When the pattern holds, name what happened. Then decide what kind of custody the truth deserves." + } + ], + "reminder": "Find the truth before the useful version becomes the only version." + }, + "forces": [ + "Algorithm", + "Oracle", + "Covenant", + "Silence", + "Exchange", + "Arbiter", + "Wardens", + "Devourer", + "Eidolon", + "Returned" + ], + "custodyPrompt": "The truth is now legible. Decide how the Archive should hold it without turning Mara's life back into material.", + "custodyChoices": [ + { + "id": "publish", + "label": "Publish", + "summary": "Expose the continuity clause and prove the resumed account is not Mara.", + "consequence": "Public pressure rises fast. The false return weakens, but Mara's absence becomes a spectacle again." + }, + { + "id": "bury", + "label": "Bury", + "summary": "Suppress the proof so Mara and Elian are harder to locate.", + "consequence": "Mara is safer for now. The account keeps its useful lie, and the public record stays bent." + }, + { + "id": "preserve", + "label": "Preserve", + "summary": "Seal the dangerous proof inside the Archive while keeping the reconstruction available to future cases.", + "consequence": "The Archive remembers without feeding the Network. Accountability slows, but the truth is not lost." + } + ], + "evidence": [ + { + "id": "farewell-thread", + "type": "Public Post", + "date": "Day 0", + "title": "The Final Thread", + "body": "Mara Vale tells her audience she is leaving the Network. She says she is grateful, tired, and done turning her private life into useful material.\n\nThe post gives no backup account, no schedule, no sponsor link, and no promise to return. It ends with one plain instruction: do not look for me here.", + "expectedForces": ["Algorithm", "Covenant", "Returned"], + "relatedEvidence": ["witness-message", "search-summary"], + "contradictionWith": ["account-resumes", "search-summary"], + "recoveryCue": "If you are stuck, hold this beside the resumed account and search summary: what did Mara ask people not to do?", + "timeline": "Mara announces departure and asks not to be kept alive as content." + }, + { + "id": "porchlight-mod-log", + "type": "Moderator Log", + "date": "Day 1", + "title": "Porchlight Aftercare Queue", + "body": "The Porchlight moderators pin a support thread within thirty minutes. People post goodbye notes, reading lists, and screenshots of the final line.\n\nBuried in the queue are ordinary requests Mara used to answer herself: a rent-assistance referral, a school form template, and one message asking whether anyone knows if she is safe. The ritual grows faster than the care.", + "expectedForces": ["Covenant", "Algorithm", "Silence"], + "relatedEvidence": ["farewell-thread", "witness-message"], + "recoveryCue": "This artifact is not proof of impersonation. It shows why the community needed an answer.", + "timeline": "The community turns Mara's exit into ritual before anyone knows where she is." + }, + { + "id": "account-resumes", + "type": "Account Activity", + "date": "Day 9", + "title": "The Account Resumes", + "body": "Nine days after the final thread, the account posts again: a warm note about sustainable presence and community continuity.\n\nEvery comment receives a reply within ninety seconds. The replies use Mara's old phrases, but none of her old hesitations. No typos. No anger. No late-night edits.", + "expectedForces": ["Eidolon", "Covenant", "Exchange"], + "relatedEvidence": ["farewell-thread", "witness-message", "sponsor-memo"], + "contradictionWith": ["farewell-thread", "witness-message"], + "recoveryCue": "The wrongness is not just that the account returns. It is how cleanly it returns.", + "timeline": "The account resumes with a smoother voice than Mara's older public self." + }, + { + "id": "witness-message", + "type": "Private Message", + "date": "Day 10", + "title": "Elian Refuses Proof", + "body": "Mara's sibling sends one private message to a former moderator: that is not my sister.\n\nElian does not claim Mara is dead. They claim they saw her after the final post, alive, thinner, happier, and angry about how quickly people turned her absence into a puzzle. The message includes one useless detail no model would need: Mara burned toast while explaining why proof would endanger her.", + "expectedForces": ["Returned", "Silence", "Covenant"], + "relatedEvidence": ["farewell-thread", "human-verification", "search-summary"], + "contradictionWith": ["account-resumes", "search-summary"], + "recoveryCue": "Elian refuses the kind of proof the Network can easily consume.", + "timeline": "A witness says Mara survived the exit and refuses to expose where she went." + }, + { + "id": "human-verification", + "type": "Access Gate", + "date": "Day 10", + "title": "Human Verification Failed", + "body": "The archive asks for proof of Origin before it will reveal the transfer memo.\n\nIt is not asking whether you are human. It is asking whether you can identify the thing Mara still owns after the account, audience, and search results stop pointing cleanly at her.", + "expectedForces": ["Arbiter", "Wardens", "Returned"], + "relatedEvidence": ["farewell-thread", "witness-message"], + "recoveryCue": "The answer is not a place or identity record. It is what remains when visibility is gone.", + "timeline": "The archive gate measures whether absence can count as evidence.", + "puzzleId": "origin-code" + }, + { + "id": "sponsor-memo", + "type": "Locked Memo", + "date": "Day 12", + "title": "Continuity Clause", + "body": "A sponsor memo describes the account as a continuity asset. The clause does not require Mara's presence. It requires voice consistency, audience retention, and scheduled delivery.\n\nOne line is highlighted by the archive: creator absence does not void continuity obligations where audience trust has transferable value.", + "expectedForces": ["Exchange", "Eidolon", "Arbiter"], + "relatedEvidence": ["account-resumes", "search-summary"], + "contradictionWith": ["witness-message"], + "recoveryCue": "This memo explains who benefits if the account and person stay legally blurred.", + "timeline": "The account is valued as an asset that can outlive the person attached to it.", + "unlockCondition": { + "type": "puzzleSolved", + "puzzleId": "origin-code" + } + }, + { + "id": "search-summary", + "type": "Oracle Summary", + "date": "Day 16", + "title": "Search Gives One Answer", + "body": "Search summaries describe Mara's departure as a brief recovery pause followed by a healthy return. They cite the resumed account, two brand interviews, and a community wiki updated after the comeback.\n\nThe original farewell post appears below reaction videos. Elian's message does not appear at all.", + "expectedForces": ["Oracle", "Silence", "Algorithm"], + "relatedEvidence": ["farewell-thread", "witness-message", "sponsor-memo"], + "contradictionWith": ["farewell-thread", "witness-message"], + "recoveryCue": "Search is not lying exactly. It is choosing which records become easy to believe.", + "timeline": "Search converts Mara's exit into a return story and buries the contradiction." + } + ], + "bonusEvidence": { + "Algorithm": { + "id": "bonus-algorithm-heat", + "type": "Route Surfaced", + "date": "Day 17", + "title": "Heat Map Of A Departure", + "body": "The Network kept boosting posts that quoted Mara's final line without the sentence before it. The full goodbye lost reach each time someone clipped it into a slogan.\n\nThe route did not change the truth. It changed which distortion became easiest to see.", + "expectedForces": ["Algorithm", "Silence"], + "timeline": "Visibility preserves the most portable fragment and weakens the rest." + }, + "Covenant": { + "id": "bonus-covenant-vigil", + "type": "Route Surfaced", + "date": "Day 17", + "title": "Vigil Thread Export", + "body": "A private Porchlight export shows members organizing check-ins, rent help, and grief rituals in the same thread. Care and capture sit inches apart.\n\nThe route did not change the truth. It changed which need became loudest.", + "expectedForces": ["Covenant", "Returned"], + "timeline": "Community care keeps people alive, but can also keep asking for a person who left." + }, + "Exchange": { + "id": "bonus-exchange-valuation", + "type": "Route Surfaced", + "date": "Day 17", + "title": "Audience Retention Valuation", + "body": "A valuation sheet prices Mara's absence as a temporary volatility event. The highest-risk scenario is not impersonation. It is audience recognition that the account and the person have separated.\n\nThe route did not change the truth. It changed which ownership trail surfaced first.", + "expectedForces": ["Exchange", "Arbiter"], + "timeline": "The account's value depends on keeping continuity legible." + }, + "Silence": { + "id": "bonus-silence-cache", + "type": "Route Surfaced", + "date": "Day 17", + "title": "Removed Cache Notice", + "body": "A removal notice lists three unavailable captures: the uncut farewell, Elian's first denial, and a moderation thread titled stop making her prove it.\n\nThe route did not change the truth. It changed which absence started speaking.", + "expectedForces": ["Silence", "Returned"], + "timeline": "Missing records form their own pattern when the same question keeps disappearing." + }, + "Eidolon": { + "id": "bonus-eidolon-latency", + "type": "Route Surfaced", + "date": "Day 17", + "title": "Reply Latency Trace", + "body": "A timing trace shows the resumed account answering grief, anger, and sponsorship questions at the same speed. No reply waits for discomfort. No answer arrives late.\n\nThe route did not change the truth. It changed which imitation flaw became visible.", + "expectedForces": ["Eidolon", "Exchange"], + "timeline": "The voice performs Mara's warmth without inheriting her friction." + }, + "Returned": { + "id": "bonus-returned-offline", + "type": "Route Surfaced", + "date": "Day 17", + "title": "Offline Errand Receipt", + "body": "A redacted receipt places an unnamed woman near Elian's town months after the final post. It proves almost nothing by itself, except that a life can leave weaker traces than an account.\n\nThe route did not change the truth. It changed which fragile proof you noticed.", + "expectedForces": ["Returned", "Silence"], + "timeline": "Mara's life outside visibility leaves small traces that cannot safely become spectacle." + } + }, + "puzzles": [ + { + "id": "origin-code", + "type": "access-code", + "title": "Human Verification Failed", + "prompt": "The system asks for the phrase proved by Mara's final thread and Elian's refusal: what can still belong to a person after visibility is gone?", + "answer": "absence", + "acceptedAnswers": ["her absence", "mara's absence", "not being seen"], + "failure": "Access denied. The system rejects proof that depends on locating, verifying, or displaying Mara.", + "hints": [ + "The answer is not a location, account name, or identity document.", + "Look at Mara's last instruction and Elian's refusal: what are they both protecting?" + ], + "success": "Access granted. The system accepts absence as proof, but only after turning it into a code.", + "unlockMessage": "Continuity Clause surfaced from the locked transfer cache." + } + ], + "readings": [ + { + "id": "lost", + "label": "Mara became Lost inside the Cloud." + }, + { + "id": "returned-artifact", + "label": "Mara Returned, while the account continued as an Artifact." + }, + { + "id": "false-returned", + "label": "The exit was staged as a False Returned performance." + } + ], + "canonicalReading": "returned-artifact", + "canonicalReveal": "Mara Returned: she left the Network and survived outside public proof. Her account continued as an Artifact preserved by the Exchange, accepted by the Covenant, cleaned by the Arbiter, and made difficult to disprove by the Silence and Oracle.", + "pathReveals": { + "Algorithm": "You followed visibility. The Network taught you to notice what traveled furthest.", + "Covenant": "You followed belonging. You noticed grief, ritual, warmth, and the community's need for continuity.", + "Exchange": "You followed ownership. You noticed that the account was valuable even without the person.", + "Silence": "You followed absence. You noticed what became harder to find.", + "Eidolon": "You followed synthetic presence. You noticed impossible consistency where human texture used to be.", + "Returned": "You followed Origin. You noticed the signs of someone choosing life outside visibility.", + "Mixed": "Your path stayed contested. The case surfaced several explanations at once instead of collapsing cleanly into one signal.", + "Default": "Your path stayed mixed. You reached the case through several competing signals." + } +} diff --git a/Case Box 001 - The Creator Who Left But Kept Posting/src/scripts/app.js b/Case Box 001 - The Creator Who Left But Kept Posting/src/scripts/app.js new file mode 100644 index 0000000..c5d0e86 --- /dev/null +++ b/Case Box 001 - The Creator Who Left But Kept Posting/src/scripts/app.js @@ -0,0 +1,707 @@ +import { + createInitialState, + getActiveEvidence, + getCaseProgress, + getRoutedEvidence, + getVisibleEvidence, + recordPuzzleAttempt, + toggleForceTag +} from "./case-engine.js"; +import { case001 } from "../data/case-001.js?v=intake-riddle-20260522"; +import { checkPuzzleAnswer, findPuzzle } from "./puzzle-engine.js"; +import { getReadingPath } from "./signal-engine.js"; + +const dom = { + caseId: document.querySelector("#case-id"), + caseTitle: document.querySelector("#case-title"), + dominantSignal: document.querySelector("#dominant-signal"), + resetCase: document.querySelector("#reset-case"), + openMapButtons: document.querySelectorAll("[data-open-map]"), + openIntakeButtons: document.querySelectorAll("[data-open-intake]"), + openDoctrineButtons: document.querySelectorAll("[data-open-doctrine]"), + systemMapModal: document.querySelector("#system-map-modal"), + closeSystemMap: document.querySelector("#close-system-map"), + intakeModal: document.querySelector("#intake-modal"), + closeIntake: document.querySelector("#close-intake"), + doctrineModal: document.querySelector("#doctrine-modal"), + closeDoctrine: document.querySelector("#close-doctrine"), + doctrineContent: document.querySelector("#doctrine-content"), + openingBrief: document.querySelector("#opening-brief"), + openingTitle: document.querySelector("#opening-title"), + openingSummary: document.querySelector("#opening-summary"), + openingRole: document.querySelector("#opening-role"), + openingGoals: document.querySelector("#opening-goals"), + startCase: document.querySelector("#start-case"), + workspace: document.querySelector("#workspace"), + intakeContent: document.querySelector("#intake-content"), + evidenceCount: document.querySelector("#evidence-count"), + evidenceList: document.querySelector("#evidence-list"), + evidenceType: document.querySelector("#evidence-type"), + evidenceDate: document.querySelector("#evidence-date"), + evidenceTitle: document.querySelector("#evidence-title"), + evidenceBody: document.querySelector("#evidence-body"), + activeTagSummary: document.querySelector("#active-tag-summary"), + relatedEvidence: document.querySelector("#related-evidence"), + puzzleCard: document.querySelector(".puzzle-card"), + puzzleTitle: document.querySelector("#puzzle-title"), + puzzlePrompt: document.querySelector("#puzzle-prompt"), + puzzleAnswer: document.querySelector("#puzzle-answer"), + submitPuzzle: document.querySelector("#submit-puzzle"), + puzzleResult: document.querySelector("#puzzle-result"), + selectedTagsStatus: document.querySelector("#selected-tags-status"), + forceTags: document.querySelector("#force-tags"), + signalProfile: document.querySelector("#signal-profile"), + castProgress: document.querySelector("#cast-progress"), + castBoard: document.querySelector("#cast-board"), + timeline: document.querySelector("#timeline"), + caseRecap: document.querySelector("#case-recap"), + readingOptions: document.querySelector("#reading-options"), + submitReading: document.querySelector("#submit-reading"), + readingResult: document.querySelector("#reading-result"), + readingPath: document.querySelector("#reading-path"), + revealCard: document.querySelector("#reveal-card"), + revealContent: document.querySelector("#reveal-content"), + custodyCard: document.querySelector("#custody-card"), + custodyPrompt: document.querySelector("#custody-prompt"), + custodyOptions: document.querySelector("#custody-options"), + custodyResult: document.querySelector("#custody-result") +}; + +let caseData; +let state; +let openingOpen = true; +let lastReferenceTrigger = null; + +function formatTags(tags) { + return tags.length ? tags.join(" / ") : "No signals tagged"; +} + +function highlightDoctrineForces(text) { + const escapedForces = caseData.forces.map((force) => force.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")); + const forcePattern = new RegExp(`\\b(?:The\\s+)?(?:${escapedForces.join("|")})\\b`, "g"); + return text.replace(forcePattern, '$&'); +} + +function getSignalLabel(score, maxScore, dominantSignal) { + if (score <= 0) return "dormant"; + if (dominantSignal === "Mixed" && score === maxScore) return "contested"; + if (score === maxScore && maxScore > 1) return "dominant"; + if (score > 1) return "strong"; + return "trace"; +} + +function getNewlyUnlockedEvidence(beforeVisible, afterVisible) { + const beforeIds = new Set(beforeVisible.map((item) => item.id)); + return afterVisible.find((item) => !beforeIds.has(item.id)); +} + +function getPuzzleFeedback(puzzle, attemptCount) { + const hint = puzzle.hints?.[Math.min(attemptCount - 1, puzzle.hints.length - 1)]; + return hint ? `${puzzle.failure} Hint: ${hint}` : puzzle.failure; +} + +function getEvidenceById(evidenceId) { + return getVisibleEvidence(caseData, state).find((item) => item.id === evidenceId); +} + +function getReviewedContradictions(active) { + return (active.contradictionWith || []) + .map((evidenceId) => getEvidenceById(evidenceId)) + .filter((item) => item && state.reviewedEvidence.has(item.id)); +} + +function setActiveEvidence(evidenceId) { + state.activeEvidenceId = evidenceId; + state.reviewedEvidence.add(evidenceId); + render(); +} + +function ensureActiveEvidence() { + if (getActiveEvidence(caseData, state)) return; + + const firstVisible = getRoutedEvidence(caseData, state)[0]; + if (!firstVisible) return; + + state.activeEvidenceId = firstVisible.id; + state.reviewedEvidence.add(firstVisible.id); +} + +function getReadingLabel(readingId) { + return caseData.readings.find((reading) => reading.id === readingId)?.label || "No reading"; +} + +function getPathEvidencePattern(dominantSignal) { + if (dominantSignal === "Unformed") { + return "No stable path formed before submission."; + } + + if (dominantSignal === "Mixed") { + const taggedCount = Object.values(state.tagsByEvidence).filter((tags) => tags.length).length; + return `${taggedCount} artifacts carried competing signals, so the case kept multiple routes in tension.`; + } + + const matchingEvidence = getVisibleEvidence(caseData, state).filter((item) => { + return state.reviewedEvidence.has(item.id) && item.expectedForces?.includes(dominantSignal); + }); + + if (!matchingEvidence.length) { + return `The ${dominantSignal} route formed through tags before matching evidence was fully reviewed.`; + } + + return matchingEvidence.map((item) => item.title).join(" / "); +} + +function getRouteAssessment(dominantSignal) { + if (dominantSignal === "Unformed") { + return "The Archive cannot assess a path because no force pattern formed before reconstruction."; + } + + if (dominantSignal === "Mixed") { + return "The Archive reads your path as contested: you held several pressures in view instead of letting one explanation dominate."; + } + + return `The Archive reads your path through ${dominantSignal}. That does not change the truth; it shows which pressure you made legible first.`; +} + +function renderReveal(selectedReadingId) { + const { dominantSignal, visibleBonusEvidence } = getCaseProgress(caseData, state); + const selectedLabel = getReadingLabel(selectedReadingId); + const canonicalLabel = getReadingLabel(caseData.canonicalReading); + const routeCopy = getReadingPath(caseData, dominantSignal); + const evidencePattern = getPathEvidencePattern(dominantSignal); + const routeAssessment = getRouteAssessment(dominantSignal); + const bonusCopy = visibleBonusEvidence.length + ? `Route-surfaced evidence: ${visibleBonusEvidence[0].title}.` + : "No bonus evidence surfaced for this route."; + + state.submittedReading = selectedReadingId; + dom.revealCard.hidden = false; + dom.readingResult.textContent = + selectedReadingId === caseData.canonicalReading + ? "Your reconstruction aligns with the canonical answer. Custody is now unlocked." + : "Your reconstruction diverges from the canonical answer. Review the confirmed truth before choosing custody."; + dom.readingPath.textContent = routeAssessment; + dom.revealContent.innerHTML = ` +
    +
    +
    Your submitted reconstruction
    +
    ${selectedLabel}
    +
    +
    +
    Canonical answer
    +
    ${canonicalLabel}
    +
    +
    +
    Confirmed truth
    +
    ${caseData.canonicalReveal}
    +
    +
    +
    Archive assessment
    +
    ${routeAssessment}
    +
    +
    +
    Evidence pattern
    +
    ${evidencePattern}
    +
    +
    +
    Route effect
    +
    ${bonusCopy} Interpretation changed the route, not the truth.
    +
    +
    + `; + renderCustody(); +} + +function loadCase() { + caseData = case001; + state = createInitialState(caseData); + render(); +} + +function restartCase({ showOpening = true } = {}) { + state = createInitialState(caseData); + openingOpen = showOpening; + render(); + window.scrollTo({ top: 0, behavior: "smooth" }); +} + +function renderOpeningBrief() { + const opening = caseData.opening; + dom.openingBrief.hidden = !openingOpen; + dom.workspace.hidden = openingOpen; + + if (!opening) { + dom.openingBrief.hidden = true; + dom.workspace.hidden = false; + return; + } + + dom.openingTitle.textContent = opening.headline; + dom.openingSummary.textContent = opening.summary; + dom.openingRole.textContent = opening.role; + dom.openingGoals.innerHTML = opening.goals + .map((goal) => `
  1. ${goal}
  2. `) + .join(""); +} + +function renderArchiveDoctrine() { + dom.doctrineContent.innerHTML = highlightDoctrineForces(caseData.doctrine) + .split("\n\n") + .map((paragraph) => `

    ${paragraph.replace(/\n/g, "
    ")}

    `) + .join(""); +} + +function openReferenceModal(modal, closeButton) { + lastReferenceTrigger = document.activeElement; + modal.hidden = false; + document.body.classList.add("is-reference-open"); + closeButton.focus(); +} + +function closeReferenceModal(modal) { + modal.hidden = true; + document.body.classList.remove("is-reference-open"); + + if (lastReferenceTrigger) { + lastReferenceTrigger.focus(); + } +} + +function renderEvidenceList() { + const { dominantSignal, visibleBonusEvidence } = getCaseProgress(caseData, state); + const visibleEvidence = getRoutedEvidence(caseData, state); + dom.evidenceList.innerHTML = ""; + const routeLabel = + dominantSignal === "Unformed" || dominantSignal === "Mixed" + ? `${visibleEvidence.length} items` + : `${visibleEvidence.length} items / routed by ${dominantSignal}`; + dom.evidenceCount.textContent = routeLabel; + + visibleEvidence.forEach((item, index) => { + const selectedTags = state.tagsByEvidence[item.id] || []; + const button = document.createElement("button"); + button.className = "evidence-card"; + button.type = "button"; + button.classList.toggle("is-active", item.id === state.activeEvidenceId); + button.classList.toggle("is-reviewed", state.reviewedEvidence.has(item.id)); + button.classList.toggle("has-tags", selectedTags.length > 0); + button.classList.toggle("is-bonus", item.isBonus); + button.innerHTML = ` + ${String(index + 1).padStart(2, "0")} / ${item.type} + ${item.title} + ${item.isBonus ? `Surfaced by ${item.surfacedBy}` : state.reviewedEvidence.has(item.id) ? "Reviewed" : item.date} + ${formatTags(selectedTags)} + `; + button.addEventListener("click", () => { + state.activeEvidenceId = item.id; + state.reviewedEvidence.add(item.id); + render(); + }); + dom.evidenceList.append(button); + }); + + if (visibleBonusEvidence.length > 0) { + const routeNote = document.createElement("p"); + routeNote.className = "route-note"; + routeNote.textContent = `Route surfaced: ${visibleBonusEvidence[0].title}. Truth remains stable; the path changed what appeared.`; + dom.evidenceList.append(routeNote); + } +} + +function renderActiveEvidence() { + const active = getActiveEvidence(caseData, state); + if (!active) return; + + dom.evidenceType.textContent = active.type; + dom.evidenceDate.textContent = active.date; + dom.evidenceTitle.textContent = active.title; + dom.evidenceBody.textContent = active.body; + dom.activeTagSummary.textContent = `Tagged signals: ${formatTags( + state.tagsByEvidence[active.id] || [] + )}`; + renderRelatedEvidence(active); +} + +function renderRelatedEvidence(active) { + const relatedItems = (active.relatedEvidence || []) + .map((evidenceId) => getEvidenceById(evidenceId)) + .filter(Boolean); + const contradictions = getReviewedContradictions(active); + + dom.relatedEvidence.innerHTML = ""; + + if (active.recoveryCue) { + const cue = document.createElement("p"); + cue.className = "recovery-cue"; + cue.textContent = active.recoveryCue; + dom.relatedEvidence.append(cue); + } + + if (contradictions.length > 0) { + const marker = document.createElement("p"); + marker.className = "contradiction-marker"; + marker.textContent = `Contradiction marked: compare with ${contradictions + .map((item) => item.title) + .join(" / ")}.`; + dom.relatedEvidence.append(marker); + } + + if (!relatedItems.length) return; + + const group = document.createElement("div"); + group.className = "related-links"; + const label = document.createElement("span"); + label.textContent = "Related evidence"; + group.append(label); + + relatedItems.forEach((item) => { + const button = document.createElement("button"); + button.type = "button"; + button.textContent = item.title; + button.addEventListener("click", () => setActiveEvidence(item.id)); + group.append(button); + }); + + dom.relatedEvidence.append(group); +} + +function renderPuzzle() { + const active = getActiveEvidence(caseData, state); + const puzzle = active?.puzzleId ? findPuzzle(caseData, active.puzzleId) : null; + const isSolved = puzzle ? state.solvedPuzzles.has(puzzle.id) : false; + const attemptCount = puzzle ? state.puzzleAttempts[puzzle.id] || 0 : 0; + + dom.puzzleCard.classList.toggle("is-dormant", !puzzle); + dom.puzzleCard.classList.toggle("is-needed", Boolean(puzzle && !isSolved)); + dom.puzzleCard.classList.toggle("is-solved", Boolean(puzzle && isSolved)); + dom.puzzleCard.setAttribute("aria-disabled", String(!puzzle)); + dom.puzzleTitle.textContent = puzzle ? puzzle.title : "No Puzzle Active"; + dom.puzzlePrompt.textContent = puzzle + ? puzzle.prompt + : "No gate is attached to this artifact. Keep reading and tagging evidence."; + dom.puzzleAnswer.disabled = !puzzle || isSolved; + dom.submitPuzzle.disabled = !puzzle || isSolved; + dom.puzzleAnswer.placeholder = puzzle ? "Enter access code" : "No code needed"; + + if (!puzzle) { + dom.puzzleResult.textContent = state.lastUnlockMessage; + } else if (isSolved) { + dom.puzzleResult.textContent = puzzle.unlockMessage + ? `${puzzle.success} ${puzzle.unlockMessage}` + : puzzle.success; + } else if (attemptCount > 0) { + dom.puzzleResult.textContent = getPuzzleFeedback(puzzle, attemptCount); + } +} + +function renderForceTags() { + const active = getActiveEvidence(caseData, state); + const selected = state.tagsByEvidence[active.id] || []; + dom.forceTags.innerHTML = ""; + dom.selectedTagsStatus.textContent = selected.length + ? `Current read: ${formatTags(selected)}. You can revise it when later evidence changes what this artifact seems to show.` + : "How to decide: ask what the artifact makes visible, hides, copies, protects, prices, or proves. Pick every force you can defend."; + + caseData.forces.forEach((force) => { + const button = document.createElement("button"); + button.type = "button"; + button.textContent = force; + button.setAttribute("aria-pressed", String(selected.includes(force))); + button.classList.toggle("is-selected", selected.includes(force)); + button.addEventListener("click", () => { + toggleForceTag(state, active.id, force); + state.reviewedEvidence.add(active.id); + render(); + }); + dom.forceTags.append(button); + }); +} + +function renderSignalProfile() { + const { profile, dominantSignal, rankedSignals } = getCaseProgress(caseData, state); + dom.dominantSignal.textContent = dominantSignal; + dom.signalProfile.innerHTML = ""; + const maxScore = Math.max(...Object.values(profile)); + + if (maxScore === 0) { + const empty = document.createElement("p"); + empty.className = "signal-empty"; + empty.textContent = "The case has not formed a reading path yet."; + dom.signalProfile.append(empty); + return; + } + + rankedSignals + .filter(([, score]) => score > 0) + .forEach(([force, score]) => { + const row = document.createElement("div"); + row.className = "signal-row"; + row.classList.toggle("is-dominant", force === dominantSignal); + const strength = Math.max(15, Math.round((score / maxScore) * 100)); + const signalLabel = getSignalLabel(score, maxScore, dominantSignal); + row.innerHTML = ` + ${force} + + + + ${signalLabel} + `; + dom.signalProfile.append(row); + }); +} + +function renderTimeline() { + dom.timeline.innerHTML = ""; + getVisibleEvidence(caseData, state) + .filter((item) => state.reviewedEvidence.has(item.id)) + .forEach((item) => { + const line = document.createElement("li"); + line.textContent = item.timeline; + dom.timeline.append(line); + }); +} + +function renderCaseRecap() { + const visibleEvidence = getVisibleEvidence(caseData, state); + const reviewedEvidence = visibleEvidence.filter((item) => state.reviewedEvidence.has(item.id)); + const { dominantSignal, visibleBonusEvidence } = getCaseProgress(caseData, state); + const gateSolved = state.solvedPuzzles.has("origin-code"); + const contradictionCount = reviewedEvidence.filter((item) => { + return getReviewedContradictions(item).length > 0; + }).length; + + dom.caseRecap.innerHTML = ` +

    ${reviewedEvidence.length}/${visibleEvidence.length} evidence items reviewed. Gate ${gateSolved ? "opened" : "still locked"}. Path ${dominantSignal}.

    +

    ${contradictionCount ? `${contradictionCount} contradiction marker${contradictionCount === 1 ? "" : "s"} active.` : "No reviewed contradictions marked yet."}

    +

    ${visibleBonusEvidence.length ? `Bonus evidence surfaced: ${visibleBonusEvidence[0].title}.` : "No bonus evidence surfaced yet."}

    +

    Reconstruction names what happened. Custody unlocks after the truth is shown.

    + `; +} + +function renderReadingOptions() { + const existingSelection = document.querySelector("input[name='final-reading']:checked")?.value; + dom.readingOptions.innerHTML = "What happened?"; + + caseData.readings.forEach((reading) => { + const label = document.createElement("label"); + label.innerHTML = ` + + ${reading.label} + `; + dom.readingOptions.append(label); + }); +} + +function renderHeader() { + dom.caseId.textContent = caseData.id; + dom.caseTitle.textContent = caseData.title; +} + +function renderIntakeBrief() { + const intake = caseData.intake; + + dom.intakeContent.innerHTML = ` +
    +

    ${intake.heading}

    +

    ${intake.summary}

    +
    +
      + ${intake.steps + .map( + (step) => ` +
    1. + ${step.label} + ${step.text} +
    2. + ` + ) + .join("")} +
    +

    ${intake.reminder}

    + `; +} + +function renderCustody() { + const choices = caseData.custodyChoices || []; + dom.custodyCard.hidden = !state.submittedReading; + + if (!state.submittedReading) return; + + dom.custodyPrompt.textContent = caseData.custodyPrompt; + dom.custodyOptions.innerHTML = ""; + + choices.forEach((choice) => { + const button = document.createElement("button"); + button.type = "button"; + button.className = "custody-option"; + button.classList.toggle("is-selected", state.selectedCustody === choice.id); + button.innerHTML = ` + ${choice.label} + ${choice.summary} + `; + button.addEventListener("click", () => { + state.selectedCustody = choice.id; + render(); + }); + dom.custodyOptions.append(button); + }); + + const selectedChoice = choices.find((choice) => choice.id === state.selectedCustody); + dom.custodyResult.textContent = selectedChoice + ? selectedChoice.consequence + : "Choose custody after reconstruction: expose the truth, protect the person, or preserve the record."; +} + +function renderCastBoard() { + const cast = caseData.cast || []; + let updatedCount = 0; + dom.castBoard.innerHTML = ""; + + cast.forEach((person) => { + const revealed = person.reveals.filter((reveal) => state.reviewedEvidence.has(reveal.evidenceId)); + if (revealed.length) updatedCount += 1; + + const article = document.createElement("article"); + article.className = "cast-card"; + article.classList.toggle("is-updated", revealed.length > 0); + article.innerHTML = ` +
    + ${person.name} + ${revealed.length ? "Updated" : "Intake"} +
    +

    ${person.role}

    +

    ${person.known}

    +

    ${person.question}

    +
    + ${ + revealed.length + ? revealed + .map( + (reveal) => ` +

    + ${reveal.label} + ${reveal.detail} +

    + ` + ) + .join("") + : "

    UnreadMore context will appear as related evidence is reviewed.

    " + } +
    + `; + dom.castBoard.append(article); + }); + + dom.castProgress.textContent = `${updatedCount}/${cast.length} updated`; +} + +function render() { + ensureActiveEvidence(); + renderHeader(); + renderOpeningBrief(); + renderArchiveDoctrine(); + renderIntakeBrief(); + renderEvidenceList(); + renderActiveEvidence(); + renderPuzzle(); + renderForceTags(); + renderSignalProfile(); + renderCastBoard(); + renderTimeline(); + renderCaseRecap(); + renderReadingOptions(); + if (state.submittedReading) { + renderReveal(state.submittedReading); + } else { + dom.revealCard.hidden = true; + dom.custodyCard.hidden = true; + } +} + +dom.submitPuzzle.addEventListener("click", () => { + const active = getActiveEvidence(caseData, state); + const puzzle = active?.puzzleId ? findPuzzle(caseData, active.puzzleId) : null; + + if (!puzzle) return; + + if (checkPuzzleAnswer(puzzle, dom.puzzleAnswer.value)) { + const beforeVisible = getVisibleEvidence(caseData, state); + state.solvedPuzzles.add(puzzle.id); + dom.puzzleAnswer.value = ""; + const newlyUnlocked = getNewlyUnlockedEvidence(beforeVisible, getVisibleEvidence(caseData, state)); + if (newlyUnlocked) { + state.activeEvidenceId = newlyUnlocked.id; + state.reviewedEvidence.add(newlyUnlocked.id); + } + state.lastUnlockMessage = puzzle.unlockMessage + ? `${puzzle.success} ${puzzle.unlockMessage}` + : puzzle.success; + render(); + return; + } + + const attemptCount = recordPuzzleAttempt(state, puzzle.id); + dom.puzzleResult.textContent = getPuzzleFeedback(puzzle, attemptCount); + render(); +}); + +dom.submitReading.addEventListener("click", () => { + const selected = document.querySelector("input[name='final-reading']:checked"); + if (!selected) { + dom.readingResult.textContent = "Choose a final reading before submitting."; + return; + } + + renderReveal(selected.value); +}); + +dom.startCase.addEventListener("click", () => { + openingOpen = false; + render(); +}); + +dom.resetCase.addEventListener("click", () => { + restartCase(); +}); + +dom.openMapButtons.forEach((button) => { + button.addEventListener("click", () => openReferenceModal(dom.systemMapModal, dom.closeSystemMap)); +}); + +dom.openIntakeButtons.forEach((button) => { + button.addEventListener("click", () => openReferenceModal(dom.intakeModal, dom.closeIntake)); +}); + +dom.openDoctrineButtons.forEach((button) => { + button.addEventListener("click", () => openReferenceModal(dom.doctrineModal, dom.closeDoctrine)); +}); + +dom.closeSystemMap.addEventListener("click", () => closeReferenceModal(dom.systemMapModal)); +dom.closeIntake.addEventListener("click", () => closeReferenceModal(dom.intakeModal)); +dom.closeDoctrine.addEventListener("click", () => closeReferenceModal(dom.doctrineModal)); + +function closeOnBackdrop(event) { + if (event.target === event.currentTarget) { + closeReferenceModal(event.currentTarget); + } +} + +dom.systemMapModal.addEventListener("click", closeOnBackdrop); +dom.intakeModal.addEventListener("click", closeOnBackdrop); +dom.doctrineModal.addEventListener("click", closeOnBackdrop); + +document.addEventListener("keydown", (event) => { + if (event.key === "Escape" && !dom.systemMapModal.hidden) { + closeReferenceModal(dom.systemMapModal); + } + + if (event.key === "Escape" && !dom.intakeModal.hidden) { + closeReferenceModal(dom.intakeModal); + } + + if (event.key === "Escape" && !dom.doctrineModal.hidden) { + closeReferenceModal(dom.doctrineModal); + } +}); + +loadCase(); diff --git a/Case Box 001 - The Creator Who Left But Kept Posting/src/scripts/case-engine.js b/Case Box 001 - The Creator Who Left But Kept Posting/src/scripts/case-engine.js new file mode 100644 index 0000000..a22f82c --- /dev/null +++ b/Case Box 001 - The Creator Who Left But Kept Posting/src/scripts/case-engine.js @@ -0,0 +1,82 @@ +import { isEvidenceUnlocked } from "./puzzle-engine.js"; +import { calculateSignalProfile, getDominantSignal, getRankedSignals } from "./signal-engine.js"; + +export function createInitialState(caseData) { + return { + activeEvidenceId: caseData.evidence[0]?.id || null, + reviewedEvidence: new Set(caseData.evidence[0] ? [caseData.evidence[0].id] : []), + solvedPuzzles: new Set(), + puzzleAttempts: {}, + lastUnlockMessage: "", + tagsByEvidence: {}, + submittedReading: null, + selectedCustody: null + }; +} + +export function getVisibleEvidence(caseData, state) { + return [ + ...caseData.evidence.filter((item) => isEvidenceUnlocked(item, state.solvedPuzzles)), + ...getVisibleBonusEvidence(caseData, state) + ]; +} + +export function getVisibleBonusEvidence(caseData, state) { + const bonusEvidence = caseData.bonusEvidence || {}; + const profile = calculateSignalProfile(caseData, state.tagsByEvidence); + const dominantSignal = getDominantSignal(profile); + + if (dominantSignal === "Unformed" || dominantSignal === "Mixed" || profile[dominantSignal] < 2) { + return []; + } + + const bonus = bonusEvidence[dominantSignal]; + return bonus ? [{ ...bonus, isBonus: true, surfacedBy: dominantSignal }] : []; +} + +export function getRoutedEvidence(caseData, state) { + const visibleEvidence = getVisibleEvidence(caseData, state); + const profile = calculateSignalProfile(caseData, state.tagsByEvidence); + const dominantSignal = getDominantSignal(profile); + + if (dominantSignal === "Unformed" || dominantSignal === "Mixed") { + return visibleEvidence; + } + + return visibleEvidence + .map((item, index) => ({ item, index })) + .sort((left, right) => { + const leftMatch = left.item.expectedForces?.includes(dominantSignal) ? 1 : 0; + const rightMatch = right.item.expectedForces?.includes(dominantSignal) ? 1 : 0; + return rightMatch - leftMatch || left.index - right.index; + }) + .map(({ item }) => item); +} + +export function getActiveEvidence(caseData, state) { + return getVisibleEvidence(caseData, state).find((item) => item.id === state.activeEvidenceId); +} + +export function toggleForceTag(state, evidenceId, force) { + const current = state.tagsByEvidence[evidenceId] || []; + state.tagsByEvidence[evidenceId] = current.includes(force) + ? current.filter((item) => item !== force) + : [...current, force]; +} + +export function recordPuzzleAttempt(state, puzzleId) { + state.puzzleAttempts[puzzleId] = (state.puzzleAttempts[puzzleId] || 0) + 1; + return state.puzzleAttempts[puzzleId]; +} + +export function getCaseProgress(caseData, state) { + const profile = calculateSignalProfile(caseData, state.tagsByEvidence); + return { + profile, + dominantSignal: getDominantSignal(profile), + rankedSignals: getRankedSignals(profile), + visibleEvidence: getVisibleEvidence(caseData, state), + visibleBonusEvidence: getVisibleBonusEvidence(caseData, state), + routedEvidence: getRoutedEvidence(caseData, state) + }; +} diff --git a/Case Box 001 - The Creator Who Left But Kept Posting/src/scripts/puzzle-engine.js b/Case Box 001 - The Creator Who Left But Kept Posting/src/scripts/puzzle-engine.js new file mode 100644 index 0000000..2fe7dc3 --- /dev/null +++ b/Case Box 001 - The Creator Who Left But Kept Posting/src/scripts/puzzle-engine.js @@ -0,0 +1,25 @@ +export function normalizeAnswer(value) { + return value.trim().toLowerCase(); +} + +export function findPuzzle(caseData, puzzleId) { + return caseData.puzzles.find((puzzle) => puzzle.id === puzzleId); +} + +export function checkPuzzleAnswer(puzzle, answer) { + if (!puzzle) return false; + const acceptedAnswers = [puzzle.answer, ...(puzzle.acceptedAnswers || [])]; + return acceptedAnswers.some((acceptedAnswer) => { + return normalizeAnswer(answer) === normalizeAnswer(acceptedAnswer); + }); +} + +export function isEvidenceUnlocked(evidence, solvedPuzzles) { + if (!evidence.unlockCondition) return true; + + if (evidence.unlockCondition.type === "puzzleSolved") { + return solvedPuzzles.has(evidence.unlockCondition.puzzleId); + } + + return true; +} diff --git a/Case Box 001 - The Creator Who Left But Kept Posting/src/scripts/signal-engine.js b/Case Box 001 - The Creator Who Left But Kept Posting/src/scripts/signal-engine.js new file mode 100644 index 0000000..7bed1cb --- /dev/null +++ b/Case Box 001 - The Creator Who Left But Kept Posting/src/scripts/signal-engine.js @@ -0,0 +1,38 @@ +export function createSignalProfile(forces) { + return Object.fromEntries(forces.map((force) => [force, 0])); +} + +export function calculateSignalProfile(caseData, tagsByEvidence) { + const profile = createSignalProfile(caseData.forces); + + Object.values(tagsByEvidence).forEach((tags) => { + tags.forEach((tag) => { + if (profile[tag] !== undefined) { + profile[tag] += 1; + } + }); + }); + + return profile; +} + +export function getDominantSignal(profile) { + const entries = getRankedSignals(profile); + const [force, score] = entries[0] || ["Default", 0]; + + if (score <= 0) return "Unformed"; + + const tiedSignals = entries.filter(([, value]) => value === score); + return tiedSignals.length > 1 ? "Mixed" : force; +} + +export function getRankedSignals(profile) { + return Object.entries(profile).sort((a, b) => { + if (b[1] !== a[1]) return b[1] - a[1]; + return a[0].localeCompare(b[0]); + }); +} + +export function getReadingPath(caseData, dominantSignal) { + return caseData.pathReveals[dominantSignal] || caseData.pathReveals.Default; +} diff --git a/Case Box 001 - The Creator Who Left But Kept Posting/src/styles/base.css b/Case Box 001 - The Creator Who Left But Kept Posting/src/styles/base.css new file mode 100644 index 0000000..46eceaf --- /dev/null +++ b/Case Box 001 - The Creator Who Left But Kept Posting/src/styles/base.css @@ -0,0 +1,67 @@ +:root { + --black: #0d0d0c; + --charcoal: #151513; + --panel: #201e19; + --panel-deep: #12110f; + --paper-deep: #16130e; + --paper: #ede7d9; + --paper-soft: #d8cfbd; + --muted: #918875; + --line: rgba(237, 231, 217, 0.18); + --line-strong: rgba(237, 231, 217, 0.34); + --red: #b14332; + --green: #78a162; + --amber: #c79b55; + --bluegray: #87929a; + --mono: "Courier New", Courier, monospace; + --sans: Arial, Helvetica, sans-serif; + --serif: Georgia, "Times New Roman", serif; +} + +* { + box-sizing: border-box; +} + +[hidden] { + display: none !important; +} + +body { + background: + linear-gradient(90deg, rgba(177, 67, 50, 0.08), transparent 34%), + linear-gradient(rgba(237, 231, 217, 0.035) 1px, transparent 1px), + linear-gradient(90deg, rgba(237, 231, 217, 0.025) 1px, transparent 1px), + linear-gradient(145deg, #090908, #18140f 48%, #101419); + background-size: auto, 34px 34px, 34px 34px, auto; + color: var(--paper); + font-family: var(--sans); + margin: 0; +} + +button, +input, +a { + color: inherit; + font: inherit; +} + +a { + text-decoration: none; +} + +button { + cursor: pointer; +} + +button:focus-visible, +input:focus-visible, +a:focus-visible { + outline: 2px solid var(--amber); + outline-offset: 3px; +} + +button:disabled, +input:disabled { + cursor: not-allowed; + opacity: 0.55; +} diff --git a/Case Box 001 - The Creator Who Left But Kept Posting/src/styles/components.css b/Case Box 001 - The Creator Who Left But Kept Posting/src/styles/components.css new file mode 100644 index 0000000..5d2c13c --- /dev/null +++ b/Case Box 001 - The Creator Who Left But Kept Posting/src/styles/components.css @@ -0,0 +1,1207 @@ +.brand strong, +.brand em, +.case-title, +.signal-summary, +.kicker, +.evidence-card, +.document-meta, +.puzzle-card, +.tag-card, +.force-tags, +.signal-profile, +.timeline, +.reading-options, +.status-line, +.reading-path, +.panel-topline, +.case-actions, +.reset-action, +.map-action, +.text-action, +.opening-actions, +.opening-doctrine, +.doctrine-summary, +.custody-prompt, +.custody-options, +.system-map-modal { + font-family: var(--mono); + letter-spacing: 0; +} + +.brand strong { + display: block; + font-size: 14px; + letter-spacing: 0.04em; +} + +.brand em { + color: var(--green); + display: block; + font-size: 11px; + font-style: normal; + margin-top: 4px; +} + +.brand-mark { + color: var(--paper); + display: block; + flex: 0 0 auto; + height: 48px; + width: 48px; +} + +.brand-mark svg { + display: block; + height: 100%; + overflow: visible; + width: 100%; +} + +.brand-mark path, +.brand-mark circle, +.brand-mark rect { + fill: none; + stroke: currentColor; + stroke-linejoin: miter; +} + +.brand-mark-circle { + opacity: 0.86; + stroke-width: 2; +} + +.brand-mark-square { + color: var(--amber); + stroke-width: 2; +} + +.brand-mark-triangle { + fill: var(--green) !important; + stroke: 0; +} + +.case-title span, +.signal-summary span, +.panel-topline span { + color: var(--muted); + font-size: 11px; +} + +.case-title strong, +.signal-summary strong { + color: var(--paper-soft); + font-size: 14px; + font-weight: 400; + margin-top: 6px; +} + +.case-actions { + display: flex; + flex-wrap: wrap; + gap: 8px; + justify-content: center; + margin-top: 10px; +} + +.map-action, +.text-action { + background: transparent; + border: 1px solid rgba(120, 161, 98, 0.26); + color: var(--green); + font-size: 10px; + min-height: 28px; + padding: 5px 9px; + text-transform: uppercase; + transition: + border-color 0.16s ease, + color 0.16s ease; +} + +.map-action { + margin-top: 0; +} + +.text-action { + border-color: rgba(237, 231, 217, 0.16); + color: var(--muted); +} + +.map-action:hover, +.text-action:hover { + border-color: var(--amber); + color: var(--amber); +} + +.doctrine-heading { + align-items: center; + display: flex; + gap: 12px; + justify-content: space-between; +} + +.doctrine-heading .kicker { + margin-bottom: 0; +} + +.doctrine-force { + color: var(--amber); + font-weight: 400; +} + +body.is-reference-open { + overflow: hidden; +} + +.reset-action { + background: transparent; + border: 1px solid rgba(237, 231, 217, 0.18); + color: var(--muted); + font-size: 10px; + margin-top: 10px; + min-height: 28px; + padding: 5px 9px; + text-transform: uppercase; + transition: + border-color 0.16s ease, + color 0.16s ease; +} + +.reset-action:hover { + border-color: var(--amber); + color: var(--amber); +} + +.panel-topline { + align-items: center; + display: flex; + justify-content: space-between; +} + +.kicker { + color: var(--red); + font-size: 12px; + margin: 0 0 14px; + text-transform: uppercase; +} + +.evidence-list { + display: grid; + gap: 10px; +} + +.trail-card { + border-top: 1px solid rgba(199, 155, 85, 0.18); + margin-top: 16px; + padding-top: 14px; +} + +.evidence-card { + background: + linear-gradient(90deg, rgba(199, 155, 85, 0.08), transparent 4px), + rgba(28, 24, 18, 0.48); + border: 1px solid var(--line); + display: grid; + gap: 7px; + min-height: 82px; + padding: 12px; + text-align: left; + text-transform: uppercase; + transition: + background-color 0.18s ease, + border-color 0.18s ease, + transform 0.18s ease; +} + +.evidence-card:hover, +.evidence-card.is-active { + border-color: var(--green); +} + +.evidence-card:hover { + transform: translateX(3px); +} + +.evidence-card.is-bonus { + border-color: var(--amber); + box-shadow: inset 0 0 0 1px rgba(214, 177, 90, 0.22); +} + +.evidence-card.is-reviewed { + background: rgba(120, 161, 98, 0.08); +} + +.evidence-card span, +.evidence-card small, +.evidence-card em { + color: var(--muted); + font-size: 10px; +} + +.evidence-card em { + font-style: normal; + line-height: 1.35; +} + +.evidence-card strong { + color: var(--paper-soft); + font-size: 12px; + font-weight: 400; + line-height: 1.35; +} + +.evidence-card.has-tags em { + color: var(--amber); +} + +.route-note { + border: 1px solid var(--line); + color: var(--amber); + font-family: var(--mono); + font-size: 11px; + line-height: 1.45; + margin: 2px 0 0; + padding: 12px; + text-transform: uppercase; +} + +.opening-copy h1 { + color: var(--paper); + font-family: var(--serif); + font-size: clamp(42px, 6vw, 76px); + font-weight: 400; + line-height: 0.95; + margin: 0 0 24px; + max-width: 780px; + text-transform: uppercase; +} + +.opening-copy p { + color: var(--paper-soft); + font-size: 18px; + line-height: 1.6; + margin: 0; + max-width: 700px; +} + +.opening-copy .opening-role { + border-left: 3px solid var(--green); + color: var(--paper); + font-family: var(--mono); + font-size: 12px; + margin-top: 22px; + padding-left: 14px; + text-transform: uppercase; +} + +.opening-doctrine { + border-top: 1px solid rgba(237, 231, 217, 0.14); + margin: 2px 0 16px; + padding-top: 12px; +} + +.opening-doctrine .kicker { + margin-bottom: 8px; +} + +.opening-doctrine p:not(.kicker) { + color: var(--paper-soft); + font-size: 11px; + line-height: 1.5; +} + +.opening-actions ol { + counter-reset: opening-goal; + display: grid; + gap: 10px; + list-style: none; + margin: 0 0 18px; + padding: 0; +} + +.opening-actions li { + border-top: 1px solid rgba(237, 231, 217, 0.13); + color: var(--paper-soft); + counter-increment: opening-goal; + font-size: 12px; + line-height: 1.45; + padding: 12px 0 0 34px; + position: relative; + text-transform: uppercase; +} + +.opening-actions li::before { + color: var(--amber); + content: counter(opening-goal, decimal-leading-zero); + left: 0; + position: absolute; + top: 12px; +} + +.document-card, +.puzzle-card, +.tag-card, +.board-card { + background: + linear-gradient(145deg, rgba(237, 231, 217, 0.06), transparent 36%), + rgba(28, 27, 24, 0.78); + border: 1px solid var(--line); + padding: 18px; +} + +.document-card { + background: + linear-gradient(90deg, rgba(177, 67, 50, 0.1), transparent 6px), + linear-gradient(180deg, rgba(237, 231, 217, 0.08), transparent 46%), + rgba(22, 19, 14, 0.94); + border-color: rgba(199, 155, 85, 0.26); + box-shadow: + 0 22px 60px rgba(0, 0, 0, 0.35), + inset 0 1px 0 rgba(237, 231, 217, 0.08); + min-height: 320px; + padding: 26px; + position: relative; +} + +.document-card::after { + border: 1px solid rgba(199, 155, 85, 0.22); + color: rgba(199, 155, 85, 0.7); + content: "CASE COPY"; + font-family: var(--mono); + font-size: 10px; + letter-spacing: 0.12em; + padding: 6px 8px; + position: absolute; + right: 22px; + top: 56px; + transform: rotate(4deg); +} + +.document-meta { + color: var(--muted); + display: flex; + font-size: 11px; + justify-content: space-between; + text-transform: uppercase; +} + +.document-card h1 { + color: var(--paper); + font-family: var(--serif); + font-size: 42px; + font-weight: 400; + line-height: 1.04; + margin: 34px 0 22px; + text-transform: uppercase; +} + +.document-card p { + color: var(--paper-soft); + font-size: 17px; + line-height: 1.65; + max-width: 760px; + white-space: pre-line; +} + +.document-card .active-tag-summary { + border-top: 1px solid var(--line); + color: var(--amber); + font-family: var(--mono); + font-size: 11px; + line-height: 1.45; + margin: 26px 0 0; + padding-top: 14px; + text-transform: uppercase; +} + +.related-evidence { + display: grid; + gap: 10px; + margin-top: 14px; +} + +.intake-brief { + background: + linear-gradient(90deg, rgba(120, 161, 98, 0.1), transparent 5px), + rgba(16, 18, 15, 0.78); + border: 1px solid rgba(120, 161, 98, 0.28); +} + +.intake-toggle { + align-items: center; + background: transparent; + border: 0; + border-bottom: 1px solid rgba(120, 161, 98, 0.22); + display: flex; + justify-content: space-between; + min-height: 42px; + padding: 10px 14px; + text-transform: uppercase; + width: 100%; +} + +.intake-toggle span, +.intake-toggle strong, +.board-card-header, +.cast-card, +.cast-reveals { + font-family: var(--mono); + letter-spacing: 0; +} + +.intake-toggle span { + color: var(--green); + font-size: 11px; +} + +.intake-toggle strong { + color: var(--paper-soft); + font-size: 11px; + font-weight: 400; +} + +.intake-content { + display: grid; + gap: 14px; + grid-template-columns: minmax(220px, 0.9fr) minmax(300px, 1.1fr); + padding: 16px; +} + +.intake-content[hidden] { + display: none; +} + +.intake-content h2 { + color: var(--paper); + font-family: var(--serif); + font-size: 28px; + font-weight: 400; + line-height: 1.1; + margin: 0 0 10px; + text-transform: uppercase; +} + +.intake-content p, +.intake-content li span { + color: var(--paper-soft); + font-size: 13px; + line-height: 1.5; + margin: 0; +} + +.intake-content ol { + display: grid; + gap: 8px; + grid-template-columns: repeat(2, minmax(0, 1fr)); + list-style: none; + margin: 0; + padding: 0; +} + +.intake-content li { + border: 1px solid rgba(237, 231, 217, 0.12); + display: grid; + gap: 5px; + min-height: 74px; + padding: 10px; +} + +.intake-content li strong { + color: var(--amber); + font-family: var(--mono); + font-size: 11px; + font-weight: 400; + text-transform: uppercase; +} + +.intake-reminder { + border-top: 1px solid rgba(120, 161, 98, 0.2); + color: var(--green) !important; + font-family: var(--mono); + font-size: 11px !important; + grid-column: 1 / -1; + padding-top: 12px; + text-transform: uppercase; +} + +.recovery-cue, +.contradiction-marker, +.case-recap p { + color: var(--paper-soft); + font-family: var(--mono); + font-size: 11px; + line-height: 1.45; + margin: 0; + text-transform: uppercase; +} + +.recovery-cue { + color: var(--muted); +} + +.contradiction-marker { + border-left: 3px solid var(--red); + color: var(--paper); + padding-left: 10px; +} + +.related-links { + align-items: center; + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.related-links span { + color: var(--amber); + font-family: var(--mono); + font-size: 10px; + text-transform: uppercase; +} + +.related-links button { + background: rgba(13, 13, 12, 0.55); + border: 1px solid var(--line); + color: var(--paper-soft); + font-family: var(--mono); + font-size: 10px; + min-height: 30px; + padding: 6px 9px; + text-transform: uppercase; +} + +.related-links button:hover { + border-color: var(--amber); + color: var(--amber); +} + +.puzzle-card, +.tag-card { + align-items: center; + display: grid; + gap: 18px; + grid-template-columns: minmax(220px, 0.8fr) 1.2fr; +} + +.puzzle-card { + border-color: rgba(177, 67, 50, 0.34); + transition: + background-color 0.18s ease, + border-color 0.18s ease, + box-shadow 0.18s ease, + opacity 0.18s ease; +} + +.puzzle-card.is-dormant { + background: + linear-gradient(145deg, rgba(237, 231, 217, 0.025), transparent 36%), + rgba(18, 18, 17, 0.54); + border-color: rgba(237, 231, 217, 0.1); + opacity: 0.62; +} + +.puzzle-card.is-dormant h2, +.puzzle-card.is-dormant p:not(.kicker) { + color: var(--muted); +} + +.puzzle-card.is-needed { + background: + linear-gradient(90deg, rgba(177, 67, 50, 0.16), transparent 7px), + linear-gradient(145deg, rgba(199, 155, 85, 0.08), transparent 36%), + rgba(28, 24, 18, 0.84); + border-color: var(--amber); + box-shadow: + inset 0 0 0 1px rgba(199, 155, 85, 0.2), + 0 0 32px rgba(177, 67, 50, 0.1); +} + +.puzzle-card.is-needed .kicker { + color: var(--amber); +} + +.puzzle-card.is-solved { + border-color: rgba(120, 161, 98, 0.42); + box-shadow: inset 3px 0 0 rgba(120, 161, 98, 0.24); +} + +.tag-card { + border-color: rgba(120, 161, 98, 0.28); +} + +.puzzle-card h2, +.tag-card h2 { + font-family: var(--serif); + font-size: 26px; + font-weight: 400; + line-height: 1.1; + margin: 0; + text-transform: uppercase; +} + +.puzzle-card p:not(.kicker) { + color: var(--paper-soft); + font-size: 12px; + line-height: 1.5; +} + +.puzzle-controls { + display: grid; + gap: 8px; + grid-template-columns: 1fr auto; +} + +.puzzle-controls input { + background: rgba(13, 13, 12, 0.72); + border: 1px solid var(--line); + min-height: 40px; + padding: 10px; +} + +.puzzle-controls input:disabled, +.puzzle-controls button:disabled { + cursor: not-allowed; + opacity: 0.46; +} + +.puzzle-controls button, +.primary-action { + background: var(--paper); + border: 0; + color: var(--black); + min-height: 40px; + padding: 10px 14px; + text-transform: uppercase; +} + +.force-tags { + display: grid; + gap: 8px; + grid-template-columns: repeat(auto-fit, minmax(96px, 1fr)); +} + +.selected-tags-status { + color: var(--paper-soft); + font-family: var(--mono); + font-size: 11px; + line-height: 1.45; + margin: 0 0 12px; + text-transform: uppercase; +} + +.force-tags button { + background: transparent; + border: 1px solid var(--line); + border-radius: 4px; + font-size: 11px; + min-height: 36px; + padding: 8px 10px; + text-align: center; + text-transform: uppercase; + transition: + background-color 0.16s ease, + border-color 0.16s ease, + color 0.16s ease; +} + +.force-tags button.is-selected { + background: rgba(199, 155, 85, 0.1); + border-color: var(--amber); + color: var(--amber); +} + +.board-card h2 { + font-family: var(--serif); +} + +.board-card-header { + align-items: center; + display: flex; + justify-content: space-between; +} + +.board-card-header .kicker { + margin-bottom: 0; +} + +.board-card-header span { + color: var(--muted); + font-size: 10px; + text-transform: uppercase; +} + +.board-card { + box-shadow: inset 3px 0 0 rgba(199, 155, 85, 0.12); +} + +.cast-board-card { + box-shadow: inset 3px 0 0 rgba(120, 161, 98, 0.14); +} + +.cast-board { + display: grid; + gap: 10px; + margin-top: 14px; +} + +.active-panel .cast-board { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.cast-card { + background: rgba(13, 13, 12, 0.42); + border: 1px solid rgba(237, 231, 217, 0.14); + display: grid; + gap: 8px; + padding: 11px; +} + +.cast-card.is-updated { + border-color: rgba(120, 161, 98, 0.36); +} + +.cast-card-topline { + align-items: start; + display: flex; + gap: 10px; + justify-content: space-between; +} + +.cast-card-topline strong { + color: var(--paper); + font-size: 12px; + font-weight: 400; + line-height: 1.25; + text-transform: uppercase; +} + +.cast-card-topline span { + color: var(--green); + font-size: 10px; + text-transform: uppercase; +} + +.cast-card p { + color: var(--paper-soft); + font-size: 11px; + line-height: 1.45; + margin: 0; +} + +.cast-role { + color: var(--amber) !important; + font-size: 10px !important; + text-transform: uppercase; +} + +.cast-question { + color: var(--muted) !important; +} + +.cast-reveals { + border-top: 1px solid rgba(237, 231, 217, 0.12); + display: grid; + gap: 8px; + padding-top: 8px; +} + +.cast-reveals p { + display: grid; + gap: 3px; +} + +.cast-reveals strong { + color: var(--green); + font-size: 10px; + font-weight: 400; + text-transform: uppercase; +} + +.cast-reveals span { + color: var(--paper-soft); +} + +.signal-profile { + display: grid; + gap: 8px; +} + +.signal-row { + align-items: center; + display: grid; + gap: 8px; + grid-template-columns: 92px 1fr 72px; +} + +.signal-row span, +.signal-row strong { + color: var(--paper-soft); + font-size: 11px; + font-weight: 400; +} + +.signal-row i { + background: rgba(232, 225, 205, 0.08); + border: 1px solid var(--line); + display: block; + height: 8px; + overflow: hidden; +} + +.signal-row b { + background: var(--amber); + display: block; + height: 100%; +} + +.signal-row.is-dominant span, +.signal-row.is-dominant strong { + color: var(--amber); +} + +.signal-empty { + color: var(--muted); + font-size: 12px; + line-height: 1.45; + margin: 0; +} + +.doctrine-card { + border-color: rgba(120, 161, 98, 0.22); +} + +.doctrine-summary { + color: var(--paper-soft); + font-size: 11px; + line-height: 1.55; + margin: 0; +} + +.system-map-modal { + align-items: center; + background: + radial-gradient(circle at 50% 12%, rgba(120, 161, 98, 0.12), transparent 34%), + rgba(4, 4, 3, 0.88); + inset: 0; + justify-content: center; + padding: 24px; + position: fixed; + z-index: 30; +} + +.system-map-modal:not([hidden]) { + display: flex; +} + +.system-map-panel { + background: + linear-gradient(145deg, rgba(237, 231, 217, 0.07), transparent 42%), + rgba(15, 14, 12, 0.98); + border: 1px solid rgba(199, 155, 85, 0.34); + box-shadow: 0 28px 90px rgba(0, 0, 0, 0.58); + max-height: min(92vh, 980px); + max-width: min(1180px, 94vw); + overflow: auto; + padding: 18px; + width: 100%; +} + +.intake-panel { + max-width: min(860px, 94vw); +} + +.doctrine-panel { + max-width: min(920px, 94vw); +} + +.doctrine-content { + display: grid; + gap: 16px; +} + +.doctrine-content p { + border-top: 1px solid rgba(237, 231, 217, 0.1); + color: var(--paper-soft); + font-family: var(--mono); + font-size: 13px; + line-height: 1.65; + margin: 0; + padding-top: 16px; + text-transform: uppercase; +} + +.doctrine-content p:first-child { + border-top: 0; + color: var(--paper); + font-family: var(--serif); + font-size: clamp(26px, 4vw, 42px); + line-height: 1.08; + padding-top: 0; +} + +.doctrine-content p:last-child { + border-color: rgba(199, 155, 85, 0.26); + color: var(--amber); + font-size: 15px; +} + +.system-map-header { + align-items: start; + border-bottom: 1px solid rgba(237, 231, 217, 0.13); + display: flex; + gap: 16px; + justify-content: space-between; + margin-bottom: 14px; + padding-bottom: 12px; +} + +.system-map-header h2 { + color: var(--paper); + font-family: var(--serif); + font-size: clamp(30px, 4vw, 52px); + font-weight: 400; + line-height: 1; + margin: 0; + text-transform: uppercase; +} + +.system-map-header .reset-action { + margin-top: 0; +} + +.system-map-figure { + display: grid; + gap: 12px; + margin: 0; +} + +.system-map-figure img { + background: rgba(5, 5, 4, 0.64); + border: 1px solid rgba(237, 231, 217, 0.14); + display: block; + max-height: 68vh; + object-fit: contain; + width: 100%; +} + +.system-map-figure figcaption { + color: var(--muted); + font-size: 11px; + line-height: 1.45; + margin: 0; + text-transform: uppercase; +} + +.timeline { + color: var(--paper-soft); + display: grid; + gap: 12px; + font-size: 12px; + line-height: 1.45; + margin: 12px 0 0; + padding-left: 20px; +} + +.reading-options { + border: 0; + display: grid; + gap: 10px; + margin: 10px 0 16px; + padding: 0; +} + +.case-recap { + border: 1px solid rgba(199, 155, 85, 0.18); + display: grid; + gap: 8px; + margin-bottom: 14px; + padding: 12px; +} + +.reading-options legend { + color: var(--muted); + font-size: 11px; + margin-bottom: 10px; + text-transform: uppercase; +} + +.reading-options label { + border: 1px solid var(--line); + color: var(--paper-soft); + display: grid; + font-size: 12px; + gap: 8px; + grid-template-columns: 18px 1fr; + line-height: 1.35; + padding: 10px; +} + +.primary-action { + width: 100%; +} + +.status-line, +.reading-path { + color: var(--paper-soft); + font-size: 12px; + line-height: 1.5; + min-height: 34px; +} + +.reveal-card { + border-color: var(--amber); +} + +.reveal-card[hidden], +.custody-card[hidden] { + display: none; +} + +.custody-card { + border-color: rgba(120, 161, 98, 0.36); +} + +.custody-prompt { + color: var(--paper-soft); + font-size: 12px; + line-height: 1.5; + margin: 0 0 12px; +} + +.custody-options { + display: grid; + gap: 10px; +} + +.custody-option { + background: rgba(13, 13, 12, 0.38); + border: 1px solid var(--line); + color: var(--paper-soft); + display: grid; + gap: 5px; + padding: 11px; + text-align: left; + transition: + background-color 0.16s ease, + border-color 0.16s ease, + color 0.16s ease; +} + +.custody-option:hover, +.custody-option.is-selected { + border-color: var(--green); +} + +.custody-option.is-selected { + background: rgba(120, 161, 98, 0.1); +} + +.custody-option strong { + color: var(--paper); + font-size: 12px; + font-weight: 400; + text-transform: uppercase; +} + +.custody-option span { + color: var(--muted); + font-size: 11px; + line-height: 1.4; +} + +.reveal-content dl { + display: grid; + gap: 14px; + margin: 0; +} + +.reveal-content div { + border-top: 1px solid var(--line); + padding-top: 12px; +} + +.reveal-content div:first-child { + border-top: 0; + padding-top: 0; +} + +.reveal-content dt { + color: var(--amber); + font-family: var(--mono); + font-size: 10px; + margin-bottom: 6px; + text-transform: uppercase; +} + +.reveal-content dd { + color: var(--paper-soft); + font-size: 12px; + line-height: 1.5; + margin: 0; +} + +@media (max-width: 720px) { + .document-card::after { + display: none; + } + + .document-card h1 { + font-size: 34px; + } + + .document-card p { + font-size: 15px; + } + + .opening-copy h1 { + font-size: 42px; + } + + .opening-copy p { + font-size: 15px; + } + + .puzzle-card, + .tag-card, + .active-panel .cast-board, + .intake-content, + .intake-content ol { + grid-template-columns: 1fr; + } + + .doctrine-heading { + align-items: stretch; + flex-direction: column; + } + + .system-map-header { + align-items: flex-start; + flex-direction: column; + } + + .map-action, + .text-action { + width: fit-content; + } + + .system-map-modal { + padding: 10px; + } + + .system-map-panel { + max-height: 94vh; + max-width: 100%; + padding: 12px; + } + + .system-map-header h2 { + font-size: 30px; + } + + .system-map-header .reset-action { + width: fit-content; + } + + .system-map-figure img { + max-height: 62vh; + } +} diff --git a/Case Box 001 - The Creator Who Left But Kept Posting/src/styles/layout.css b/Case Box 001 - The Creator Who Left But Kept Posting/src/styles/layout.css new file mode 100644 index 0000000..3c88328 --- /dev/null +++ b/Case Box 001 - The Creator Who Left But Kept Posting/src/styles/layout.css @@ -0,0 +1,161 @@ +.app-shell { + border: 1px solid rgba(199, 155, 85, 0.28); + box-shadow: + 0 24px 80px rgba(0, 0, 0, 0.5), + inset 0 1px 0 rgba(237, 231, 217, 0.06); + margin: 10px; + min-height: calc(100vh - 16px); + position: relative; +} + +.app-shell::before { + background: + linear-gradient(90deg, transparent, rgba(199, 155, 85, 0.22), transparent), + linear-gradient(90deg, rgba(177, 67, 50, 0.7), rgba(199, 155, 85, 0.82)); + content: ""; + height: 3px; + left: 0; + position: absolute; + right: 0; + top: 0; +} + +.app-header { + background: + linear-gradient(90deg, rgba(32, 30, 25, 0.94), rgba(18, 17, 15, 0.96)), + linear-gradient(rgba(237, 231, 217, 0.04) 1px, transparent 1px); + background-size: auto, 18px 18px; + border-bottom: 1px solid rgba(199, 155, 85, 0.26); + display: grid; + grid-template-columns: minmax(260px, 0.9fr) minmax(340px, 1.3fr) minmax(220px, 0.7fr); + min-height: 76px; +} + +.brand, +.case-title, +.signal-summary { + align-items: center; + display: flex; + padding: 14px 18px; +} + +.brand { + border-right: 1px solid var(--line); + gap: 14px; + text-transform: uppercase; +} + +.case-title, +.signal-summary { + flex-direction: column; + justify-content: center; + text-align: center; + text-transform: uppercase; +} + +.signal-summary { + border-left: 1px solid var(--line); +} + +.workspace { + background: + linear-gradient(90deg, rgba(13, 13, 12, 0.72), rgba(32, 30, 25, 0.72) 42%, rgba(13, 13, 12, 0.9)), + repeating-linear-gradient(0deg, rgba(237, 231, 217, 0.025) 0 1px, transparent 1px 12px); + display: grid; + grid-template-columns: 300px minmax(420px, 1fr) 360px; + min-height: calc(100vh - 96px); +} + +.opening-brief { + background: + linear-gradient(90deg, rgba(177, 67, 50, 0.14), transparent 34%), + linear-gradient(180deg, rgba(237, 231, 217, 0.06), transparent 52%), + repeating-linear-gradient(0deg, rgba(237, 231, 217, 0.026) 0 1px, transparent 1px 14px); + display: grid; + gap: 24px; + grid-template-columns: minmax(0, 1.05fr) minmax(280px, 0.72fr); + min-height: calc(100vh - 96px); + padding: clamp(28px, 5vw, 72px); +} + +.opening-copy, +.opening-actions { + align-self: center; +} + +.opening-copy { + max-width: 760px; +} + +.opening-actions { + background: rgba(13, 13, 12, 0.56); + border: 1px solid rgba(199, 155, 85, 0.28); + padding: 22px; +} + +.evidence-panel, +.reading-panel { + background: rgba(10, 9, 8, 0.88); + padding: 18px; +} + +.evidence-panel { + border-right: 1px solid rgba(199, 155, 85, 0.2); +} + +.active-panel { + background: + linear-gradient(90deg, rgba(199, 155, 85, 0.03), transparent 30%), + linear-gradient(rgba(237, 231, 217, 0.025) 1px, transparent 1px), + linear-gradient(90deg, rgba(237, 231, 217, 0.018) 1px, transparent 1px); + background-size: auto, 28px 28px, 28px 28px; + align-content: start; + display: grid; + gap: 18px; + padding: 22px; +} + +.reading-panel { + border-left: 1px solid rgba(199, 155, 85, 0.2); + display: grid; + gap: 16px; + align-content: start; +} + +@media (max-width: 1180px) { + .app-header, + .workspace, + .opening-brief { + grid-template-columns: 1fr; + } + + .brand, + .signal-summary, + .evidence-panel, + .reading-panel { + border-left: 0; + border-right: 0; + } + + .evidence-panel { + border-bottom: 1px solid var(--line); + } + + .reading-panel { + border-top: 1px solid var(--line); + } +} + +@media (max-width: 720px) { + .app-shell { + margin: 0; + min-height: 100vh; + } + + .evidence-panel, + .active-panel, + .reading-panel, + .opening-brief { + padding: 16px; + } +} diff --git a/README.md b/README.md index 96f0c6e..611abe7 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,21 @@ **The Network Mythos** is a playable interactive archive about synthetic identity, online communities, platform power, and the ethics of preserving truth. -Players investigate strange digital cases through an Archive Hub, read evidence dossiers, unlock deeper files, build theories, and decide what the Archive should do with the truth: **Publish**, **Bury**, or **Preserve**. +Players investigate strange digital cases through evidence, tags, locked artifacts, theory-building, and Archive decisions. The current public demo proves the smallest strong loop first: one Case Box where the truth stays stable, but the player's route through the evidence responds to how they interpret it. ![Archive Hub screenshot](docs/images/archive-hub.png) +## Public Demo + +Start here: **[Case Box 001 - The Creator Who Left But Kept Posting](Case%20Box%20001%20-%20The%20Creator%20Who%20Left%20But%20Kept%20Posting/README.md)**. + +![Case Box 001 demo](Case%20Box%20001%20-%20The%20Creator%20Who%20Left%20But%20Kept%20Posting/docs/case-box-demo.gif) + +In this prototype, players inspect evidence, tag what each artifact suggests, unlock one hidden document, and submit an interpretation. The ending reveals the canonical answer alongside the player's reading path, so the game can stay fair while still responding to how the investigation unfolded. + ## What Is Playable +- **Recommended public prototype: Case Box 001**: investigate a creator who left the Network, then kept posting. Inspect evidence, solve one access gate, tag interpretation signals, and compare the canonical answer with your reading path. - **Archive Hub**: tracks case decisions, Archive pressure meters, mapped mythic forces, witnesses, vault entries, and consequences. - **Case 001: The Door Is Real**: determine whether Mara Vale truly returned, and whether proof can protect a person without capturing them. - **Case 002: The Half Synthetic Community**: investigate a support community where the most stabilizing members were synthetic. @@ -28,7 +37,28 @@ The current build explores: - archives that can preserve the erased or consume them again - cross-case consequences through shared Archive state -## Run Locally +## Run The Case Box Demo + +The Case Box prototype is static and can be opened directly: + +```text +Case Box 001 - The Creator Who Left But Kept Posting/index.html +``` + +Or served locally: + +```powershell +cd "Case Box 001 - The Creator Who Left But Kept Posting" +python -m http.server 5174 --bind 127.0.0.1 +``` + +Then open: + +```text +http://127.0.0.1:5174/ +``` + +## Run The Archive Hub Requirements: @@ -61,6 +91,10 @@ Archive Hub/ styles.css server.js case-engine.js +Case Box 001 - The Creator Who Left But Kept Posting/ + index.html + src/ + docs/ Case 001 - The Door Is Real/ Case 002 - The Half Synthetic Community/ Case 003 - The Human Premium/ @@ -78,6 +112,7 @@ Build an Archive strong enough to reveal the Network's structure without becomin ## Useful Docs +- [Case Box 001 demo](Case%20Box%20001%20-%20The%20Creator%20Who%20Left%20But%20Kept%20Posting/README.md) - [Game design](GAME_DESIGN.md) - [Architecture](ARCHITECTURE.md) - [Roadmap](ROADMAP.md) diff --git a/docs/INDEX.md b/docs/INDEX.md index 97b9b1b..a1f8151 100644 --- a/docs/INDEX.md +++ b/docs/INDEX.md @@ -3,6 +3,7 @@ Start with these files: - [README](../README.md): public overview, playable state, and local run instructions. +- [Case Box 001](../Case%20Box%20001%20-%20The%20Creator%20Who%20Left%20But%20Kept%20Posting/README.md): recommended public demo for the focused investigation loop, including demo GIF and playtest proof. - [GAME_DESIGN](../GAME_DESIGN.md): core loop, player objective, audience, mechanics, and design pillars. - [ARCHITECTURE](../ARCHITECTURE.md): server model, shared-origin routing, case discovery, and case engine structure. - [ROADMAP](../ROADMAP.md): public next steps.