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>
