Compare commits

..

5 Commits

Author SHA1 Message Date
OpenClaw Engineer
d57951466e Update difficulty selection visibility, enhance scores data, and modify password error message 2026-03-07 15:22:13 -06:00
OpenClaw Engineer
2377d4fa71 Reduce Easy-mode water gap widths significantly 2026-03-07 15:11:04 -06:00
OpenClaw Engineer
50671b483f Fix end-of-world portal flow to allow forward-only progression 2026-03-07 11:15:59 -06:00
OpenClaw Engineer
52717f67a9 Add difficulty portals, rebalance hearts, and tune easy water gaps 2026-03-07 10:39:21 -06:00
OpenClaw Engineer
e4a1849669 Add Dino Land logo to start screen modal 2026-03-06 15:56:52 -06:00
5 changed files with 150 additions and 17 deletions

View File

@ -25,7 +25,8 @@ LAN test URL (from another device on your network):
## Gameplay Rules ## Gameplay Rules
- TRex starts with **3 hearts**. - TRex starts with **5 hearts** (or **10 hearts** in VIP mode).
- Easy and Medium end with a portal that advances to the next difficulty while preserving score.
- Moving forward earns score (**+1 per forward step unit**). - Moving forward earns score (**+1 per forward step unit**).
- Water gaps cause instant death if landed in. - Water gaps cause instant death if landed in.
- Ant collision costs 1 heart; jumping on ants crushes them. - Ant collision costs 1 heart; jumping on ants crushes them.

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -16,6 +16,13 @@
<div id="startScreen"> <div id="startScreen">
<div class="panel"> <div class="panel">
<img
class="start-logo"
src="assets/images/dinoland-logo.png"
alt="Dino Land logo"
width="320"
height="180"
/>
<h1>Start Dino Land</h1> <h1>Start Dino Land</h1>
<form id="startForm"> <form id="startForm">
<label for="startPassword">Password</label> <label for="startPassword">Password</label>
@ -23,17 +30,18 @@
<label for="accessMode">Access Mode</label> <label for="accessMode">Access Mode</label>
<select id="accessMode"> <select id="accessMode">
<option value="standard" selected>Standard (3 hearts)</option> <option value="standard" selected>Standard (5 hearts)</option>
<option value="vip">VIP (6 hearts)</option> <option value="vip">VIP (10 hearts)</option>
<option value="vipPlus">VIP+ (9 hearts)</option>
</select> </select>
<label for="difficulty">Difficulty</label> <div style="display: none; !important;">
<select id="difficulty"> <label for="difficulty" style="display: none; !important;">Difficulty</label>
<option value="easy">Easy</option> <select id="difficulty" style="display: none; !important;">
<option value="medium" selected>Medium</option> <option value="easy" selected>Easy</option>
<option value="medium">Medium</option>
<option value="hard">Hard</option> <option value="hard">Hard</option>
</select> </select>
</div>
<p id="startError" class="error"></p> <p id="startError" class="error"></p>
<button type="submit">Start Game</button> <button type="submit">Start Game</button>

View File

@ -98,7 +98,7 @@ function playSfx(type) {
} catch (_) {} } catch (_) {}
} }
const waterGaps = [ const baseWaterGaps = [
{ x: 700, width: 130 }, { x: 700, width: 130 },
{ x: 1300, width: 150 }, { x: 1300, width: 150 },
{ x: 2100, width: 170 }, { x: 2100, width: 170 },
@ -143,9 +143,19 @@ function getBiomeAt(x) {
} }
function getMaxHearts(accessMode) { function getMaxHearts(accessMode) {
if (accessMode === 'vipPlus') return 9; if (accessMode === 'vip') return 10;
if (accessMode === 'vip') return 6; return 5;
return 3; }
function getWaterGaps(difficulty) {
if (difficulty !== 'easy') return baseWaterGaps;
return baseWaterGaps.map((gap) => ({ ...gap, width: Math.round(gap.width * 0.5) }));
}
function getNextDifficulty(difficulty) {
if (difficulty === 'easy') return 'medium';
if (difficulty === 'medium') return 'hard';
return null;
} }
function createInitialState(config = gameConfig) { function createInitialState(config = gameConfig) {
@ -162,6 +172,19 @@ function createInitialState(config = gameConfig) {
maxHearts, maxHearts,
hitCooldown: 0, hitCooldown: 0,
floatingTexts: [], floatingTexts: [],
worldCleared: false,
portal: {
active: false,
entered: false,
// Keep portal close to the world edge so forward motion naturally carries
// the player into it once progression is unlocked.
x: worldWidth - 60,
width: 52,
height: 92,
y: groundY - 92,
pulse: 0
},
waterGaps: getWaterGaps(config.difficulty),
ants: [ ants: [
{ x: 1000, y: groundY - 20, width: 22, height: 20, vx: -65, alive: true }, { x: 1000, y: groundY - 20, width: 22, height: 20, vx: -65, alive: true },
{ x: 1750, y: groundY - 20, width: 22, height: 20, vx: -70, alive: true }, { x: 1750, y: groundY - 20, width: 22, height: 20, vx: -70, alive: true },
@ -219,7 +242,7 @@ startFormEl.addEventListener('submit', (e) => {
e.preventDefault(); e.preventDefault();
const pass = passwordEl.value.trim(); const pass = passwordEl.value.trim();
if (pass !== '1027') { if (pass !== '1027') {
startErrorEl.textContent = 'Wrong password. Try 1027.'; startErrorEl.textContent = 'Wrong password. Ask Tanner.';
return; return;
} }
@ -326,7 +349,7 @@ function drawBackground() {
ctx.fillRect(0, groundY, canvas.width, canvas.height - groundY); ctx.fillRect(0, groundY, canvas.width, canvas.height - groundY);
// Water and lava pits // Water and lava pits
for (const gap of waterGaps) { for (const gap of state.waterGaps) {
const sx = gap.x - state.cameraX; const sx = gap.x - state.cameraX;
if (sx + gap.width < 0 || sx > canvas.width) continue; if (sx + gap.width < 0 || sx > canvas.width) continue;
ctx.fillStyle = '#2e8bcf'; ctx.fillStyle = '#2e8bcf';
@ -354,7 +377,7 @@ function drawBackground() {
ctx.font = 'bold 20px Arial'; ctx.font = 'bold 20px Arial';
ctx.fillText(`Biome: ${biome.name}`, 18, 70); ctx.fillText(`Biome: ${biome.name}`, 18, 70);
ctx.fillText(`Difficulty: ${gameConfig.difficulty.toUpperCase()}`, 18, 96); ctx.fillText(`Difficulty: ${gameConfig.difficulty.toUpperCase()}`, 18, 96);
const modeLabel = gameConfig.accessMode === 'vipPlus' ? 'VIP+ MODE' : gameConfig.accessMode === 'vip' ? 'VIP MODE' : 'STANDARD'; const modeLabel = gameConfig.accessMode === 'vip' ? 'VIP MODE' : 'STANDARD MODE';
ctx.fillText(modeLabel, 18, 122); ctx.fillText(modeLabel, 18, 122);
} }
@ -436,6 +459,97 @@ function drawMeteors() {
} }
} }
function drawPortal() {
if (!state.portal.active) return;
const portal = state.portal;
const sx = portal.x - state.cameraX;
if (sx + portal.width < 0 || sx > canvas.width) return;
const pulse = (Math.sin(portal.pulse * 6) + 1) / 2;
const glowWidth = portal.width + 20;
const glowHeight = portal.height + 16;
ctx.save();
ctx.globalAlpha = 0.2 + pulse * 0.2;
ctx.fillStyle = '#84c7ff';
ctx.fillRect(sx - 10, portal.y - 8, glowWidth, glowHeight);
ctx.restore();
ctx.fillStyle = '#2a2a56';
ctx.fillRect(sx, portal.y, portal.width, portal.height);
ctx.fillStyle = '#4eb4ff';
ctx.fillRect(sx + 4, portal.y + 4, portal.width - 8, portal.height - 8);
for (let i = 0; i < 5; i++) {
const stripeY = portal.y + 10 + i * 15;
const wobble = Math.sin(portal.pulse * 7 + i * 0.9) * 4;
ctx.fillStyle = i % 2 === 0 ? '#dbf4ff' : '#8bd9ff';
ctx.fillRect(sx + 10 + wobble, stripeY, portal.width - 20, 6);
}
ctx.fillStyle = '#88e1ff';
ctx.fillRect(sx - 4, portal.y + 2, 4, portal.height - 4);
ctx.fillRect(sx + portal.width, portal.y + 2, 4, portal.height - 4);
ctx.fillStyle = '#0f0f0f';
ctx.font = 'bold 14px Arial';
ctx.fillText('PORTAL', sx - 8, portal.y - 10);
}
function resetWorldForDifficulty(nextDifficulty) {
const score = state.score;
const hearts = state.hearts;
const maxHearts = state.maxHearts;
gameConfig.difficulty = nextDifficulty;
const nextState = createInitialState(gameConfig);
state = nextState;
state.score = score;
state.hearts = Math.min(hearts, maxHearts);
state.maxHearts = maxHearts;
state.running = true;
state.dead = false;
}
function updatePortal(dt) {
state.portal.pulse += dt;
const nextDifficulty = getNextDifficulty(gameConfig.difficulty);
if (!nextDifficulty) {
state.portal.active = false;
return;
}
const p = state.player;
const worldEndX = worldWidth - p.width - 8;
// Activate slightly before the hard clamp at world end so the player can
// continue moving right and enter the portal without reversing direction.
const portalActivationX = Math.max(0, state.portal.x - p.width - 24);
const reachedProgressionPoint = p.x >= portalActivationX;
if (reachedProgressionPoint) {
state.portal.active = true;
state.worldCleared = p.x >= worldEndX;
}
if (!state.portal.active || state.portal.entered) return;
const portalHitbox = {
x: state.portal.x,
y: state.portal.y,
width: state.portal.width,
height: state.portal.height
};
if (intersects(p, portalHitbox)) {
state.portal.entered = true;
addFloatingText(state.portal.x + state.portal.width / 2, state.portal.y - 14, `${nextDifficulty.toUpperCase()}!`, '#9fe6ff');
resetWorldForDifficulty(nextDifficulty);
}
}
function addFloatingText(x, y, text, color = '#ffe066') { function addFloatingText(x, y, text, color = '#ffe066') {
state.floatingTexts.push({ x, y, text, color, ttl: 0.8, vy: -36 }); state.floatingTexts.push({ x, y, text, color, ttl: 0.8, vy: -36 });
} }
@ -506,7 +620,7 @@ function applyHit(reason) {
function isOverWater(player) { function isOverWater(player) {
const centerX = player.x + player.width / 2; const centerX = player.x + player.width / 2;
return waterGaps.some((g) => centerX >= g.x && centerX <= g.x + g.width); return state.waterGaps.some((g) => centerX >= g.x && centerX <= g.x + g.width);
} }
function isOverLava(player) { function isOverLava(player) {
@ -701,6 +815,7 @@ function tick(ts) {
if (state.running) { if (state.running) {
state.hitCooldown = Math.max(0, state.hitCooldown - dt); state.hitCooldown = Math.max(0, state.hitCooldown - dt);
updatePlayer(dt); updatePlayer(dt);
updatePortal(dt);
updateAnts(dt); updateAnts(dt);
updateCactus(); updateCactus();
updateMonkeys(dt); updateMonkeys(dt);
@ -719,6 +834,7 @@ function tick(ts) {
drawMammoths(); drawMammoths();
drawPterodactyls(); drawPterodactyls();
drawMeteors(); drawMeteors();
drawPortal();
drawFloatingTexts(); drawFloatingTexts();
drawTRex(state.player); drawTRex(state.player);

View File

@ -40,6 +40,14 @@ body {
padding: 20px; padding: 20px;
width: min(92vw, 460px); width: min(92vw, 460px);
} }
.start-logo {
display: block;
width: min(100%, 320px);
height: auto;
margin: 0 auto 8px;
}
.hidden { display: none !important; } .hidden { display: none !important; }
button, input, select { button, input, select {
font-size: 16px; font-size: 16px;