Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>SIN CITY 3000 - RPG</title> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Share+Tech+Mono&display=swap" | |
| rel="stylesheet"> | |
| <style> | |
| :root { | |
| --neon-pink: #ff2a6d; | |
| --neon-blue: #05d9e8; | |
| --neon-green: #00ff9d; | |
| --neon-yellow: #f9c80e; | |
| --bg-dark: #01012b; | |
| --bg-panel: #020238; | |
| --text-main: #d1f7ff; | |
| --glass: rgba(5, 217, 232, 0.1); | |
| --border-glow: 0 0 10px var(--neon-blue); | |
| } | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| scrollbar-width: thin; | |
| scrollbar-color: var(--neon-blue) var(--bg-dark); | |
| } | |
| body { | |
| background-color: #000; | |
| background-image: | |
| radial-gradient(circle at 50% 50%, #1a1a40 0%, #000 100%), | |
| linear-gradient(0deg, rgba(0, 0, 0, 0.9) 0%, rgba(0, 0, 0, 0) 50%, rgba(0, 0, 0, 0.9) 100%); | |
| color: var(--text-main); | |
| font-family: 'Share Tech Mono', monospace; | |
| height: 100vh; | |
| overflow: hidden; | |
| display: flex; | |
| flex-direction: column; | |
| position: relative; | |
| } | |
| /* CRT Overlay Effect */ | |
| body::after { | |
| content: " "; | |
| display: block; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| bottom: 0; | |
| right: 0; | |
| background: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.25) 50%), linear-gradient(90deg, rgba(255, 0, 0, 0.06), rgba(0, 255, 0, 0.02), rgba(0, 0, 255, 0.06)); | |
| z-index: 100; | |
| background-size: 100% 2px, 3px 100%; | |
| pointer-events: none; | |
| } | |
| /* Header */ | |
| header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| padding: 1rem 2rem; | |
| border-bottom: 2px solid var(--neon-pink); | |
| background: rgba(0, 0, 0, 0.8); | |
| z-index: 10; | |
| box-shadow: 0 0 20px rgba(255, 42, 109, 0.3); | |
| } | |
| .brand { | |
| font-family: 'Orbitron', sans-serif; | |
| font-size: 1.8rem; | |
| font-weight: 900; | |
| color: var(--neon-pink); | |
| text-transform: uppercase; | |
| letter-spacing: 3px; | |
| text-shadow: 2px 2px 0px var(--neon-blue); | |
| animation: glitch 3s infinite; | |
| } | |
| .anycoder-link { | |
| font-family: 'Orbitron', sans-serif; | |
| color: var(--neon-green); | |
| text-decoration: none; | |
| font-size: 0.9rem; | |
| border: 1px solid var(--neon-green); | |
| padding: 0.5rem 1rem; | |
| transition: all 0.3s ease; | |
| text-transform: uppercase; | |
| } | |
| .anycoder-link:hover { | |
| background: var(--neon-green); | |
| color: #000; | |
| box-shadow: 0 0 15px var(--neon-green); | |
| } | |
| /* Main Layout */ | |
| main { | |
| flex: 1; | |
| display: grid; | |
| grid-template-columns: 300px 1fr 300px; | |
| gap: 1rem; | |
| padding: 1rem; | |
| max-width: 1600px; | |
| margin: 0 auto; | |
| width: 100%; | |
| position: relative; | |
| z-index: 5; | |
| } | |
| /* Panels */ | |
| .panel { | |
| background: var(--bg-panel); | |
| border: 1px solid var(--neon-blue); | |
| padding: 1rem; | |
| position: relative; | |
| box-shadow: inset 0 0 20px rgba(5, 217, 232, 0.1); | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .panel::before { | |
| content: ''; | |
| position: absolute; | |
| top: -2px; | |
| left: -2px; | |
| right: -2px; | |
| bottom: -2px; | |
| background: linear-gradient(45deg, var(--neon-pink), transparent, var(--neon-blue)); | |
| z-index: -1; | |
| opacity: 0.3; | |
| } | |
| .panel-header { | |
| font-family: 'Orbitron', sans-serif; | |
| color: var(--neon-blue); | |
| border-bottom: 1px solid var(--neon-blue); | |
| padding-bottom: 0.5rem; | |
| margin-bottom: 1rem; | |
| text-transform: uppercase; | |
| font-size: 1.2rem; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| /* Left Panel: Stats */ | |
| .stat-row { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 1rem; | |
| font-size: 1.1rem; | |
| } | |
| .stat-label { | |
| color: var(--neon-green); | |
| } | |
| .stat-value { | |
| color: #fff; | |
| font-weight: bold; | |
| } | |
| .health-bar-container { | |
| width: 100%; | |
| height: 10px; | |
| background: #333; | |
| margin-top: 5px; | |
| border: 1px solid #555; | |
| } | |
| .health-bar-fill { | |
| height: 100%; | |
| background: var(--neon-pink); | |
| width: 100%; | |
| transition: width 0.3s ease; | |
| box-shadow: 0 0 10px var(--neon-pink); | |
| } | |
| .inventory-list { | |
| list-style: none; | |
| margin-top: 1rem; | |
| flex: 1; | |
| overflow-y: auto; | |
| } | |
| .inventory-item { | |
| padding: 0.5rem; | |
| border-bottom: 1px solid rgba(255, 255, 255, 0.1); | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .inventory-item i { | |
| color: var(--neon-yellow); | |
| } | |
| /* Center Panel: Game Log / Scene */ | |
| .scene-display { | |
| flex: 1; | |
| overflow-y: auto; | |
| margin-bottom: 1rem; | |
| padding-right: 10px; | |
| font-size: 1.1rem; | |
| line-height: 1.6; | |
| } | |
| .log-entry { | |
| margin-bottom: 1.5rem; | |
| animation: fadeIn 0.5s ease; | |
| } | |
| .log-entry.system { | |
| color: var(--neon-green); | |
| font-style: italic; | |
| border-left: 3px solid var(--neon-green); | |
| padding-left: 10px; | |
| } | |
| .log-entry.danger { | |
| color: var(--neon-pink); | |
| border-left: 3px solid var(--neon-pink); | |
| padding-left: 10px; | |
| } | |
| .log-entry.narrative { | |
| color: var(--text-main); | |
| } | |
| .typing-cursor::after { | |
| content: '█'; | |
| animation: blink 1s infinite; | |
| color: var(--neon-blue); | |
| margin-left: 5px; | |
| } | |
| /* Action Area */ | |
| .actions-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | |
| gap: 1rem; | |
| margin-top: auto; | |
| } | |
| .action-btn { | |
| background: transparent; | |
| border: 1px solid var(--neon-blue); | |
| color: var(--neon-blue); | |
| padding: 1rem; | |
| font-family: 'Orbitron', sans-serif; | |
| font-size: 1rem; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| text-transform: uppercase; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .action-btn:hover { | |
| background: var(--neon-blue); | |
| color: #000; | |
| box-shadow: 0 0 20px var(--neon-blue); | |
| } | |
| .action-btn:disabled { | |
| border-color: #555; | |
| color: #555; | |
| cursor: not-allowed; | |
| box-shadow: none; | |
| } | |
| /* Right Panel: Visuals/Data */ | |
| .visual-feed { | |
| height: 200px; | |
| border: 1px solid var(--neon-pink); | |
| margin-bottom: 1rem; | |
| background: #000; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| overflow: hidden; | |
| position: relative; | |
| } | |
| .visual-icon { | |
| font-size: 5rem; | |
| color: var(--neon-pink); | |
| animation: pulse 2s infinite; | |
| z-index: 2; | |
| } | |
| .grid-bg { | |
| position: absolute; | |
| width: 200%; | |
| height: 200%; | |
| background-image: | |
| linear-gradient(var(--glass) 1px, transparent 1px), | |
| linear-gradient(90deg, var(--glass) 1px, transparent 1px); | |
| background-size: 20px 20px; | |
| transform: perspective(500px) rotateX(60deg) translateY(-100px) translateZ(-200px); | |
| animation: gridMove 20s linear infinite; | |
| } | |
| .status-log { | |
| font-size: 0.8rem; | |
| color: #888; | |
| font-family: monospace; | |
| } | |
| .status-line { | |
| margin-bottom: 4px; | |
| } | |
| /* Animations */ | |
| @keyframes glitch { | |
| 0% { | |
| text-shadow: 2px 2px 0px var(--neon-blue); | |
| transform: translate(0); | |
| } | |
| 20% { | |
| text-shadow: -2px -2px 0px var(--neon-pink); | |
| transform: translate(-2px, 2px); | |
| } | |
| 40% { | |
| text-shadow: 2px -2px 0px var(--neon-green); | |
| transform: translate(2px, -2px); | |
| } | |
| 60% { | |
| text-shadow: -2px 2px 0px var(--neon-blue); | |
| transform: translate(-2px, 2px); | |
| } | |
| 80% { | |
| text-shadow: 2px 2px 0px var(--neon-pink); | |
| transform: translate(2px, -2px); | |
| } | |
| 100% { | |
| text-shadow: 2px 2px 0px var(--neon-blue); | |
| transform: translate(0); | |
| } | |
| } | |
| @keyframes blink { | |
| 0%, | |
| 100% { | |
| opacity: 1; | |
| } | |
| 50% { | |
| opacity: 0; | |
| } | |
| } | |
| @keyframes fadeIn { | |
| from { | |
| opacity: 0; | |
| transform: translateY(10px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| @keyframes pulse { | |
| 0% { | |
| transform: scale(1); | |
| text-shadow: 0 0 10px var(--neon-pink); | |
| } | |
| 50% { | |
| transform: scale(1.1); | |
| text-shadow: 0 0 30px var(--neon-pink); | |
| } | |
| 100% { | |
| transform: scale(1); | |
| text-shadow: 0 0 10px var(--neon-pink); | |
| } | |
| } | |
| @keyframes gridMove { | |
| 0% { | |
| transform: perspective(500px) rotateX(60deg) translateY(0) translateZ(-200px); | |
| } | |
| 100% { | |
| transform: perspective(500px) rotateX(60deg) translateY(100px) translateZ(-200px); | |
| } | |
| } | |
| /* Responsive */ | |
| @media (max-width: 1024px) { | |
| main { | |
| grid-template-columns: 1fr; | |
| grid-template-rows: auto 1fr auto; | |
| height: auto; | |
| overflow-y: auto; | |
| } | |
| body { | |
| overflow: auto; | |
| height: auto; | |
| } | |
| .visual-feed { | |
| display: none; | |
| } | |
| /* Hide visual on mobile to save space */ | |
| .panel { | |
| min-height: 300px; | |
| } | |
| .panel:first-child { | |
| order: 2; | |
| } | |
| .panel:nth-child(2) { | |
| order: 1; | |
| min-height: 50vh; | |
| } | |
| .panel:last-child { | |
| order: 3; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <div class="brand"><i class="fa-solid fa-city"></i> Sin City 3000</div> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-link">Built with | |
| anycoder</a> | |
| </header> | |
| <main> | |
| <!-- Left Panel: Character Stats --> | |
| <aside class="panel"> | |
| <div class="panel-header"> | |
| <span><i class="fa-solid fa-user-astronaut"></i> Status</span> | |
| <span id="player-class">Mercenary</span> | |
| </div> | |
| <div class="stat-row"> | |
| <span class="stat-label">HP</span> | |
| <div style="flex:1; margin-left: 10px; text-align: right;"> | |
| <span id="hp-val" class="stat-value">100/100</span> | |
| <div class="health-bar-container"> | |
| <div id="hp-bar" class="health-bar-fill"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="stat-row"> | |
| <span class="stat-label">Credits</span> | |
| <span class="stat-value" style="color: var(--neon-yellow);">¥ <span id="credits-val">50</span></span> | |
| </div> | |
| <div class="stat-row"> | |
| <span class="stat-label">Street Cred</span> | |
| <span class="stat-value" style="color: var(--neon-pink);"><span id="rep-val">0</span></span> | |
| </div> | |
| <div class="panel-header" style="margin-top: 1rem;"> | |
| <span><i class="fa-solid fa-microchip"></i> Inventory</span> | |
| </div> | |
| <ul class="inventory-list" id="inventory-list"> | |
| <!-- Items injected here --> | |
| </ul> | |
| </aside> | |
| <!-- Center Panel: Narrative --> | |
| <section class="panel"> | |
| <div class="panel-header"> | |
| <span><i class="fa-solid fa-terminal"></i> Neural Link</span> | |
| <span style="font-size: 0.8rem; opacity: 0.7;">V.3.0.1 connected</span> | |
| </div> | |
| <div class="scene-display" id="game-log"> | |
| <!-- Game text goes here --> | |
| </div> | |
| <div class="actions-grid" id="actions-container"> | |
| <!-- Buttons injected here --> | |
| </div> | |
| </section> | |
| <!-- Right Panel: World Data --> | |
| <aside class="panel"> | |
| <div class="panel-header"> | |
| <span><i class="fa-solid fa-satellite-dish"></i> Environment</span> | |
| </div> | |
| <div class="visual-feed"> | |
| <div class="grid-bg"></div> | |
| <i id="scene-icon" class="fa-solid fa-biohazard visual-icon"></i> | |
| </div> | |
| <div class="panel-header" style="font-size: 1rem;">System Log</div> | |
| <div class="status-log" id="system-log"> | |
| <div class="status-line">> Initializing biosystems... OK</div> | |
| <div class="status-line">> Connecting to CityNet... OK</div> | |
| <div class="status-line">> Location detected: Sector 7 Slums</div> | |
| <div class="status-line">> Weather: Acid Rain</div> | |
| </div> | |
| </aside> | |
| </main> | |
| <script> | |
| // --- Game State --- | |
| const gameState = { | |
| hp: 100, | |
| maxHp: 100, | |
| credits: 100, | |
| rep: 10, | |
| inventory: ['Data Deck', 'Stimpak'], | |
| flags: {}, // For story tracking | |
| currentScene: 'start' | |
| }; | |
| // --- Story Data --- | |
| const scenes = { | |
| 'start': { | |
| text: "You wake up face down in a puddle of neon-reflecting sludge. Your head is throbbing—a side effect of the cheap neural implant you installed last night. The alleyway smells of ozone and rotting synth-meat. Sirens wail in the distance, growing louder. You need to move.", | |
| icon: "fa-skull", | |
| choices: [ | |
| { text: "Check pockets & run", action: 'flee_alley' }, | |
| { text: "Hide in the dumpster", action: 'hide_dumpster' }, | |
| { text: "Scan for network signals", action: 'scan_network', req: { item: 'Data Deck' } } | |
| ] | |
| }, | |
| 'flee_alley': { | |
| text: "You scramble to your feet and sprint towards the main street. A flying patrol drone sweeps a red laser across the spot you just left. Close call. You emerge into the neon chaos of Sector 7 Market. Hawkers are selling everything from illegal memory chips to synthetic organs.", | |
| icon: "fa-person-running", | |
| choices: [ | |
| { text: "Visit the Ripperdoc", action: 'ripperdoc' }, | |
| { text: "Enter the 'Binary Bar'", action: 'bar' }, | |
| { text: "Look for a job board", action: 'job_board' } | |
| ] | |
| }, | |
| 'hide_dumpster': { | |
| text: "You dive into the trash. It's gross, but effective. Two NCPD officers run past, weapons drawn. Once they're gone, you climb out. You find a discarded energy cell in the trash.", | |
| icon: "fa-trash-can", | |
| effect: () => { addItem('Energy Cell'); }, | |
| choices: [ | |
| { text: "Go to the Market", action: 'flee_alley' } | |
| ] | |
| }, | |
| 'scan_network': { | |
| text: "You jack into a loose cable hanging from the wall. Your vision floods with green code. You intercept a police dispatch: 'Suspect heading to Sector 7'. You also manage to siphon a few credits from an unsecured vending machine nearby.", | |
| icon: "fa-wifi", | |
| effect: () => { addCredits(50); addRep(5); }, | |
| choices: [ | |
| { text: "Disconnect & Run", action: 'flee_alley' } | |
| ] | |
| }, | |
| 'ripperdoc': { | |
| text: "Dr. Chrome's clinic is a blood-stained basement. He looks at you with cybernetic eyes that zoom in and out. 'You look like slag, kid. Need a tune-up? Or maybe something... lethal?'", | |
| icon: "fa-user-doctor", | |
| choices: [ | |
| { text: "Heal (50 Credits)", action: 'heal', cost: 50 }, | |
| { text: "Buy Combat Chip (200 Credits)", action: 'buy_chip', cost: 200 }, | |
| { text: "Leave", action: 'hub_return' } | |
| ] | |
| }, | |
| 'bar': { | |
| text: "The Binary Bar is loud. Bass thumps in your chest. A holographic dancer flickers on the stage. A shady fixer in the corner signals you over.", | |
| icon: "fa-martini-glass-citrus", | |
| choices: [ | |
| { text: "Talk to the Fixer", action: 'talk_fixer' }, | |
| { text: "Buy a drink (10 Credits)", action: 'buy_drink', cost: 10 }, | |
| { text: "Leave", action: 'hub_return' } | |
| ] | |
| }, | |
| 'talk_fixer': { | |
| text: "'Name's Vex. I got a job. A corpo courier is passing through the lower levels. Intercept him, get the package. Pays 500. Interested?'", | |
| icon: "fa-user-secret", | |
| choices: [ | |
| { text: "Accept Job", action: 'mission_courier' }, | |
| { text: "Refuse", action: 'hub_return' } | |
| ] | |
| }, | |
| 'mission_courier': { | |
| text: "You ambush the courier in a transit tunnel. He draws a pistol. Combat initiated!", | |
| icon: "fa-gun", | |
| type: 'combat', | |
| enemy: { name: 'Corpo Courier', hp: 30, damage: 10 }, | |
| choices: [ | |
| { text: "Attack with Fists", action: 'combat_attack' }, | |
| { text: "Use Stimpak", action: 'combat_heal', req: { item: 'Stimpak' } }, | |
| { text: "Hack his gun", action: 'combat_hack', req: { item: 'Data Deck' } } | |
| ] | |
| }, | |
| 'job_board': { | |
| text: "The digital board flickers. Most jobs are trash collection or organ harvesting. One stands out: 'Beta Tester needed for experimental reflex booster. High risk.'", | |
| icon: "fa-clipboard-list", | |
| choices: [ | |
| { text: "Take the risk", action: 'experiment' }, | |
| { text: "Back to Market", action: 'hub_return' } | |
| ] | |
| }, | |
| 'experiment': { | |
| text: "You plug into the test terminal. A surge of electricity fries your nerves but rewires your reflexes. You take 20 damage but gain Street Cred.", | |
| icon: "fa-bolt", | |
| effect: () => { takeDamage(20); addRep(20); }, | |
| choices: [ | |
| { text: "Stumble away", action: 'hub_return' } | |
| ] | |
| }, | |
| 'hub_return': { | |
| text: "You are back in the center of Sector 7 Market. The neon lights buzz overhead.", | |
| icon: "fa-store", | |
| choices: [ | |
| { text: "Visit Ripperdoc", action: 'ripperdoc' }, | |
| { text: "Visit Bar", action: 'bar' }, | |
| { text: "Job Board", action: 'job_board' } | |
| ] | |
| }, | |
| 'game_over': { | |
| text: "CRITICAL SYSTEM FAILURE. Your vitals have ceased. Your story ends here in the gutter of Sin City 3000.", | |
| icon: "fa-skull-crossbones", | |
| choices: [ | |
| { text: "Reboot System", action: 'restart' } | |
| ] | |
| }, | |
| 'combat_win': { | |
| text: "The courier drops. You grab the package. It's heavy. Inside is a prototype chip. You wire Vex, and the credits transfer instantly.", | |
| icon: "fa-trophy", | |
| effect: () => { addCredits(500); addRep(50); addItem('Proto-Chip'); }, | |
| choices: [ | |
| { text: "Return to Market", action: 'hub_return' } | |
| ] | |
| } | |
| }; | |
| // --- Engine Functions --- | |
| function initGame() { | |
| updateHUD(); | |
| renderScene('start'); | |
| logSystem("System initialized. User: Guest."); | |
| } | |
| function updateHUD() { | |
| // Stats | |
| document.getElementById('hp-val').innerText = `${gameState.hp}/${gameState.maxHp}`; | |
| document.getElementById('hp-bar').style.width = `${(gameState.hp / gameState.maxHp) * 100}%`; | |
| document.getElementById('credits-val').innerText = gameState.credits; | |
| document.getElementById('rep-val').innerText = gameState.rep; | |
| // Inventory | |
| const invList = document.getElementById('inventory-list'); | |
| invList.innerHTML = ''; | |
| if(gameState.inventory.length === 0) { | |
| invList.innerHTML = '<li class="inventory-item" style="opacity:0.5">Empty</li>'; | |
| } else { | |
| gameState.inventory.forEach(item => { | |
| const li = document.createElement('li'); | |
| li.className = 'inventory-item'; | |
| let icon = 'fa-box'; | |
| if(item === 'Data Deck') icon = 'fa-laptop-code'; | |
| if(item === 'Stimpak') icon = 'fa-syringe'; | |
| if(item === 'Energy Cell') icon = 'fa-battery-full'; | |
| li.innerHTML = `<i class="fa-solid ${icon}"></i> ${item}`; | |
| invList.appendChild(li); | |
| }); | |
| } | |
| } | |
| function logSystem(msg) { | |
| const log = document.getElementById('system-log'); | |
| const div = document.createElement('div'); | |
| div.className = 'status-line'; | |
| div.innerText = `> ${msg}`; | |
| log.prepend(div); | |
| if(log.children.length > 6) log.lastChild.remove(); | |
| } | |
| function addItem(item) { | |
| if(!gameState.inventory.includes(item)) { | |
| gameState.inventory.push(item); | |
| logText(`System: Acquired [${item}]`, 'system'); | |
| updateHUD(); | |
| } | |
| } | |
| function removeItem(item) { | |
| const index = gameState.inventory.indexOf(item); | |
| if (index > -1) { | |
| gameState.inventory.splice(index, 1); | |
| updateHUD(); | |
| } | |
| } | |
| function addCredits(amount) { | |
| gameState.credits += amount; | |
| logText(`System: Transfer received ¥${amount}`, 'system'); | |
| updateHUD(); | |
| } | |
| function addRep(amount) { | |
| gameState.rep += amount; | |
| logText(`System: Street Cred increased +${amount}`, 'system'); | |
| updateHUD(); | |
| } | |
| function takeDamage(amount) { | |
| gameState.hp -= amount; | |
| if(gameState.hp <= 0) { | |
| gameState.hp = 0; | |
| renderScene('game_over'); | |
| } | |
| updateHUD(); | |
| // Flash red | |
| document.body.style.boxShadow = "inset 0 0 50px red"; | |
| setTimeout(() => document.body.style.boxShadow = "none", 200); | |
| } | |
| function heal(amount) { | |
| gameState.hp = Math.min(gameState.maxHp, gameState.hp + amount); | |
| updateHUD(); | |
| } | |
| // --- Rendering --- | |
| function logText(text, type = 'narrative') { | |
| const display = document.getElementById('game-log'); | |
| const div = document.createElement('div'); | |
| div.className = `log-entry ${type}`; | |
| if(type === 'narrative') { | |
| div.classList.add('typing-cursor'); | |
| // Typewriter effect | |
| let i = 0; | |
| const speed = 20; | |
| function typeWriter() { | |
| if (i < text.length) { | |
| div.textContent += text.charAt(i); | |
| i++; | |
| display.scrollTop = display.scrollHeight; // Auto scroll | |
| setTimeout(typeWriter, speed); | |
| } else { | |
| div.classList.remove('typing-cursor'); | |
| } | |
| } | |
| display.appendChild(div); | |
| typeWriter(); | |
| } else { | |
| div.textContent = text; | |
| display.appendChild(div); | |
| display.scrollTop = display.scrollHeight; | |
| } | |
| } | |
| function renderScene(sceneKey) { | |
| const scene = scenes[sceneKey]; | |
| gameState.currentScene = sceneKey; | |
| // Update visual | |
| const iconElement = document.getElementById('scene-icon'); | |
| iconElement.className = `fa-solid ${scene.icon || 'fa-circle-question'} visual-icon`; | |
| // Execute effects if any | |
| if(scene.effect) { | |
| scene.effect(); | |
| // If effect killed player, stop here | |
| if(gameState.hp <= 0) return; | |
| } | |
| // Log text | |
| logText(scene.text); | |
| // Render Buttons | |
| const actionsContainer = document.getElementById('actions-container'); | |
| actionsContainer.innerHTML = ''; | |
| if (scene.type === 'combat') { | |
| handleCombat(scene); | |
| return; | |
| } | |
| scene.choices.forEach(choice => { | |
| const btn = document.createElement('button'); | |
| btn.className = 'action-btn'; | |
| let label = choice.text; | |
| let disabled = false; | |
| // Check requirements | |
| if(choice.req) { | |
| if(choice.req.item && !gameState.inventory.includes(choice.req.item)) { | |
| disabled = true; | |
| label += ` [Req: ${choice.req.item}]`; | |
| } | |
| } | |
| // Check costs | |
| if(choice.cost) { | |
| if(gameState.credits < choice.cost) { | |
| disabled = true; | |
| } | |
| } | |
| btn.innerText = label; | |
| btn.disabled = disabled; | |
| btn.onclick = () => { | |
| if(choice.cost) addCredits(-choice.cost); | |
| if(choice.action === 'restart') { | |
| location.reload(); | |
| } else { | |
| renderScene(choice.action); | |
| } | |
| }; | |
| actionsContainer.appendChild(btn); | |
| }); | |
| } | |
| // Simple Combat Logic | |
| let currentEnemy = null; | |
| function handleCombat(scene) { | |
| currentEnemy = { ...scene.enemy }; // Clone enemy | |
| const actionsContainer = document.getElementById('actions-container'); | |
| // Create attack buttons dynamically based on loop | |
| renderCombatButtons(scene); | |
| } | |
| function renderCombatButtons(scene) { | |
| const actionsContainer = document.getElementById('actions-container'); | |
| actionsContainer.innerHTML = ''; | |
| scene.choices.forEach(choice => { | |
| const btn = document.createElement('button'); | |
| btn.className = 'action-btn'; | |
| let label = choice.text; | |
| let disabled = false; | |
| if(choice.req && choice.req.item && !gameState.inventory.includes(choice.req.item)) { | |
| disabled = true; | |
| label += ` [Missing: ${choice.req.item}]`; | |
| } | |
| btn.innerText = label; | |
| btn.disabled = disabled; | |
| btn.onclick = () => { | |
| combatRound(choice.action, scene); | |
| }; | |
| actionsContainer.appendChild(btn); | |
| }); | |
| } | |
| function combatRound(action, scene) { | |
| // Player Turn | |
| let playerDmg = 0; | |
| let logMsg = ""; | |
| if(action === 'combat_attack') { | |
| playerDmg = Math.floor(Math.random() * 10) + 5; | |
| logMsg = `You punch the enemy for ${playerDmg} damage!`; | |
| } else if (action === 'combat_heal') { | |
| heal(30); | |
| removeItem('Stimpak'); | |
| logMsg = "You injected a Stimpak. +30 HP."; | |
| } else if (action === 'combat_hack') { | |
| playerDmg = 20; | |
| logMsg = "Hack successful! Weapon backfire caused 20 damage."; | |
| } | |
| if(playerDmg > 0) { | |
| currentEnemy.hp -= playerDmg; | |
| logText(logMsg, 'danger'); | |
| } else { | |
| logText(logMsg, 'system'); | |
| } | |
| // Check Enemy Death | |
| if(currentEnemy.hp <= 0) { | |
| logText("Enemy defeated!", 'system'); | |
| setTimeout(() => renderScene('combat_win'), 1000); | |
| return; | |
| } | |
| // Enemy Turn | |
| setTimeout(() => { | |
| const enemyDmg = Math.floor(Math.random() * currentEnemy.damage); | |
| takeDamage(enemyDmg); | |
| logText(`${currentEnemy.name} hits you for ${enemyDmg} damage!`, 'danger'); | |
| if(gameState.hp > 0) { | |
| renderCombatButtons(scene); // Refresh buttons | |
| } | |
| }, 800); | |
| } | |
| // Boot | |
| window.onload = initGame; | |
| </script> | |
| </body> | |
| </html> |