<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>The Newspaper Boy – A Life Lived in Pages</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Courier New', monospace; overflow: hidden; background: #000000; }
#gameContainer { width: 100vw; height: 100vh; display: flex; flex-direction: column; }
#canvas { flex: 1; display: block; background: #000000; }
.hud { position: fixed; top: 0; left: 0; width: 100%; padding: 20px 40px; display: flex; justify-content: space-between; align-items: flex-start; z-index: 100; pointer-events: none; }
.stat-box { background: rgba(20, 20, 20, 0.9); border: 2px solid #00ff00; padding: 15px 20px; color: #00ff00; font-size: 13px; line-height: 1.8; letter-spacing: 2px; text-transform: uppercase; backdrop-filter: blur(5px); font-family: 'Courier New', monospace; }
.time-box { text-align: left; }
.right-hud { text-align: right; }
.story-box { position: fixed; bottom: 0; left: 0; width: 100%; background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.95) 40%); padding: 60px 60px 280px 60px; color: #00ff00; font-size: 15px; line-height: 1.8; letter-spacing: 0.5px; max-height: 45%; overflow-y: auto; z-index: 99; pointer-events: none; font-family: 'Courier New', monospace; }
.choice-buttons { position: fixed; bottom: 20px; left: 60px; right: 60px; display: flex; gap: 15px; justify-content: center; flex-wrap: wrap; z-index: 100; pointer-events: auto; }
button { background: rgba(0, 0, 0, 0.8); border: 2px solid #00ff00; color: #00ff00; padding: 10px 20px; font-size: 12px; cursor: pointer; text-transform: uppercase; letter-spacing: 1px; transition: all 0.2s ease; font-family: 'Courier New', monospace; backdrop-filter: blur(5px); min-width: 140px; }
button:hover { background: rgba(0, 255, 0, 0.1); border-color: #00ff00; color: #ffffff; box-shadow: 0 0 10px rgba(0, 255, 0, 0.5); }
button:disabled { opacity: 0.4; cursor: not-allowed; }
.story-intro-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.99); display: flex; flex-direction: column; justify-content: center; align-items: center; z-index: 1001; backdrop-filter: blur(3px); }
.story-intro-content { text-align: center; color: #00ff00; max-width: 700px; padding: 60px; position: relative; z-index: 10; }
.story-intro-text { font-size: 18px; line-height: 2; letter-spacing: 0.5px; margin-bottom: 40px; font-style: italic; font-family: 'Courier New', monospace; color: #00ff00; }
.intro-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.99); display: none; flex-direction: column; justify-content: center; align-items: center; z-index: 1000; backdrop-filter: blur(3px); }
.intro-content { text-align: center; color: #00ff00; max-width: 600px; position: relative; z-index: 10; }
.intro-title { font-size: 28px; margin-bottom: 15px; letter-spacing: 2px; color: #00ff00; font-weight: normal; font-family: 'Courier New', monospace; }
.intro-input-group { display: flex; gap: 10px; justify-content: center; margin-bottom: 20px; }
#nameInput { background: rgba(0, 0, 0, 0.9); border: 2px solid #00ff00; color: #00ff00; padding: 12px 20px; font-size: 16px; font-family: 'Courier New', monospace; min-width: 300px; outline: none; transition: all 0.3s ease; backdrop-filter: blur(5px); }
#nameInput:focus { border-color: #00ff00; background: rgba(0, 0, 0, 0.95); box-shadow: 0 0 10px rgba(0, 255, 0, 0.4); }
.intro-button { background: rgba(0, 0, 0, 0.8); border: 2px solid #00ff00; color: #00ff00; padding: 12px 30px; font-size: 14px; cursor: pointer; text-transform: uppercase; letter-spacing: 1px; transition: all 0.3s ease; font-family: 'Courier New', monospace; backdrop-filter: blur(5px); }
.intro-button:disabled { opacity: 0.4; cursor: not-allowed; }
.qr-section { margin-bottom: 30px; padding: 20px; background: rgba(0, 0, 0, 0.5); border: 2px solid #00ff00; display: inline-block; text-align: center; }
.qr-label { color: #00ff00; font-size: 12px; letter-spacing: 1px; text-transform: uppercase; margin-bottom: 15px; font-family: 'Courier New', monospace; display: block; }
.fade-card-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.8); display: none; flex-direction: column; justify-content: center; align-items: center; z-index: 500; backdrop-filter: blur(2px); }
.fade-card { background: rgba(0, 0, 0, 0.95); border: 2px solid #00ff00; padding: 60px 80px; max-width: 500px; text-align: center; color: #00ff00; backdrop-filter: blur(10px); animation: cardFadeIn 0.8s ease-out; font-family: 'Courier New', monospace; }
@keyframes cardFadeIn { from { opacity: 0; transform: scale(0.9) translateY(-20px); } to { opacity: 1; transform: scale(1) translateY(0); } }
.fade-card-text { font-size: 20px; line-height: 1.8; letter-spacing: 0.5px; margin-bottom: 60px; font-style: italic; font-family: 'Courier New', monospace; }
.fade-card-buttons { display: flex; flex-direction: column; gap: 15px; margin-top: 20px; }
.fade-card-button { background: rgba(0, 0, 0, 0.8); border: 2px solid #00ff00; color: #00ff00; padding: 12px 40px; font-size: 14px; cursor: pointer; text-transform: uppercase; letter-spacing: 1px; transition: all 0.3s ease; font-family: 'Courier New', monospace; backdrop-filter: blur(5px); }
.fade-card-button:hover { background: rgba(0, 255, 0, 0.1); border-color: #00ff00; color: #ffffff; box-shadow: 0 0 10px rgba(0, 255, 0, 0.5); }
.scent-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.85); display: none; flex-direction: column; justify-content: center; align-items: center; z-index: 600; backdrop-filter: blur(3px); }
.scent-card { background: rgba(0, 0, 0, 0.95); border: 2px solid #00ff00; padding: 80px 100px; max-width: 600px; text-align: center; color: #00ff00; backdrop-filter: blur(10px); animation: cardFadeIn 0.8s ease-out; }
.scent-name { font-size: 32px; margin-bottom: 50px; letter-spacing: 1px; font-family: 'Courier New', monospace; color: #00ff00; }
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: rgba(40, 35, 30, 0.3); }
::-webkit-scrollbar-thumb { background: rgba(180, 140, 90, 0.4); border-radius: 3px; }
</style>
</head>
<body>
<div id="gameContainer">
<canvas id="canvas"></canvas>
<div id="storyIntroOverlay" class="story-intro-overlay">
<div class="story-intro-content">
<div class="qr-section">
<div class="qr-label">Scan to play on your device</div>
<div style="background: white; padding: 10px; width: 200px; height: 200px; margin: auto;">QR Code</div>
</div>
<p class="story-intro-text">Once upon a time, there was a boy who sold newspapers on the streets every morning, even in pouring rain and blistering storms, merely to make ends meet...</p>
<button id="proceedButton" style="margin-top: 20px;">Click to proceed</button>
</div>
</div>
<div id="introOverlay" class="intro-overlay">
<div class="intro-content">
<div class="qr-section">
<div class="qr-label">Scan to play on your device</div>
<div style="background: white; padding: 10px; width: 200px; height: 200px; margin: auto;">QR Code</div>
</div>
<div class="intro-title">The Newspaper Boy</div>
<div class="intro-input-group">
<input type="text" id="nameInput" placeholder="Enter your name..." maxlength="30">
<button class="intro-button" id="startButton" disabled>Begin</button>
</div>
</div>
</div>
<div class="hud">
<div class="stat-box time-box">
<div id="dayDisplay">Day 1</div>
<div id="timeDisplay">MORNING</div>
<div id="weatherDisplay">Clear Skies</div>
</div>
<div class="stat-box right-hud">
<div>Papers: <span id="paperCount">0</span> / 50</div>
<div>Sold: <span id="soldCount">0</span></div>
</div>
</div>
<div class="story-box" id="storyBox"></div>
<div class="choice-buttons" id="choiceButtons"></div>
<div class="fade-card-overlay" id="fadeCardOverlay">
<div class="fade-card">
<div class="fade-card-text" id="fadeCardMessage"></div>
<div class="fade-card-buttons">
<button class="fade-card-button" id="scent-button">Check your scent</button>
<button class="fade-card-button" id="fadeCardButton">Begin Tomorrow</button>
</div>
</div>
</div>
<div class="scent-overlay" id="scentOverlay">
<div class="scent-card">
<div class="scent-name" id="scentName">Aesop Hwyl Eau de Parfum</div>
<button class="fade-card-button" id="closeScent">Back</button>
</div>
</div>
</div>
<script>
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const game = {
day: 1,
time: 'morning',
papers: 50,
sold: 0,
coins: 0,
weather: 'clear',
phase: 'intro',
currentStep: 0,
particles: [],
embers: [],
memories: 0,
gameTime: 0,
totalMoneyEarned: 0,
playerName: ''
};
function getWeather() {
const rand = Math.random();
if (rand < 0.4) return 'clear';
if (rand < 0.65) return 'rain';
return 'storm';
}
class Particle {
constructor(x, y, type = 'smoke') {
this.x = x; this.y = y; this.startX = x; this.startY = y; this.type = type;
this.vx = (Math.random() - 0.5) * 1.5; this.vy = -Math.random() * 1.8 - 0.3;
this.life = 1; this.maxLife = Math.random() * 120 + 80; this.size = Math.random() * 12 + 6;
this.hue = Math.random() * 40 + 20; this.wobble = Math.random() * Math.PI * 2;
this.flowTime = 0; this.rotation = Math.random() * Math.PI * 2;
this.rotationSpeed = (Math.random() - 0.5) * 0.15;
}
update() {
this.flowTime += 0.02;
const flow1 = Math.sin(this.flowTime) * 0.8;
const flow2 = Math.cos(this.flowTime * 0.7) * 0.6;
const flow3 = Math.sin(this.flowTime * 1.3 + this.wobble) * 0.5;
this.x += this.vx + flow1 + flow2 * 0.5; this.y += this.vy + flow3 * 0.3;
this.vy *= 0.96; this.vx *= 0.95; this.wobble += 0.08;
this.life--; this.size *= 0.985; this.rotation += this.rotationSpeed;
}
draw(ctx) {
const alpha = (this.life / this.maxLife) * 0.6;
ctx.save(); ctx.translate(this.x, this.y); ctx.rotate(this.rotation);
ctx.fillStyle = `rgba(200, 120, 60, ${alpha * 0.6})`; ctx.beginPath();
for (let i = 0; i < 12; i++) {
const angle = (i / 12) * Math.PI * 2;
const wobble = Math.sin(this.flowTime + i * 0.5) * 0.5;
const radius = this.size * (0.8 + wobble);
const px = Math.cos(angle) * radius; const py = Math.sin(angle) * radius;
if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
}
ctx.closePath(); ctx.fill();
ctx.fillStyle = `rgba(220, 140, 80, ${alpha * 0.4})`; ctx.beginPath();
ctx.arc(0, 0, this.size * 0.6, 0, Math.PI * 2); ctx.fill(); ctx.restore();
}
isDead() { return this.life <= 0; }
}
class Ember {
constructor(x, y) {
this.x = x; this.y = y; this.startX = x; this.startY = y;
this.vx = (Math.random() - 0.5) * 2.5; this.vy = -Math.random() * 2.5 - 0.8;
this.life = Math.random() * 80 + 50; this.maxLife = this.life;
this.size = Math.random() * 7 + 3; this.flowTime = 0;
this.rotation = Math.random() * Math.PI * 2; this.rotationSpeed = (Math.random() - 0.5) * 0.2;
this.waveOffset = Math.random() * Math.PI * 2;
}
update() {
this.flowTime += 0.04;
const waveX = Math.sin(this.flowTime + this.waveOffset) * 1.2;
const waveY = Math.cos(this.flowTime * 0.8 + this.waveOffset) * 0.8;
this.x += this.vx + waveX; this.y += this.vy + waveY;
this.vy += 0.08; this.vx *= 0.94; this.life--; this.size *= 0.94;
this.rotation += this.rotationSpeed;
}
draw(ctx) {
const alpha = (this.life / this.maxLife) * 0.8;
ctx.save(); ctx.translate(this.x, this.y); ctx.rotate(this.rotation);
const pixelSize = Math.max(2, this.size * 0.7);
const gridSize = Math.ceil(this.size / pixelSize);
for (let py = -gridSize; py <= gridSize; py++) {
for (let px = -gridSize; px <= gridSize; px++) {
const dist = Math.sqrt(px * px + py * py);
if (dist <= gridSize * 0.85) {
const distAlpha = (1 - dist / (gridSize * 0.85)) * 0.9;
const flicker = Math.sin(this.flowTime * 1.5 + px + py) * 0.5 + 0.5;
const cyanAmount = 255 * flicker;
ctx.fillStyle = `rgba(100, ${cyanAmount}, 255, ${alpha * distAlpha})`;
ctx.fillRect(px * pixelSize - pixelSize / 2, py * pixelSize - pixelSize / 2, pixelSize, pixelSize);
}
}
}
ctx.restore();
}
isDead() { return this.life <= 0; }
}
function drawIntroBackground() {
ctx.fillStyle = '#000000'; ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.globalAlpha = 0.08;
for (let i = 0; i < 40; i++) {
const seed = i * 3.7;
const x = (canvas.width / 40 * i + Math.sin(game.gameTime * 0.002 + seed) * canvas.width * 0.3);
const y = canvas.height * 0.4 + Math.sin(game.gameTime * 0.0015 + seed * 2) * 120;
const size = Math.sin(game.gameTime * 0.003 + seed * 3) * 3 + 2;
ctx.fillStyle = '#00ff00'; ctx.beginPath();
for (let j = 0; j < 6; j++) {
const angle = (j / 6) * Math.PI * 2;
const wobble = Math.sin(game.gameTime * 0.004 + i + j) * 0.5;
const radius = size * (0.8 + wobble);
const px = x + Math.cos(angle) * radius;
const py = y + Math.sin(angle) * radius;
if (j === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
}
ctx.closePath(); ctx.fill();
}
ctx.globalAlpha = 1.0;
}
function drawMorningStreets() {
ctx.fillStyle = '#000000'; ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.globalAlpha = 0.03; ctx.strokeStyle = '#00ff00'; ctx.lineWidth = 1;
for (let x = 0; x < canvas.width; x += 50) {
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, canvas.height); ctx.stroke();
}
for (let y = 0; y < canvas.height; y += 50) {
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(canvas.width, y); ctx.stroke();
}
ctx.globalAlpha = 1.0;
if (game.weather === 'rain' || game.weather === 'storm') {
const rainCount = game.weather === 'storm' ? 80 : 40;
const rainAlpha = game.weather === 'storm' ? 0.25 : 0.12;
ctx.strokeStyle = `rgba(0, 255, 0, ${rainAlpha})`; ctx.lineWidth = 1.2;
for (let i = 0; i < rainCount; i++) {
const seed = i * 7.23;
const x = (Math.sin(game.gameTime * 0.01 + seed) * canvas.width * 0.6 + canvas.width * 0.2);
const y = ((game.gameTime * 1.8 + seed * 15) % (canvas.height * 1.2)) - canvas.height * 0.2;
ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x, y + 15); ctx.stroke();
}
}
}
function drawFireplace() {
ctx.fillStyle = '#000000'; ctx.fillRect(0, 0, canvas.width, canvas.height);
const fireX = canvas.width / 2; const fireY = canvas.height * 0.65;
const fireGradient = ctx.createRadialGradient(fireX, fireY, 50, fireX, fireY, 400);
fireGradient.addColorStop(0, 'rgba(255, 150, 30, 0.5)');
fireGradient.addColorStop(0.3, 'rgba(255, 100, 0, 0.2)');
fireGradient.addColorStop(0.6, 'rgba(200, 50, 0, 0.05)');
fireGradient.addColorStop(1, 'rgba(100, 20, 0, 0)');
ctx.fillStyle = fireGradient; ctx.fillRect(0, 0, canvas.width, canvas.height);
const pixelSize = 20; const fireWidth = 250; const fireHeight = 350;
for (let py = 0; py < fireHeight / pixelSize; py++) {
for (let px = 0; px < fireWidth / pixelSize; px++) {
const centerX = fireWidth / 2 / pixelSize;
const distFromCenter = Math.abs(px - centerX);
const noise1 = Math.sin((px * 0.3 + game.gameTime * 0.05) * Math.PI) * 0.5 + 0.5;
const noise2 = Math.sin((px * 0.15 + py * 0.2 + game.gameTime * 0.08) * Math.PI) * 0.5 + 0.5;
const noise3 = Math.sin((py * 0.25 - game.gameTime * 0.06) * Math.PI) * 0.5 + 0.5;
const noiseValue = (noise1 * 0.4 + noise2 * 0.35 + noise3 * 0.25);
const heightFactor = 1 - (py / (fireHeight / pixelSize));
const widthFactor = 1 - (distFromCenter / (fireWidth / pixelSize * 0.5));
const intensity = noiseValue * heightFactor * Math.max(0, widthFactor) * 1.5;
if (intensity > 0.3) {
let r, g, b, alpha;
if (intensity > 0.8) { r = 255; g = 220; b = 100; alpha = Math.min(1, intensity) * 0.8; }
else if (intensity > 0.6) { r = 255; g = 140; b = 30; alpha = intensity * 0.7; }
else if (intensity > 0.4) { r = 200; g = 80; b = 20; alpha = intensity * 0.6; }
else { r = 100; g = 30; b = 10; alpha = intensity * 0.3; }
const x = fireX - fireWidth / 2 + px * pixelSize;
const y = fireY - fireHeight + py * pixelSize;
ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${alpha})`; ctx.fillRect(x, y, pixelSize, pixelSize);
}
}
}
}
const storyPhases = {
selling: [
{ text: "You wake before dawn, your small bundle of newspapers heavy in your hands. 50 copies, warm from the press. The morning air is cool, carrying the smell of ink and binding glue—a scent you've known since childhood.",
choices: [{ text: "Head to the busy intersection", action: "dayProgress" }] },
{ text: (weather) => {
if (weather === 'storm') return "The storm rages through the streets. Most people hurry indoors, barely glancing at your papers. Sales are scarce.";
if (weather === 'rain') return "The rain patters against the papers in your hands. Commuters rush past but some stop. Sales are modest.";
return "The streets are alive. Clear skies bring foot traffic. Papers sell quickly.";
},
choices: [{ text: "Keep selling", action: "morningToAfternoon" }] },
{ text: (weather) => {
if (weather === 'storm') return "The afternoon brings no relief. You've sold what you could.";
if (weather === 'rain') return "The afternoon wears on. You've managed decent sales despite the rain.";
return "The afternoon wears on. The day has been good.";
},
choices: [{ text: "Return home", action: "goHome" }] }
],
home: [
{ text: (papers) => {
if (papers > 0) return `You return home with ${papers} unsold papers. Winter is coming. They could burn well.`;
return "You return home, pockets lighter with coins.";
},
choices: (papers) => papers > 0 ? [{ text: "Light the fire", action: "goToFireplace" }] : [{ text: "Rest and begin again", action: "nextDay" }] }
],
fireplace: [
{ text: "You strike the match. The flame catches. For a moment, it's just fire. But then something shifts.",
choices: [{ text: "Continue", action: "nextStep" }] },
{ text: "The room fills with a scent unlike any other. You smell rainy mornings. The metallic trace of ink.",
choices: [{ text: "Breathe it in", action: "nextStep" }] },
{ text: "Each crackle feels like turning a page. Each wisp of smoke carries away something you'll never read.",
choices: [{ text: "Feel the presence", action: "nextStep" }] },
{ text: "The scent curls around you like an old friend. Warm, musty, sweet and bitter. The papers are gone.",
choices: [{ text: "Let the fire fade", action: "showFade" }] }
]
};
const fadeMessages = [
"Everything burns. Everything transforms. A faint trace of smoke and cedar.",
"What remains is not ash, but memory. The scent of old paper lingers.",
"The work is done. Tomorrow begins again. Warmth of amber and vanilla.",
"Fire consumes. Fire purifies. Acrid sweetness of burning ink.",
"In the ashes, something lingers. A whisper of rose and ash.",
"All things pass. All things matter. The musk of smoke and time."
];
const fragrances = ["Aesop Hwyl Eau de Parfum", "Hermès Terre d'Hermès", "Diptyque Orphéon", "Tom Ford Ébène Fumé"];
function getStoryText() {
const phase = storyPhases[game.phase];
if (!phase || game.currentStep >= phase.length) return '';
let text = phase[game.currentStep].text;
if (typeof text === 'function') text = text(game.weather || game.papers);
if (game.phase === 'selling' && game.currentStep === 0) {
text = `${game.playerName}, every morning you get up with newspapers. 50 copies, warm from the press. The morning air is cool, carrying the smell of ink.`;
}
return text;
}
function getChoices() {
const phase = storyPhases[game.phase];
if (!phase || game.currentStep >= phase.length) return [];
let choices = phase[game.currentStep].choices;
if (typeof choices === 'function') choices = choices(game.papers);
return choices;
}
function updateStory() {
document.getElementById('storyBox').innerHTML = `<p>${getStoryText()}</p>`;
const choiceButtons = document.getElementById('choiceButtons');
choiceButtons.innerHTML = '';
getChoices().forEach(choice => {
const btn = document.createElement('button');
btn.textContent = choice.text;
btn.onclick = () => handleAction(choice.action);
choiceButtons.appendChild(btn);
});
document.getElementById('dayDisplay').textContent = `Day ${game.day}`;
document.getElementById('timeDisplay').textContent = game.time