Roanoke RPG

https://svonberg.org/wp-content/uploads/2025/12/nokerpg5.html

Extremely early development, building a Roanoke VA Adventure, basic walk around and combat, some mood lighting,  magic items and local legends


<!DOCTYPE html>
<html lang=”en”>
<head>
    <meta charset=”UTF-8″>
    <meta name=”viewport” content=”width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no”>
    <title>Shadows of the Star City: Landmarks Update</title>
    <style>
        :root {
            –bg-color: #1a1a2e;
            –text-color: #e0e0e0;
            –panel-bg: #16213e;
            –highlight: #4ecca3;
            –accent: #0f3460;
            –danger: #e94560;
            –warning: #f7b731;
            –rare: #d633ff;
            –dr-pepper: #71161d;
            –star-white: #ffffff;
        }

        * {
            box-sizing: border-box;
            margin: 0;
            padding: 0;
            font-family: ‘Courier New’, Courier, monospace;
            -webkit-tap-highlight-color: transparent;
        }

        body {
            background-color: var(–bg-color);
            color: var(–text-color);
            height: 100dvh;
            width: 100vw;
            display: flex;
            flex-direction: column;
            overflow: hidden;
        }

        /* — Header — */
        header {
            background-color: var(–accent);
            padding: 8px 15px;
            text-align: center;
            border-bottom: 2px solid var(–highlight);
            flex: 0 0 auto;
            display: flex;
            justify-content: space-between;
            align-items: center;
            z-index: 10;
        }

        h1 {
            font-size: 1rem;
            color: var(–highlight);
            text-transform: uppercase;
            letter-spacing: 1px;
            white-space: nowrap;
        }

        /* — Main Layout — */
        #game-container {
            flex: 1;
            display: flex;
            flex-direction: column;
            width: 100%;
            height: 100%;
            overflow: hidden;
        }

        /* — Dashboard (Visuals & Stats) — */
        #dashboard {
            flex: 0 0 auto;
            background-color: var(–panel-bg);
            border-bottom: 1px solid var(–accent);
        }

        #location-visual {
            width: 100%;
            height: 120px;
            background: #000;
            display: flex;
            align-items: center;
            justify-content: center;
            position: relative;
            overflow: hidden;
            transition: background 0.5s;
        }

        /* Dynamic CSS Backgrounds for Locations */
        .scene-mountain {
            /* Mill Mountain Star – dark sky, blue glow */
            background: linear-gradient(to bottom, #071328 0%, #16213e 100%);
            border-top: 5px solid var(–star-white);
        }
        .scene-city {
            /* Downtown – dark buildings, street lights */
            background: linear-gradient(to bottom, #000 0%, #2c3e50 100%);
            box-shadow: inset 0 0 10px rgba(255, 165, 0, 0.5); /* Orange/Yellow city glow */
        }
        .scene-museum {
            /* Transportation Museum – Steel, rust, green 611 */
            background: linear-gradient(180deg, #3a3f44 0%, #000 100%);
            border-left: 5px solid #1b5e20; /* 611 Green */
        }
        .scene-hotel {
            /* Hotel Roanoke – Tudor structure, gold/yellow light */
            background: linear-gradient(to bottom, #5d4037 0%, #1a1a2e 100%);
            border-bottom: 5px solid #ffcc00; /* Gold */
        }
        .scene-drpepper {
            /* Dr Pepper Sign – Red Neon */
            background: radial-gradient(circle, var(–dr-pepper) 10%, #000 90%);
            border: 3px solid #ff0000;
            box-shadow: inset 0 0 20px #ff0000;
        }
        .scene-coffeepot {
            /* Coffee Pot – Log cabin wood */
            background: repeating-linear-gradient(0deg, #4e342e, #4e342e 10px, #3e2723 10px, #3e2723 20px);
            border-top: 5px solid #d7ccc8; /* Coffee mug top */
        }
        .scene-village {
            /* Grandin Village – Historic, trees, movie theater feel */
            background: linear-gradient(to bottom, #424242 0%, #212121 100%);
            border-bottom: 3px solid #fbc02d; /* Marquee yellow */
        }
        .scene-misfit {
            /* Misfit Beauty – Neon, dark, Nehi mural */
            background: repeating-linear-gradient(90deg, #111, #111 20px, #6a1b9a 20px, #6a1b9a 40px);
            border-bottom: 4px solid #ff00ff; /* Misfit Pink/Purple */
            box-shadow: 0 0 15px #6a1b9a;
        }
        .scene-dragon {
            /* Dragon Bite – Books and green dragon */
            background: linear-gradient(135deg, #2e7d32 0%, #000 100%);
            box-shadow: 0 0 10px rgba(255, 255, 255, 0.1); /* Glow from the books */
        }
        .scene-rainy {
            /* Rainy Day Reads/16 West – Indoors, dark, dusty, wet outside */
            background: linear-gradient(to bottom, #37474f 0%, #1c262a 100%);
            border: 3px solid #90caf9; /* Light blue/rain */
            color: #f5f5f5;
        }

        #visual-text {
            position: absolute;
            bottom: 5px;
            right: 5px;
            font-size: 0.75rem;
            background: rgba(0,0,0,0.7);
            padding: 4px 8px;
            border-radius: 4px;
            border: 1px solid var(–accent);
        }

        /* Stats Bar */
        #stats-bar {
            padding: 8px;
            display: flex;
            justify-content: space-between;
            align-items: center;
            background: rgba(0,0,0,0.2);
            font-size: 0.8rem;
        }

        .hp-container {
            width: 100px;
            margin-left: 10px;
        }

        .health-bar {
            width: 100%;
            height: 6px;
            background: #333;
            margin-top: 2px;
            border-radius: 3px;
            overflow: hidden;
        }

        .health-fill {
            height: 100%;
            background: var(–danger);
            width: 100%;
            transition: width 0.3s;
        }

        #inv-toggle {
            background: none;
            border: 1px solid var(–accent);
            color: #888;
            padding: 4px 8px;
            font-size: 0.7rem;
            cursor: pointer;
            border-radius: 4px;
        }

        /* — Scrollable Log — */
        #log-area {
            flex: 1;
            padding: 15px;
            overflow-y: auto;
            background-color: var(–bg-color);
            display: flex;
            flex-direction: column;
            gap: 10px;
            scroll-behavior: smooth;
        }

        .log-entry {
            font-size: 0.95rem;
            line-height: 1.4;
            opacity: 0;
            animation: fadeIn 0.3s forwards;
            padding-bottom: 8px;
            border-bottom: 1px solid rgba(255,255,255,0.05);
        }

        .log-entry.combat { color: var(–danger); font-weight: bold; border-left: 3px solid var(–danger); padding-left: 8px; }
        .log-entry.gain { color: var(–highlight); }
        .log-entry.lore { color: var(–rare); font-style: italic; }
        .log-entry.info { color: #88ccff; }
        .log-entry.story { color: #dcdcdc; }
        .log-entry.fizzy { color: #ff9999; text-shadow: 0 0 5px red; }

        @keyframes fadeIn { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } }

        /* — Controls — */
        #controls {
            flex: 0 0 auto;
            background-color: var(–panel-bg);
            padding: 10px;
            border-top: 1px solid var(–accent);
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 8px;
            min-height: 90px;
            padding-bottom: max(10px, env(safe-area-inset-bottom));
        }

        button {
            background-color: var(–accent);
            color: var(–text-color);
            border: 1px solid var(–highlight);
            padding: 12px 5px;
            cursor: pointer;
            font-weight: bold;
            font-size: 0.95rem;
            border-radius: 4px;
            transition: background 0.1s;
            touch-action: manipulation;
            display: flex;
            align-items: center;
            justify-content: center;
            text-align: center;
            line-height: 1.1;
        }

        button:active {
            background-color: var(–highlight);
            color: var(–bg-color);
        }

        /* — Overlays — */
        .overlay {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(26, 26, 46, 0.98);
            z-index: 100;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            padding: 20px;
            text-align: center;
        }

        .overlay.hidden { display: none; }

        .class-select {
            display: flex;
            flex-direction: column;
            gap: 12px;
            width: 100%;
            max-width: 400px;
            margin-top: 20px;
            overflow-y: auto;
            max-height: 60vh;
        }

        .class-card {
            background: var(–panel-bg);
            border: 2px solid var(–accent);
            padding: 15px;
            width: 100%;
            border-radius: 8px;
            text-align: left;
        }
       
        /* Inventory List */
        #inventory-overlay {
            justify-content: flex-start;
            padding-top: 60px;
        }

        #inventory-list {
            width: 100%;
            max-width: 400px;
            list-style: none;
            overflow-y: auto;
            padding-bottom: 20px;
        }

        #inventory-list li {
            background: var(–panel-bg);
            border: 1px solid var(–accent);
            padding: 12px;
            margin-bottom: 8px;
            display: flex;
            justify-content: space-between;
            align-items: center;
            border-radius: 4px;
        }

        #close-inv {
            position: absolute;
            top: 15px;
            right: 15px;
            background: var(–danger);
            border: none;
            padding: 10px 20px;
            width: auto;
        }

    </style>
</head>
<body>

    <header>
        <div style=”font-size:1.2rem”>★</div>
        <h1>Shadows of Star City</h1>
        <div style=”font-size:1.2rem”>★</div>
    </header>

    <div id=”game-container”>
        <div id=”dashboard”>
            <div id=”location-visual” class=”scene-city”>
                <div id=”visual-text”>Downtown</div>
            </div>
           
            <div id=”stats-bar”>
                <div style=”display:flex; align-items:center; gap:10px;”>
                    <span id=”player-class” style=”color:var(–highlight)”>Worker</span>
                    <span>Lvl <span id=”player-level”>1</span></span>
                </div>
               
                <div style=”display:flex; align-items:center;”>
                    <button id=”inv-toggle” onclick=”toggleInventory()”>BAG</button>
                    <div class=”hp-container”>
                        <div style=”display:flex; justify-content:space-between; font-size:0.7rem; margin-bottom:2px;”>
                            <span>HP</span>
                            <span id=”hp-text”>100</span>
                        </div>
                        <div class=”health-bar”>
                            <div class=”health-fill” id=”hp-fill”></div>
                        </div>
                    </div>
                </div>
            </div>
        </div>

        <main id=”log-area”>
            <div class=”log-entry story”>Welcome to Roanoke. The Star is dark. The shadows are long.</div>
        </main>

        <div id=”controls”></div>
    </div>

    <div id=”inventory-overlay” class=”overlay hidden”>
        <button id=”close-inv” onclick=”toggleInventory()”>Close</button>
        <h2 style=”margin-bottom:20px; color:var(–highlight)”>Backpack</h2>
        <ul id=”inventory-list”></ul>
        <p id=”empty-inv-msg” style=”color:#666; margin-top:20px;”>Your bag is empty.</p>
    </div>

    <div id=”start-screen” class=”overlay”>
        <h1 style=”font-size: 1.8rem; color: var(–star-white); text-shadow: 0 0 10px var(–highlight);”>Shadows of<br>Star City</h1>
        <p style=”font-size: 0.9rem; max-width: 300px; color:#aaa; margin: 10px 0;”>
            Restore the light to Mill Mountain before the ghosts of the past reclaim Roanoke.
        </p>
        <div class=”class-select”>
            <div class=”class-card” onclick=”startGame(‘Railroad Worker’)”>
                <h3 style=”color:var(–warning)”>Railroad Worker</h3>
                <small>High HP • Heavy Wrench</small>
            </div>
            <div class=”class-card” onclick=”startGame(‘Blue Ridge Hiker’)”>
                <h3 style=”color:var(–highlight)”>Blue Ridge Hiker</h3>
                <small>Agile • Hiking Stick • Trail Mix</small>
            </div>
            <div class=”class-card” onclick=”startGame(‘Local Historian’)”>
                <h3 style=”color:#88ccff”>Local Historian</h3>
                <small>Lore Bonus • Mad Stone • Map</small>
            </div>
        </div>
    </div>

    <script>
        // — Game State —
        const state = {
            player: {
                class: ”,
                hp: 100,
                maxHp: 100,
                dmg: 10,
                defense: 0,
                inventory: [],
                xp: 0,
                level: 1,
                money: 10
            },
            currentLocation: ‘downtown’,
            inCombat: false,
            currentEnemy: null,
            flags: {
                starLit: false,
                hasTube: false,
                drPepperPower: 0
            }
        };

        // — Data —
        const locations = {
            ‘downtown’: {
                name: “Downtown Hub”,
                desc: “The center of the Star City. From here, all roads diverge, stretching towards Mill Mountain and beyond.”,
                visualClass: “scene-city”,
                actions: [
                    { label: “Go to Grandin Village”, target: “grand_village” },
                    { label: “Mill Mountain (Up)”, target: “mill_mountain” },
                    { label: “Hotel Roanoke (North)”, target: “hotel” },
                    { label: “Transp. Museum (Near)”, target: “museum” },
                    { label: “Dr Pepper Sign (East)”, target: “dr_pepper” },
                    { label: “The Coffee Pot (West)”, target: “coffee_pot” }
                ],
                enemies: [“Market Rat”, “Lost Tourist”]
            },
            ‘grand_village’: {
                name: “Grandin Village”,
                desc: “A historic avenue and neighborhood center, connected to Downtown by the Memorial Bridge.”,
                visualClass: “scene-village”,
                actions: [
                    { label: “Return Downtown”, target: “downtown” },
                    { label: “Misfit Beauty”, target: “misfit” },
                    { label: “Dragon Bite”, target: “dragon_bite” },
                    { label: “16 West Marketplace”, target: “rainy_day_entry” } /* New intermediary entry point */
                ],
                enemies: [“Lost Tourist”, “Street Performer Ghost”]
            },
            ‘dr_pepper’: {
                name: “Dr Pepper Sign”,
                desc: “10-2-4. The massive neon sign hums with electric energy, casting a red glow over the eastern street.”,
                visualClass: “scene-drpepper”,
                actions: [
                    { label: “Return Downtown”, target: “downtown” },
                    { label: “Absorb 10-2-4”, action: “absorb_neon” },
                    { label: “Search Rooftop”, action: “search_roof” }
                ],
                enemies: [“Neon Wisp”, “Sugar Imp”]
            },
            ‘coffee_pot’: {
                name: “The Coffee Pot”,
                desc: “The giant log cabin building, shaped like a coffee pot, still stands on Salem Avenue. Rockabilly music thumps from inside.”,
                visualClass: “scene-coffeepot”,
                actions: [
                    { label: “Return Downtown”, target: “downtown” },
                    { label: “Enter Roadhouse”, action: “enter_pot” },
                    { label: “Check Spout”, action: “check_spout” }
                ],
                enemies: [“Roadhouse Brawler”, “Rockabilly Ghost”]
            },
            ‘misfit’: {
                name: “Misfit Beauty”,
                desc: “A vintage salon. Outside, the large Nehi mural covers the wall. The mirrors inside reflect things that aren’t standing in front of them.”,
                visualClass: “scene-misfit”,
                actions: [
                    { label: “Back to Village”, target: “grand_village” },
                    { label: “Get Styled”, action: “style_hair” },
                    { label: “Check Mirrors”, action: “check_mirrors” }
                ],
                enemies: [“Scissorcomb Critter”, “Phantom Stylist”]
            },
            ‘dragon_bite’: {
                name: “Dragon Bite Books”,
                desc: “Books & Crafts. A plastic dragon in the window seems to be watching you. Shelves are stacked with games and yarn.”,
                visualClass: “scene-dragon”,
                actions: [
                    { label: “Back to Village”, target: “grand_village” },
                    { label: “Roll for Luck”, action: “roll_dice” },
                    { label: “Inspect Yarn”, action: “inspect_yarn” }
                ],
                enemies: [“Yarn Golem”, “Rule Lawyer”]
            },
            ‘rainy_day_entry’: {
                name: “16 West Marketplace”,
                desc: “A historic building now housing small businesses. You see the sign for a bookstore inside.”,
                visualClass: “scene-rainy”,
                actions: [
                    { label: “Back to Village”, target: “grand_village” },
                    { label: “Enter Bookstore”, target: “rainy_day” }
                ],
                enemies: [“Dust Spirit”]
            },
            ‘rainy_day’: {
                name: “Rainy Day Reads”,
                desc: “Located deep inside 16 West Marketplace, this is a labyrinth of used books. It smells of old paper, coffee, and rain.”,
                visualClass: “scene-rainy”,
                actions: [
                    { label: “Exit Marketplace”, target: “grand_village” },
                    { label: “Read Quietly”, action: “read_book” },
                    { label: “Search Stacks”, action: “search_stacks” }
                ],
                enemies: [“Dust Spirit”, “Silence Keeper”]
            },
            ‘hotel’: {
                name: “Hotel Roanoke”,
                desc: “The Tudor-style giant on the hill, overlooking the city center. The crystal ballroom is silent, but footsteps echo in the halls.”,
                visualClass: “scene-hotel”,
                actions: [
                    { label: “Return Downtown”, target: “downtown” },
                    { label: “Explore Lobby”, action: “explore_hotel” },
                    { label: “Kitchens”, action: “kitchen” }
                ],
                enemies: [“Spectral Bellhop”, “Ghostly Bride”]
            },
            ‘museum’: {
                name: “Transp. Museum”,
                desc: “Steam giants sleep here, just south of the tracks. The J-611 sits majestic and silent.”,
                visualClass: “scene-museum”,
                actions: [
                    { label: “Return Downtown”, target: “downtown” },
                    { label: “Search 611”, action: “search_train” },
                    { label: “Read Plaque”, action: “lore_train” }
                ],
                enemies: [“Phantom Conductor”, “Rust Golem”]
            },
            ‘mill_mountain’: {
                name: “Mill Mountain”,
                desc: “The Star is dark. You have climbed high above the city. The lights below twinkle like a sea of diamonds.”,
                visualClass: “scene-mountain”,
                actions: [
                    { label: “Go Downtown (Down)”, target: “downtown” },
                    { label: “Inspect Star”, action: “inspect_star” },
                    { label: “Hike Trail”, action: “hike” }
                ],
                enemies: [“Shadow Wolf”, “Woman in Black”]
            }
        };

        const enemies = {
            “Market Rat”: { hp: 20, dmg: 5, xp: 10, drop: “Bagel” },
            “Lost Tourist”: { hp: 30, dmg: 4, xp: 15, drop: “Camera” },
            “Spectral Bellhop”: { hp: 45, dmg: 8, xp: 25, drop: “Room Key” },
            “Ghostly Bride”: { hp: 50, dmg: 9, xp: 30, drop: “Silver Ring” },
            “Roadhouse Brawler”: { hp: 60, dmg: 10, xp: 35, drop: “Broken Bottle” },
            “Rockabilly Ghost”: { hp: 55, dmg: 8, xp: 30, drop: “Vintage Comb” },
            “Neon Wisp”: { hp: 40, dmg: 12, xp: 25, drop: “Neon Shard” },
            “Sugar Imp”: { hp: 30, dmg: 6, xp: 20, drop: “Syrup Packet” },
            “Phantom Conductor”: { hp: 60, dmg: 10, xp: 40, drop: “Gold Watch” },
            “Rust Golem”: { hp: 80, dmg: 6, xp: 50, drop: “Railroad Spike” },
            “Shadow Wolf”: { hp: 45, dmg: 8, xp: 25, drop: “Wolf Pelt” },
            “Street Performer Ghost”: { hp: 35, dmg: 6, xp: 20, drop: “Guitar Pick” },
            “Scissorcomb Critter”: { hp: 40, dmg: 8, xp: 25, drop: “Wig” },
            “Phantom Stylist”: { hp: 45, dmg: 9, xp: 30, drop: “Hairspray” },
            “Yarn Golem”: { hp: 50, dmg: 5, xp: 30, drop: “Knitting Needles” },
            “Rule Lawyer”: { hp: 55, dmg: 2, xp: 40, drop: “D20 Die” },
            “Dust Spirit”: { hp: 25, dmg: 4, xp: 15, drop: “Bookmark” },
            “Silence Keeper”: { hp: 60, dmg: 10, xp: 45, drop: “Ancient Tomes” },
            “Woman in Black”: { hp: 150, dmg: 15, xp: 500, drop: “Cursed Necklace”, isBoss: true }
        };

        const items = {
            “Drinking Vinegar”: { type: “heal”, value: 30, desc: “+30 HP” },
            “Bagel”: { type: “heal”, value: 10, desc: “+10 HP. Stale.” },
            “Roadhouse Brew”: { type: “heal”, value: 40, desc: “Strong coffee. +40 HP.” },
            “Spoon Bread”: { type: “heal”, value: 25, desc: “Hotel delicacy. +25 HP.” },
            “Trail Mix”: { type: “heal”, value: 20, desc: “+20 HP” },
            “Hot Cocoa”: { type: “heal”, value: 15, desc: “Warm and cozy. +15 HP.” },
            “Dr. Pepper”: { type: “buff”, value: 5, desc: “Sugar rush! +5 Max HP” },
            “Vintage Soda”: { type: “buff”, value: 10, desc: “Original recipe. +10 Max HP” },
            “Hairspray”: { type: “buff”, value: 2, desc: “Sticky defense. +2 Defense” },
            “D20 Die”: { type: “misc”, value: 0, desc: “A critical hit waiting to happen.” },
            “Mad Stone”: { type: “cure”, desc: “A legendary stone said to cure poison.” },
            “Railroad Spike”: { type: “weapon”, value: 5, desc: “+5 Dmg” },
            “Broken Bottle”: { type: “weapon”, value: 6, desc: “+6 Dmg” },
            “Knitting Needles”: { type: “weapon”, value: 4, desc: “+4 Dmg” },
            “Neon Tube”: { type: “key”, desc: “The missing piece for the Star.” }, // Updated description
            “Heavy Wrench”: { type: “weapon”, value: 0, desc: “Starting Tool” },
            “Hiking Stick”: { type: “weapon”, value: 0, desc: “Walking aid” },
            “Old Map”: { type: “misc”, value: 0, desc: “Shows secrets” }
        };

        // — Core Logic —

        function startGame(className) {
            document.getElementById(‘start-screen’).classList.add(‘hidden’);
           
            state.player.class = className;
            let shortName = className.split(‘ ‘)[1] || className;
            document.getElementById(‘player-class’).innerText = shortName;

            if (className === ‘Railroad Worker’) {
                state.player.maxHp = 120;
                state.player.hp = 120;
                state.player.dmg = 15;
                addItem(“Heavy Wrench”);
            } else if (className === ‘Blue Ridge Hiker’) {
                state.player.defense = 5;
                addItem(“Trail Mix”);
                addItem(“Hiking Stick”);
            } else if (className === ‘Local Historian’) {
                addItem(“Old Map”);
                addItem(“Mad Stone”);
                state.player.xp = 50;
            }

            updateStats();
            log(`You stand in Downtown Roanoke. The silence is heavy.`, ‘story’);
            renderLocation();
        }

        function log(msg, type = ‘info’) {
            const logArea = document.getElementById(‘log-area’);
            const entry = document.createElement(‘div’);
            entry.className = `log-entry ${type}`;
            entry.innerHTML = msg;
            logArea.appendChild(entry);
            setTimeout(() => { logArea.scrollTop = logArea.scrollHeight; }, 50);
        }

        function updateStats() {
            document.getElementById(‘hp-text’).innerText = `${state.player.hp}`;
            document.getElementById(‘hp-fill’).style.width = `${(state.player.hp / state.player.maxHp) * 100}%`;
            document.getElementById(‘player-level’).innerText = state.player.level;
            // No need to call renderInventory here, only when inventory is opened/closed
        }

        function toggleInventory() {
            const overlay = document.getElementById(‘inventory-overlay’);
            if (overlay.classList.contains(‘hidden’)) {
                overlay.classList.remove(‘hidden’);
                renderInventory();
            } else {
                overlay.classList.add(‘hidden’);
            }
        }

        function renderInventory() {
            const list = document.getElementById(‘inventory-list’);
            const emptyMsg = document.getElementById(’empty-inv-msg’);
            list.innerHTML = ”;

            if (state.player.inventory.length === 0) {
                emptyMsg.style.display = ‘block’;
                return;
            } else {
                emptyMsg.style.display = ‘none’;
            }

            state.player.inventory.forEach((itemName, index) => {
                const itemData = items[itemName] || { desc: “Unknown Item” };
                const li = document.createElement(‘li’);
                li.innerHTML = `<span><strong>${itemName}</strong><br><small style=”color:#aaa”>${itemData.desc}</small></span>`;
               
                const btn = document.createElement(‘button’);
                btn.innerText = “USE”;
                btn.style.padding = “6px 12px”;
                btn.style.fontSize = “0.75rem”;
                btn.onclick = (e) => { e.stopPropagation(); useItem(index); };
               
                li.appendChild(btn);
                list.appendChild(li);
            });
        }

        // NEW FUNCTION: Dedicated action for fixing the star
        function tryFixStar() {
            if (state.currentLocation === ‘mill_mountain’ && state.player.inventory.includes(“Neon Tube”)) {
                const tubeIndex = state.player.inventory.indexOf(“Neon Tube”);
                if (tubeIndex !== -1) {
                    state.player.inventory.splice(tubeIndex, 1);
                    updateStats();
                    winGame();
                }
            } else {
                log(“You need a Neon Tube to do this.”, ‘info’);
            }
        }

        function useItem(index) {
            const itemName = state.player.inventory[index];
            const itemData = items[itemName];
            let consumed = false;

            if (state.inCombat && (!itemData || itemData.type !== ‘heal’)) {
                 toggleInventory();
                 log(“Cannot use this during combat!”, ‘info’);
                 return;
            }

            if (itemData && itemData.type === ‘heal’) {
                const oldHp = state.player.hp;
                state.player.hp = Math.min(state.player.maxHp, state.player.hp + itemData.value);
                log(`Consumed ${itemName}. Restored ${state.player.hp – oldHp} HP.`, ‘gain’);
                consumed = true;
            } else if (itemData && itemData.type === ‘buff’) {
                if (itemName === “Hairspray”) {
                    state.player.defense += itemData.value;
                    log(`Applied Hairspray. You feel stickier and safer. Defense Up!`, ‘gain’);
                } else {
                    state.player.maxHp += itemData.value;
                    state.player.hp += itemData.value;
                    log(`Consumed ${itemName}. Max HP increased!`, ‘gain’);
                }
                consumed = true;
            } else if (itemName === “Neon Tube”) {
                // If they try to use the tube via the inventory, direct them to the main action
                toggleInventory();
                log(“You should use the Neon Tube by taking the ‘Fix Star’ action at Mill Mountain.”, ‘info’);
                return;
            } else if (itemData && itemData.type === “weapon”) {
                log(`You equip the ${itemName}. Damage increased!`, ‘gain’);
                state.player.dmg += itemData.value;
                consumed = true;
            } else if (itemName === “D20 Die”) {
                 let roll = Math.floor(Math.random() * 20) + 1;
                 if (roll === 20) {
                     log(“Natural 20! You feel invincible!”, ‘gain’);
                     state.player.maxHp += 20;
                     state.player.hp += 20;
                 } else if (roll === 1) {
                     log(“Critical Fail! You stub your toe.”, ‘combat’);
                     state.player.hp -= 10;
                 } else {
                     log(`You rolled a ${roll}. Nothing happens.`, ‘info’);
                 }
                 consumed = true;
            } else {
                log(`You inspect the ${itemName}.`, ‘info’);
            }

            if (consumed) {
                state.player.inventory.splice(index, 1);
                renderInventory();
                updateStats();
                toggleInventory();
            }
        }

        function renderLocation() {
            const loc = locations[state.currentLocation];
           
            const visual = document.getElementById(‘location-visual’);
            visual.className = ‘scene-city ‘ + loc.visualClass; // Keep scene-city base for general styling
            visual.className = loc.visualClass;
            document.getElementById(‘visual-text’).innerText = loc.name;

            log(`<strong>${loc.name}</strong><br>${loc.desc}`, ‘story’);

            const controls = document.getElementById(‘controls’);
            controls.innerHTML = ”;
           
            // — UI Improvement: Add “Fix Star” button when conditions are met —
            if (state.currentLocation === ‘mill_mountain’ && state.player.inventory.includes(“Neon Tube”)) {
                const fixBtn = document.createElement(‘button’);
                fixBtn.innerText = “★ FIX STAR! ★”;
                fixBtn.style.gridColumn = “span 2”;
                fixBtn.style.backgroundColor = “var(–rare)”;
                fixBtn.style.borderColor = “var(–star-white)”;
                fixBtn.style.fontSize = “1.2rem”;
                fixBtn.onclick = tryFixStar;
                controls.appendChild(fixBtn); // Add as the most important action
            }

            loc.actions.forEach(act => {
                const btn = document.createElement(‘button’);
                btn.innerText = act.label;
                btn.onclick = () => handleAction(act);
                controls.appendChild(btn);
            });
        }

        function handleAction(act) {
            if (act.target) {
                state.currentLocation = act.target;
                if (Math.random() < 0.25) {
                    triggerCombat();
                } else {
                    renderLocation();
                }
            }
            // — Custom Location Logics —
            else if (act.action === “absorb_neon”) {
                log(“You stand under the humming sign at 10, 2, or 4…”, ‘fizzy’);
                if (Math.random() > 0.6) {
                    log(“A surge of sugary energy! You found a Vintage Soda.”, ‘gain’);
                    addItem(“Vintage Soda”);
                } else {
                    log(“The sign buzzes angrily. A spark flies!”, ‘combat’);
                    triggerCombat(0.5);
                }
            }
            else if (act.action === “search_roof”) {
                log(“You climb to the roof under the sign.”, ‘story’);
                if (Math.random() > 0.8 && !state.player.inventory.includes(“Neon Tube”)) {
                    log(“Found the missing Neon Tube!”, ‘gain’);
                    addItem(“Neon Tube”);
                } else {
                    log(“Just pigeon feathers and rust.”, ‘info’);
                }
            }
            else if (act.action === “enter_pot”) {
                log(“The Coffee Pot Roadhouse. It’s smoky in here.”, ‘story’);
                if (Math.random() > 0.5) {
                    log(“The ghostly band is playing a tune. You feel energized.”, ‘gain’);
                    state.player.hp = Math.min(state.player.maxHp, state.player.hp + 15);
                    updateStats();
                } else {
                    log(“A spectral bouncer stops you!”, ‘combat’);
                    triggerCombat(0);
                }
            }
            else if (act.action === “check_spout”) {
                log(“You check the giant coffee pot spout.”, ‘story’);
                log(“Someone hid a ‘Roadhouse Brew’ here!”, ‘gain’);
                addItem(“Roadhouse Brew”);
            }
            else if (act.action === “style_hair”) {
                log(“Misfit Beauty Stylist: ‘Let’s fix that apocalypse hair.'”, ‘story’);
                if (Math.random() > 0.5) {
                    log(“You look fabulous. +10 HP”, ‘gain’);
                    state.player.hp = Math.min(state.player.maxHp, state.player.hp + 10);
                    updateStats();
                } else {
                    log(“The scissors snip too close to your ear! Ouch!”, ‘combat’);
                    triggerCombat(0.5);
                }
            }
            else if (act.action === “check_mirrors”) {
                log(“You stare into the antique mirror…”, ‘story’);
                if(Math.random() > 0.6) {
                    log(“A reflection hands you a Hairspray!”, ‘gain’);
                    addItem(“Hairspray”);
                } else {
                    log(“Your reflection blinks when you don’t. Creepy.”, ‘lore’);
                }
            }
            else if (act.action === “roll_dice”) {
                log(“You roll a giant foam D20 at the counter.”, ‘story’);
                let outcome = Math.floor(Math.random() * 20) + 1;
                if (outcome > 15) {
                    log(`Rolled ${outcome}. You gain combat experience!`, ‘gain’);
                    state.player.xp += 25;
                } else if (outcome < 5) {
                    log(`Rolled ${outcome}. The Rule Lawyer attacks!`, ‘combat’);
                    triggerCombat(0.0);
                } else {
                    log(`Rolled ${outcome}. ‘House rules say that’s a miss,’ says the clerk.`, ‘info’);
                }
            }
            else if (act.action === “inspect_yarn”) {
                log(“Rows of colorful yarn. It’s mesmerizing.”, ‘info’);
                if(Math.random() > 0.7) addItem(“Knitting Needles”);
            }
            else if (act.action === “read_book”) {
                log(“You sink into a chair with a book about local ghosts.”, ‘gain’);
                state.player.hp = Math.min(state.player.maxHp, state.player.hp + 15);
                updateStats();
                triggerCombat(0.3);
            }
            else if (act.action === “search_stacks”) {
                log(“You navigate the maze of bookshelves.”, ‘story’);
                if (Math.random() > 0.6) {
                    log(“Found a hidden stash: Hot Cocoa!”, ‘gain’);
                    addItem(“Hot Cocoa”);
                } else {
                    log(“You knock over a stack of encyclopedias. It’s loud.”, ‘combat’);
                    triggerCombat(0.2);
                }
            }
            else if (act.action === “kitchen”) {
                log(“You raid the Hotel kitchen. Found Spoon Bread!”, ‘gain’);
                addItem(“Spoon Bread”);
                triggerCombat(0.5);
            }
            else if (act.action === “explore_hotel”) {
                log(“You feel a cold spot near the elevator. Lore says a woman in red died here in 1902.”, ‘lore’);
                triggerCombat(0.3);
            }
            else if (act.action === “hike”) {
                log(“Fresh mountain air clears your mind.”, ‘gain’);
                state.player.hp = Math.min(state.player.maxHp, state.player.hp + 10);
                updateStats();
                triggerCombat(0.3);
            }
            else if (act.action === “search_train”) {
                if (!state.player.inventory.includes(“Neon Tube”)) {
                    log(“In the tender of the 611, you find the Neon Tube!”, ‘gain’);
                    addItem(“Neon Tube”);
                } else {
                    log(“Empty.”, ‘info’);
                }
            }
            else if (act.action === “lore_train”) {
                log(“Plaque: ‘N&W Class J 611. The pinnacle of steam technology. Built right here.'”, ‘lore’);
            }
            else if (act.action === “inspect_star”) {
                if (state.player.inventory.includes(“Neon Tube”)) {
                    log(“You have the Tube! The ‘Fix Star!’ button has appeared below.”, ‘gain’);
                } else {
                    log(“The Star is broken. It needs a replacement Neon Tube.”, ‘info’);
                }
            }
        }

        function addItem(item) {
            state.player.inventory.push(item);
            log(`Found: ${item}`, ‘gain’);
            // updateStats() is no longer called here, relies on inventory toggle/open.
        }

        // — Combat —
        function triggerCombat(chance = 1.0) {
            if (Math.random() > chance) return;

            state.inCombat = true;
            const loc = locations[state.currentLocation];
            const enemyName = loc.enemies[Math.floor(Math.random() * loc.enemies.length)];
           
            // Boss Check at Mountain
            if (state.currentLocation === ‘mill_mountain’ && Math.random() < 0.15) {
                 state.currentEnemy = JSON.parse(JSON.stringify(enemies[“Woman in Black”]));
                 state.currentEnemy.name = “Woman in Black”;
                 log(“THE WOMAN IN BLACK APPEARS!”, ‘combat’);
            } else {
                 state.currentEnemy = JSON.parse(JSON.stringify(enemies[enemyName]));
                 state.currentEnemy.name = enemyName;
                 log(`Enemy: ${enemyName}`, ‘combat’);
            }

            renderCombatControls();
        }

        function renderCombatControls() {
            const controls = document.getElementById(‘controls’);
            controls.innerHTML = ”;
            const actions = [
                { txt: “Attack”, fn: combatAttack, style: “border-color:var(–danger)” },
                { txt: “Defend”, fn: combatDefend },
                { txt: “Item”, fn: toggleInventory },
                { txt: “Flee”, fn: combatFlee }
            ];
            actions.forEach(a => {
                const btn = document.createElement(‘button’);
                btn.innerText = a.txt;
                btn.onclick = a.fn;
                if(a.style) btn.setAttribute(‘style’, a.style);
                controls.appendChild(btn);
            });
        }

        function combatAttack() {
            if (!state.currentEnemy) return;
            const dmg = Math.floor(state.player.dmg * (0.8 + Math.random() * 0.4));
            state.currentEnemy.hp -= dmg;
            log(`Hit for ${dmg} dmg.`, ‘combat’);
            checkCombatStatus();
            if (state.inCombat) enemyTurn();
        }

        function combatDefend() {
            log(“Defending…”, ‘info’);
            state.player.tempDefense = 5;
            enemyTurn();
            state.player.tempDefense = 0;
        }

        function combatFlee() {
            if (Math.random() > 0.5) {
                log(“Escaped!”, ‘info’);
                state.inCombat = false;
                state.currentEnemy = null;
                renderLocation();
            } else {
                log(“Trapped!”, ‘combat’);
                enemyTurn();
            }
        }

        function enemyTurn() {
            if (!state.currentEnemy) return;
            let dmg = Math.floor(state.currentEnemy.dmg * (0.8 + Math.random() * 0.4));
            dmg -= (state.player.defense + (state.player.tempDefense || 0));
            if (dmg < 0) dmg = 0;
            state.player.hp -= dmg;
            log(`${state.currentEnemy.name} attacks! -${dmg} HP`, ‘combat’);
            updateStats();
            if (state.player.hp <= 0) gameOver();
        }

        function checkCombatStatus() {
            if (state.currentEnemy.hp <= 0) {
                log(`Defeated ${state.currentEnemy.name}!`, ‘gain’);
                state.player.xp += state.currentEnemy.xp;
                if (Math.random() > 0.6 && state.currentEnemy.drop) {
                    addItem(state.currentEnemy.drop);
                }
                if (state.player.xp >= state.player.level * 100) {
                    state.player.level++;
                    state.player.maxHp += 20;
                    state.player.hp = state.player.maxHp;
                    state.player.dmg += 2;
                    log(`LEVEL UP! Now Lvl ${state.player.level}.`, ‘gain’);
                }
                state.inCombat = false;
                state.currentEnemy = null;
                setTimeout(renderLocation, 1000);
            }
        }

        function gameOver() {
            state.inCombat = false;
            document.getElementById(‘controls’).innerHTML = ”;
            log(“YOU DIED. The valley claims another soul.”, ‘combat’);
            const btn = document.createElement(‘button’);
            btn.innerText = “Try Again”;
            btn.style.gridColumn = “span 2”;
            btn.onclick = () => location.reload();
            document.getElementById(‘controls’).appendChild(btn);
        }

        function winGame() {
            state.flags.starLit = true;
            const visual = document.getElementById(‘location-visual’);
            visual.style.background = “#000”;
            visual.innerHTML = `<div style=”color:white; font-size:4rem; text-shadow: 0 0 20px red, 0 0 40px white, 0 0 60px blue;”>★</div>`;
            document.getElementById(‘log-area’).innerHTML = ”;
            log(“The Star flickers to life!”, ‘gain’);
            log(“Red. White. Blue. The shadows retreat from the valley. You have restored the Star City.”, ‘story’);
            document.getElementById(‘controls’).innerHTML = ”;
            const btn = document.createElement(‘button’);
            btn.innerText = “Play Again”;
            btn.style.gridColumn = “span 2”;
            btn.onclick = () => location.reload();
            document.getElementById(‘controls’).appendChild(btn);
        }
    </script>
</body>
</html>

Leave a Reply