Created
January 6, 2026 00:05
-
-
Save rndmcnlly/34aafb0cd2a75ae377d10d1058e03189 to your computer and use it in GitHub Desktop.
Executable concept art for TALL TALE, a game about hauling away America's stuff while a sad giant tells you what it meant.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>TALL TALE - Prototype Concepts</title> | |
| <style> | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| } | |
| body { | |
| font-family: Georgia, serif; | |
| background: #F5F0E6; | |
| color: #3D3528; | |
| min-height: 100vh; | |
| padding: 20px; | |
| } | |
| h1 { | |
| text-align: center; | |
| font-weight: normal; | |
| font-size: 1.8rem; | |
| margin-bottom: 8px; | |
| color: #5C4033; | |
| } | |
| .subtitle { | |
| text-align: center; | |
| font-style: italic; | |
| font-size: 0.95rem; | |
| color: #7D6B5D; | |
| margin-bottom: 24px; | |
| } | |
| /* Tab Navigation */ | |
| .tab-nav { | |
| display: flex; | |
| justify-content: center; | |
| gap: 8px; | |
| flex-wrap: wrap; | |
| margin-bottom: 24px; | |
| } | |
| .tab-btn { | |
| background: #E8E0D0; | |
| border: 2px solid #C9B99A; | |
| border-radius: 8px 8px 0 0; | |
| padding: 12px 20px; | |
| font-family: Georgia, serif; | |
| font-size: 0.95rem; | |
| color: #5C4033; | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| } | |
| .tab-btn:hover { | |
| background: #DDD5C3; | |
| } | |
| .tab-btn.active { | |
| background: #FFF9F0; | |
| border-bottom-color: #FFF9F0; | |
| font-weight: bold; | |
| } | |
| /* Tab Content */ | |
| .tab-content { | |
| display: none; | |
| background: #FFF9F0; | |
| border: 2px solid #C9B99A; | |
| border-radius: 12px; | |
| padding: 24px; | |
| min-height: 500px; | |
| } | |
| .tab-content.active { | |
| display: block; | |
| } | |
| .tab-title { | |
| font-size: 1.3rem; | |
| margin-bottom: 8px; | |
| color: #5C4033; | |
| } | |
| .tab-desc { | |
| font-size: 0.9rem; | |
| color: #7D6B5D; | |
| margin-bottom: 20px; | |
| font-style: italic; | |
| } | |
| /* Wireframe Area */ | |
| .wireframe { | |
| background: #F5F0E6; | |
| border: 2px dashed #C9B99A; | |
| border-radius: 8px; | |
| padding: 20px; | |
| margin-bottom: 20px; | |
| min-height: 350px; | |
| position: relative; | |
| } | |
| /* Expander */ | |
| .expander { | |
| border-top: 1px solid #C9B99A; | |
| margin-top: 16px; | |
| } | |
| .expander-header { | |
| padding: 12px 0; | |
| cursor: pointer; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| color: #7D6B5D; | |
| font-size: 0.9rem; | |
| } | |
| .expander-header:hover { | |
| color: #5C4033; | |
| } | |
| .expander-arrow { | |
| transition: transform 0.2s ease; | |
| } | |
| .expander.open .expander-arrow { | |
| transform: rotate(90deg); | |
| } | |
| .expander-content { | |
| display: none; | |
| padding: 12px 0; | |
| font-size: 0.85rem; | |
| line-height: 1.6; | |
| color: #5C4033; | |
| } | |
| .expander.open .expander-content { | |
| display: block; | |
| } | |
| .expander-content ul { | |
| margin-left: 20px; | |
| } | |
| .expander-content li { | |
| margin-bottom: 8px; | |
| } | |
| /* ============ TAB A: Vignette Composer ============ */ | |
| .vignette-area { | |
| display: flex; | |
| flex-direction: column; | |
| height: 340px; | |
| } | |
| .constellation { | |
| flex: 1; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| position: relative; | |
| } | |
| .drop-zone { | |
| width: 70px; | |
| height: 70px; | |
| border: 2px dashed #A89880; | |
| border-radius: 50%; | |
| position: absolute; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 0.7rem; | |
| color: #A89880; | |
| background: #FFFDF8; | |
| transition: all 0.2s ease; | |
| } | |
| .drop-zone.drag-over { | |
| border-color: #5C4033; | |
| background: #F0EBE0; | |
| } | |
| .drop-zone.filled { | |
| border-style: solid; | |
| border-color: #8B7355; | |
| background: #E8E0D0; | |
| font-size: 0.65rem; | |
| color: #5C4033; | |
| font-weight: bold; | |
| } | |
| .drop-zone:nth-child(1) { top: 10px; left: 50%; transform: translateX(-50%); } | |
| .drop-zone:nth-child(2) { top: 60px; right: 60px; } | |
| .drop-zone:nth-child(3) { bottom: 30px; right: 80px; } | |
| .drop-zone:nth-child(4) { bottom: 30px; left: 80px; } | |
| .drop-zone:nth-child(5) { top: 60px; left: 60px; } | |
| .flatbed { | |
| background: #8B7355; | |
| border-radius: 8px; | |
| padding: 12px; | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 8px; | |
| min-height: 80px; | |
| } | |
| .object-tile { | |
| padding: 8px 12px; | |
| border-radius: 6px; | |
| font-size: 0.75rem; | |
| cursor: grab; | |
| color: white; | |
| font-weight: bold; | |
| user-select: none; | |
| transition: transform 0.1s ease; | |
| } | |
| .object-tile:hover { | |
| transform: scale(1.05); | |
| } | |
| .object-tile.dragging { | |
| opacity: 0.5; | |
| } | |
| .object-tile[data-object="freezer"] { background: #6B8E8E; } | |
| .object-tile[data-object="dress"] { background: #C9A87C; } | |
| .object-tile[data-object="trombone"] { background: #9E7B5B; } | |
| .object-tile[data-object="sign"] { background: #7B6B5B; } | |
| .object-tile[data-object="mower"] { background: #6B8B6B; } | |
| .object-tile[data-object="tv"] { background: #5B6B7B; } | |
| .paul-output { | |
| margin-top: 16px; | |
| padding: 16px; | |
| background: #FFFDF8; | |
| border-left: 4px solid #C9B99A; | |
| font-style: italic; | |
| font-size: 0.9rem; | |
| line-height: 1.5; | |
| color: #5C4033; | |
| min-height: 60px; | |
| opacity: 0; | |
| transition: opacity 0.5s ease; | |
| } | |
| .paul-output.visible { | |
| opacity: 1; | |
| } | |
| .listen-btn { | |
| margin-top: 12px; | |
| padding: 10px 24px; | |
| background: #5C4033; | |
| color: #FFF9F0; | |
| border: none; | |
| border-radius: 6px; | |
| font-family: Georgia, serif; | |
| font-size: 0.9rem; | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| opacity: 0.3; | |
| pointer-events: none; | |
| } | |
| .listen-btn.enabled { | |
| opacity: 1; | |
| pointer-events: auto; | |
| } | |
| .listen-btn.enabled:hover { | |
| background: #7D6B5D; | |
| } | |
| /* ============ TAB B: Collection Conversation ============ */ | |
| .conversation-area { | |
| display: flex; | |
| gap: 24px; | |
| height: 280px; | |
| } | |
| .person-panel { | |
| flex: 1; | |
| background: #FFFDF8; | |
| border: 2px solid #C9B99A; | |
| border-radius: 8px; | |
| padding: 20px; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .person-avatar { | |
| width: 80px; | |
| height: 80px; | |
| border-radius: 8px; | |
| margin: 0 auto 12px; | |
| } | |
| .person-name { | |
| text-align: center; | |
| font-weight: bold; | |
| font-size: 1.1rem; | |
| color: #5C4033; | |
| margin-bottom: 8px; | |
| } | |
| .person-desc { | |
| font-size: 0.85rem; | |
| color: #7D6B5D; | |
| line-height: 1.4; | |
| text-align: center; | |
| } | |
| .choices-panel { | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 12px; | |
| } | |
| .choice-btn { | |
| flex: 1; | |
| padding: 16px; | |
| background: #E8E0D0; | |
| border: 2px solid #C9B99A; | |
| border-radius: 8px; | |
| font-family: Georgia, serif; | |
| font-size: 0.95rem; | |
| color: #5C4033; | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| } | |
| .choice-btn:hover { | |
| background: #DDD5C3; | |
| border-color: #A89880; | |
| } | |
| .response-area { | |
| margin-top: 16px; | |
| padding: 16px; | |
| background: #FFFDF8; | |
| border-left: 4px solid #C9B99A; | |
| font-size: 0.9rem; | |
| line-height: 1.5; | |
| color: #5C4033; | |
| min-height: 60px; | |
| } | |
| .truck-counter { | |
| margin-top: 12px; | |
| text-align: right; | |
| font-size: 0.85rem; | |
| color: #7D6B5D; | |
| } | |
| /* ============ TAB C: The Stack ============ */ | |
| .stack-area { | |
| height: 320px; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .stack-flatbed { | |
| position: absolute; | |
| bottom: 0; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| width: 200px; | |
| height: 8px; | |
| background: #8B7355; | |
| border-radius: 4px; | |
| } | |
| .stack-zone { | |
| position: absolute; | |
| bottom: 8px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| width: 200px; | |
| height: 300px; | |
| display: flex; | |
| flex-direction: column-reverse; | |
| align-items: center; | |
| } | |
| .stacked-object { | |
| border-radius: 4px; | |
| transition: all 0.3s ease; | |
| position: relative; | |
| } | |
| .drop-btn { | |
| position: absolute; | |
| top: 10px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| padding: 12px 28px; | |
| background: #5C4033; | |
| color: #FFF9F0; | |
| border: none; | |
| border-radius: 6px; | |
| font-family: Georgia, serif; | |
| font-size: 0.95rem; | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| } | |
| .drop-btn:hover { | |
| background: #7D6B5D; | |
| } | |
| .stack-counter { | |
| position: absolute; | |
| top: 10px; | |
| right: 20px; | |
| font-size: 0.9rem; | |
| color: #7D6B5D; | |
| } | |
| .reset-stack-btn { | |
| position: absolute; | |
| top: 10px; | |
| left: 20px; | |
| padding: 8px 16px; | |
| background: #E8E0D0; | |
| border: 2px solid #C9B99A; | |
| border-radius: 6px; | |
| font-family: Georgia, serif; | |
| font-size: 0.8rem; | |
| color: #5C4033; | |
| cursor: pointer; | |
| } | |
| @keyframes fall { | |
| 0% { transform: translateY(-200px); opacity: 0; } | |
| 20% { opacity: 1; } | |
| 100% { transform: translateY(0); } | |
| } | |
| .stacked-object.falling { | |
| animation: fall 0.4s ease-out forwards; | |
| } | |
| /* ============ TAB D: Paul's Voice ============ */ | |
| .voice-area { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| height: 320px; | |
| text-align: center; | |
| } | |
| .paul-monologue { | |
| max-width: 500px; | |
| font-size: 1.05rem; | |
| line-height: 1.7; | |
| color: #5C4033; | |
| font-style: italic; | |
| min-height: 150px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 20px; | |
| } | |
| .paul-monologue .typing { | |
| color: #A89880; | |
| } | |
| .paul-monologue .text { | |
| opacity: 0; | |
| transition: opacity 0.8s ease; | |
| } | |
| .paul-monologue .text.visible { | |
| opacity: 1; | |
| } | |
| .ask-paul-btn { | |
| padding: 14px 32px; | |
| background: #5C4033; | |
| color: #FFF9F0; | |
| border: none; | |
| border-radius: 8px; | |
| font-family: Georgia, serif; | |
| font-size: 1rem; | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| } | |
| .ask-paul-btn:hover { | |
| background: #7D6B5D; | |
| } | |
| .voice-counter { | |
| margin-top: 16px; | |
| font-size: 0.8rem; | |
| color: #A89880; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <h1>TALL TALE</h1> | |
| <p class="subtitle">Prototype Concepts — Executable Sketches</p> | |
| <nav class="tab-nav"> | |
| <button class="tab-btn active" data-tab="vignette">Vignette Composer</button> | |
| <button class="tab-btn" data-tab="conversation">Collection Conversation</button> | |
| <button class="tab-btn" data-tab="stack">The Stack</button> | |
| <button class="tab-btn" data-tab="voice">Paul's Voice</button> | |
| </nav> | |
| <!-- ============ TAB A: Vignette Composer ============ --> | |
| <div class="tab-content active" id="tab-vignette"> | |
| <h2 class="tab-title">Vignette Composer</h2> | |
| <p class="tab-desc">Drag objects into the constellation. Paul finds the story.</p> | |
| <div class="wireframe"> | |
| <div class="vignette-area"> | |
| <div class="constellation"> | |
| <div class="drop-zone" data-slot="0">empty</div> | |
| <div class="drop-zone" data-slot="1">empty</div> | |
| <div class="drop-zone" data-slot="2">empty</div> | |
| <div class="drop-zone" data-slot="3">empty</div> | |
| <div class="drop-zone" data-slot="4">empty</div> | |
| </div> | |
| <div class="flatbed" id="flatbed-a"> | |
| <div class="object-tile" draggable="true" data-object="freezer">Freezer</div> | |
| <div class="object-tile" draggable="true" data-object="dress">Wedding Dress</div> | |
| <div class="object-tile" draggable="true" data-object="trombone">Trombone</div> | |
| <div class="object-tile" draggable="true" data-object="sign">Campaign Sign</div> | |
| <div class="object-tile" draggable="true" data-object="mower">Push Mower</div> | |
| <div class="object-tile" draggable="true" data-object="tv">Old TV</div> | |
| </div> | |
| </div> | |
| <button class="listen-btn" id="listen-btn">Listen to Paul</button> | |
| <div class="paul-output" id="paul-output"></div> | |
| </div> | |
| <div class="expander" id="expander-a"> | |
| <div class="expander-header"> | |
| <span>What the real prototype would test →</span> | |
| <span class="expander-arrow">▶</span> | |
| </div> | |
| <div class="expander-content"> | |
| <ul> | |
| <li><strong>Authorship feel:</strong> Does arranging objects feel like composing, or just sorting?</li> | |
| <li><strong>Generative coherence:</strong> Can LLM-assembled narration feel discovered rather than random?</li> | |
| <li><strong>Slot count:</strong> Is 5 slots too many? Too few? Does constraint breed meaning?</li> | |
| <li><strong>Replay value:</strong> Do players try different combinations to hear different stories?</li> | |
| <li><strong>Success signal:</strong> Players saying "I made him say that" rather than "it said something."</li> | |
| </ul> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- ============ TAB B: Collection Conversation ============ --> | |
| <div class="tab-content" id="tab-conversation"> | |
| <h2 class="tab-title">Collection Conversation</h2> | |
| <p class="tab-desc">Meet the people. Decide what to carry.</p> | |
| <div class="wireframe"> | |
| <div class="conversation-area"> | |
| <div class="person-panel"> | |
| <div class="person-avatar" id="person-avatar" style="background: #6B8E8E;"></div> | |
| <div class="person-name" id="person-name">Darlene</div> | |
| <div class="person-desc" id="person-desc">She's standing next to a chest freezer. It's been unplugged for six months.</div> | |
| </div> | |
| <div class="choices-panel"> | |
| <button class="choice-btn" data-choice="take">Take it</button> | |
| <button class="choice-btn" data-choice="ask">Ask about it</button> | |
| <button class="choice-btn" data-choice="leave">Leave it</button> | |
| </div> | |
| </div> | |
| <div class="response-area" id="response-area"> | |
| <em>She's waiting for you to decide.</em> | |
| </div> | |
| <div class="truck-counter" id="truck-counter">Truck: 0 items</div> | |
| </div> | |
| <div class="expander" id="expander-b"> | |
| <div class="expander-header"> | |
| <span>What the real prototype would test →</span> | |
| <span class="expander-arrow">▶</span> | |
| </div> | |
| <div class="expander-content"> | |
| <ul> | |
| <li><strong>Pacing:</strong> How long should each encounter last? When does curiosity become impatience?</li> | |
| <li><strong>Story appetite:</strong> Do players click "Ask" or rush to "Take"? What ratio emerges?</li> | |
| <li><strong>Leave it weight:</strong> Does leaving something behind feel meaningful or wasteful?</li> | |
| <li><strong>Character voice:</strong> How much personality can we convey in 2-3 lines?</li> | |
| <li><strong>Success signal:</strong> Players pausing before choosing. Players mentioning characters by name later.</li> | |
| </ul> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- ============ TAB C: The Stack ============ --> | |
| <div class="tab-content" id="tab-stack"> | |
| <h2 class="tab-title">The Stack</h2> | |
| <p class="tab-desc">No story. Just the satisfaction of accumulation.</p> | |
| <div class="wireframe"> | |
| <div class="stack-area"> | |
| <button class="reset-stack-btn" id="reset-stack-btn">Reset</button> | |
| <button class="drop-btn" id="drop-btn">Drop Next Object</button> | |
| <div class="stack-counter" id="stack-counter">Objects: 0</div> | |
| <div class="stack-zone" id="stack-zone"></div> | |
| <div class="stack-flatbed"></div> | |
| </div> | |
| </div> | |
| <div class="expander" id="expander-c"> | |
| <div class="expander-header"> | |
| <span>What the real prototype would test →</span> | |
| <span class="expander-arrow">▶</span> | |
| </div> | |
| <div class="expander-content"> | |
| <ul> | |
| <li><strong>Core satisfaction:</strong> Is watching the pile grow rewarding without any narrative wrapper?</li> | |
| <li><strong>Physics feel:</strong> How much wobble and precarity is fun vs. stressful?</li> | |
| <li><strong>Failure state:</strong> Should the stack ever collapse? Or is permanence the point?</li> | |
| <li><strong>Visual identity:</strong> Do random shapes and colors read as "stuff" or just "blocks"?</li> | |
| <li><strong>Success signal:</strong> Players clicking more than 10 times. Players showing someone else their stack.</li> | |
| </ul> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- ============ TAB D: Paul's Voice ============ --> | |
| <div class="tab-content" id="tab-voice"> | |
| <h2 class="tab-title">Paul's Voice</h2> | |
| <p class="tab-desc">No mechanics. Just the tone of a giant who remembers.</p> | |
| <div class="wireframe"> | |
| <div class="voice-area"> | |
| <div class="paul-monologue" id="paul-monologue"> | |
| <span class="typing" id="typing-indicator" style="display:none;">. . .</span> | |
| <span class="text" id="monologue-text"></span> | |
| </div> | |
| <button class="ask-paul-btn" id="ask-paul-btn">What do you see, Paul?</button> | |
| <div class="voice-counter" id="voice-counter"></div> | |
| </div> | |
| </div> | |
| <div class="expander" id="expander-d"> | |
| <div class="expander-header"> | |
| <span>What the real prototype would test →</span> | |
| <span class="expander-arrow">▶</span> | |
| </div> | |
| <div class="expander-content"> | |
| <ul> | |
| <li><strong>Length tolerance:</strong> How long can Paul talk before players skip? 20 words? 50? 100?</li> | |
| <li><strong>Tone balance:</strong> What ratio of melancholy, humor, and strangeness feels right?</li> | |
| <li><strong>Voice consistency:</strong> Can we define Paul clearly enough that LLM output stays in character?</li> | |
| <li><strong>Silence value:</strong> Do pauses and short lines feel meaningful or broken?</li> | |
| <li><strong>Success signal:</strong> Players clicking through all monologues. Players quoting Paul back.</li> | |
| </ul> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // ============ TAB SWITCHING ============ | |
| const tabBtns = document.querySelectorAll('.tab-btn'); | |
| const tabContents = document.querySelectorAll('.tab-content'); | |
| tabBtns.forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| tabBtns.forEach(b => b.classList.remove('active')); | |
| tabContents.forEach(c => c.classList.remove('active')); | |
| btn.classList.add('active'); | |
| document.getElementById('tab-' + btn.dataset.tab).classList.add('active'); | |
| }); | |
| }); | |
| // ============ EXPANDERS ============ | |
| document.querySelectorAll('.expander-header').forEach(header => { | |
| header.addEventListener('click', () => { | |
| header.parentElement.classList.toggle('open'); | |
| }); | |
| }); | |
| // ============ TAB A: Vignette Composer ============ | |
| const vignetteSlots = {}; | |
| const objectPhrases = { | |
| freezer: [ | |
| "The freezer held everything she couldn't let thaw.", | |
| "It hummed for years after they unplugged it. Nobody knows why.", | |
| "Someone told me freezers remember the shape of what's gone." | |
| ], | |
| dress: [ | |
| "The dress was never worn. That was the point.", | |
| "She kept it sealed. As if time was a kind of moisture.", | |
| "Some things are too finished to use." | |
| ], | |
| trombone: [ | |
| "He played it once at a funeral. Couldn't stop after that.", | |
| "Brass holds heat longer than you'd think.", | |
| "The slide still moves. That's the sad part." | |
| ], | |
| sign: [ | |
| "The campaign ended. The sign stayed up. Weather made it abstract.", | |
| "Nobody remembers what he ran for. Just that he lost.", | |
| "A promise is just a shape if you wait long enough." | |
| ], | |
| mower: [ | |
| "The lawn grew over it one summer. Felt intentional.", | |
| "He said he'd fix it. That was the last thing he fixed.", | |
| "Green machine, green grass, eventually no difference." | |
| ], | |
| tv: [ | |
| "It still turns on. Shows the same static it always did.", | |
| "They watched the moon landing on it. And everything after.", | |
| "The glass is curved like an eye that won't close." | |
| ] | |
| }; | |
| const connectives = [ | |
| "And somehow, that reminds me of", | |
| "Which sits next to", | |
| "Nobody asked, but nearby there's", | |
| "And of course,", | |
| "Then there's" | |
| ]; | |
| let draggedObject = null; | |
| document.querySelectorAll('#flatbed-a .object-tile').forEach(tile => { | |
| tile.addEventListener('dragstart', (e) => { | |
| draggedObject = e.target.dataset.object; | |
| e.target.classList.add('dragging'); | |
| }); | |
| tile.addEventListener('dragend', (e) => { | |
| e.target.classList.remove('dragging'); | |
| }); | |
| }); | |
| document.querySelectorAll('.drop-zone').forEach(zone => { | |
| zone.addEventListener('dragover', (e) => { | |
| e.preventDefault(); | |
| zone.classList.add('drag-over'); | |
| }); | |
| zone.addEventListener('dragleave', () => { | |
| zone.classList.remove('drag-over'); | |
| }); | |
| zone.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| zone.classList.remove('drag-over'); | |
| if (draggedObject && !zone.classList.contains('filled')) { | |
| zone.classList.add('filled'); | |
| zone.textContent = draggedObject.charAt(0).toUpperCase() + draggedObject.slice(1); | |
| vignetteSlots[zone.dataset.slot] = draggedObject; | |
| updateListenButton(); | |
| } | |
| }); | |
| zone.addEventListener('click', () => { | |
| if (zone.classList.contains('filled')) { | |
| zone.classList.remove('filled'); | |
| zone.textContent = 'empty'; | |
| delete vignetteSlots[zone.dataset.slot]; | |
| updateListenButton(); | |
| } | |
| }); | |
| }); | |
| function updateListenButton() { | |
| const count = Object.keys(vignetteSlots).length; | |
| const btn = document.getElementById('listen-btn'); | |
| if (count >= 3) { | |
| btn.classList.add('enabled'); | |
| } else { | |
| btn.classList.remove('enabled'); | |
| } | |
| } | |
| document.getElementById('listen-btn').addEventListener('click', () => { | |
| const objects = Object.values(vignetteSlots); | |
| if (objects.length < 3) return; | |
| let story = ''; | |
| objects.forEach((obj, i) => { | |
| const phrases = objectPhrases[obj]; | |
| const phrase = phrases[Math.floor(Math.random() * phrases.length)]; | |
| if (i === 0) { | |
| story += phrase; | |
| } else { | |
| const conn = connectives[Math.floor(Math.random() * connectives.length)]; | |
| story += ' ' + conn + ' ' + phrase.charAt(0).toLowerCase() + phrase.slice(1); | |
| } | |
| }); | |
| const output = document.getElementById('paul-output'); | |
| output.textContent = '"' + story + '"'; | |
| output.classList.add('visible'); | |
| }); | |
| // ============ TAB B: Collection Conversation ============ | |
| const people = [ | |
| { | |
| name: 'Darlene', | |
| color: '#6B8E8E', | |
| desc: "She's standing next to a chest freezer. It's been unplugged for six months.", | |
| askResponse: '"It was my mother\'s. She kept everything labeled by year. I couldn\'t bring myself to open it after. You understand."', | |
| takeResponse: 'She nods once, slow. "Good. Thank you." She doesn\'t watch you load it.' | |
| }, | |
| { | |
| name: 'Earl', | |
| color: '#9E7B5B', | |
| desc: "He's holding a trombone case like it might run off.", | |
| askResponse: '"Played it in the marching band. \'71. We were terrible but loud. My son doesn\'t want it. Nobody wants it."', | |
| takeResponse: 'He hands it over fast, then looks at his hands like he doesn\'t recognize them.' | |
| }, | |
| { | |
| name: 'Marcy', | |
| color: '#C9A87C', | |
| desc: "There's a wedding dress on a hanger behind her. Still in plastic.", | |
| askResponse: '"It fit perfect. That was thirty years ago. I think perfect was the problem."', | |
| takeResponse: 'She laughs—surprised by herself. "Lighter already. Isn\'t that something."' | |
| }, | |
| { | |
| name: 'Tom', | |
| color: '#7B6B5B', | |
| desc: "He's standing in front of a campaign sign, faded to almost nothing.", | |
| askResponse: '"I ran for water commissioner. Lost by eleven votes. Kept the sign to remember what trying felt like."', | |
| takeResponse: '"Take the whole post if you want. Ground\'s soft. It\'ll come right up."' | |
| } | |
| ]; | |
| let personIndex = 0; | |
| let truckCount = 0; | |
| function loadPerson(index) { | |
| if (index >= people.length) { | |
| document.getElementById('person-name').textContent = 'End of the road'; | |
| document.getElementById('person-desc').textContent = 'No one else is waiting. Time to move on.'; | |
| document.getElementById('person-avatar').style.background = '#C9B99A'; | |
| document.getElementById('response-area').innerHTML = '<em>The town is quiet now.</em>'; | |
| document.querySelectorAll('.choice-btn').forEach(btn => btn.style.display = 'none'); | |
| return; | |
| } | |
| const p = people[index]; | |
| document.getElementById('person-name').textContent = p.name; | |
| document.getElementById('person-desc').textContent = p.desc; | |
| document.getElementById('person-avatar').style.background = p.color; | |
| document.getElementById('response-area').innerHTML = '<em>Waiting for you to decide.</em>'; | |
| } | |
| document.querySelectorAll('.choice-btn').forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| const choice = btn.dataset.choice; | |
| const p = people[personIndex]; | |
| const response = document.getElementById('response-area'); | |
| if (choice === 'ask') { | |
| response.innerHTML = p.askResponse; | |
| } else if (choice === 'take') { | |
| truckCount++; | |
| document.getElementById('truck-counter').textContent = 'Truck: ' + truckCount + ' items'; | |
| response.innerHTML = '<em>' + p.takeResponse + '</em>'; | |
| setTimeout(() => { | |
| personIndex++; | |
| loadPerson(personIndex); | |
| }, 1500); | |
| } else if (choice === 'leave') { | |
| response.innerHTML = '<em>You nod and move on. Some things stay where they are.</em>'; | |
| setTimeout(() => { | |
| personIndex++; | |
| loadPerson(personIndex); | |
| }, 1200); | |
| } | |
| }); | |
| }); | |
| // ============ TAB C: The Stack ============ | |
| const stackColors = ['#6B8E8E', '#C9A87C', '#9E7B5B', '#7B6B5B', '#6B8B6B', '#5B6B7B', '#8B7B6B', '#6B7B8B']; | |
| let stackCount = 0; | |
| let stackHeight = 0; | |
| document.getElementById('drop-btn').addEventListener('click', () => { | |
| const zone = document.getElementById('stack-zone'); | |
| const width = 40 + Math.random() * 80; | |
| const height = 15 + Math.random() * 30; | |
| const color = stackColors[Math.floor(Math.random() * stackColors.length)]; | |
| const rotation = -5 + Math.random() * 10; | |
| const offsetX = -15 + Math.random() * 30; | |
| const obj = document.createElement('div'); | |
| obj.className = 'stacked-object falling'; | |
| obj.style.width = width + 'px'; | |
| obj.style.height = height + 'px'; | |
| obj.style.background = color; | |
| obj.style.transform = `translateX(${offsetX}px) rotate(${rotation}deg)`; | |
| obj.style.marginTop = '-2px'; | |
| zone.appendChild(obj); | |
| stackCount++; | |
| stackHeight += height; | |
| document.getElementById('stack-counter').textContent = 'Objects: ' + stackCount; | |
| if (stackHeight > 280) { | |
| document.getElementById('drop-btn').textContent = 'Stack full!'; | |
| document.getElementById('drop-btn').disabled = true; | |
| } | |
| }); | |
| document.getElementById('reset-stack-btn').addEventListener('click', () => { | |
| document.getElementById('stack-zone').innerHTML = ''; | |
| stackCount = 0; | |
| stackHeight = 0; | |
| document.getElementById('stack-counter').textContent = 'Objects: 0'; | |
| document.getElementById('drop-btn').textContent = 'Drop Next Object'; | |
| document.getElementById('drop-btn').disabled = false; | |
| }); | |
| // ============ TAB D: Paul's Voice ============ | |
| const monologues = [ | |
| "I've been walking for a long time. Longer than the roads. Sometimes I forget which towns I've already passed through. They all have a water tower. They all have someone who stayed too long.", | |
| "You're doing good work. I mean it. I can't carry the small things anymore. My hands are too big. I pick up a barn and a life falls out.", | |
| "Do you ever think about what enough would feel like? I used to. Now I think the thinking was the problem.", | |
| "There was a town once—I won't say which—where everyone gave me something. I carried it all west. Set it down somewhere in Nevada. It's probably still there. A little mountain of having.", | |
| "Thank you. For the truck. For the asking. Some days I forget anyone's still listening. The wind up here is very loud." | |
| ]; | |
| let monologueIndex = 0; | |
| document.getElementById('ask-paul-btn').addEventListener('click', () => { | |
| const typing = document.getElementById('typing-indicator'); | |
| const text = document.getElementById('monologue-text'); | |
| const counter = document.getElementById('voice-counter'); | |
| text.classList.remove('visible'); | |
| typing.style.display = 'inline'; | |
| setTimeout(() => { | |
| typing.style.display = 'none'; | |
| text.textContent = monologues[monologueIndex]; | |
| text.classList.add('visible'); | |
| counter.textContent = (monologueIndex + 1) + ' / ' + monologues.length; | |
| monologueIndex = (monologueIndex + 1) % monologues.length; | |
| }, 800); | |
| }); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment