Compare commits

..

No commits in common. "d57951466ee431b21fa096dc6185739d3e335d4c" and "85e2bbb48bfcc5490ed89a3e0bbab03f002dd5b0" have entirely different histories.

5 changed files with 17 additions and 150 deletions

View File

@ -25,8 +25,7 @@ LAN test URL (from another device on your network):
## Gameplay Rules ## Gameplay Rules
- TRex starts with **5 hearts** (or **10 hearts** in VIP mode). - TRex starts with **3 hearts**.
- 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.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@ -16,13 +16,6 @@
<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>
@ -30,19 +23,18 @@
<label for="accessMode">Access Mode</label> <label for="accessMode">Access Mode</label>
<select id="accessMode"> <select id="accessMode">
<option value="standard" selected>Standard (5 hearts)</option> <option value="standard" selected>Standard (3 hearts)</option>
<option value="vip">VIP (10 hearts)</option> <option value="vip">VIP (6 hearts)</option>
<option value="vipPlus">VIP+ (9 hearts)</option>
</select> </select>
<div style="display: none; !important;"> <label for="difficulty">Difficulty</label>
<label for="difficulty" style="display: none; !important;">Difficulty</label> <select id="difficulty">
<select id="difficulty" style="display: none; !important;"> <option value="easy">Easy</option>
<option value="easy" selected>Easy</option> <option value="medium" selected>Medium</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>
</form> </form>

View File

@ -98,7 +98,7 @@ function playSfx(type) {
} catch (_) {} } catch (_) {}
} }
const baseWaterGaps = [ const waterGaps = [
{ 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,19 +143,9 @@ function getBiomeAt(x) {
} }
function getMaxHearts(accessMode) { function getMaxHearts(accessMode) {
if (accessMode === 'vip') return 10; if (accessMode === 'vipPlus') return 9;
return 5; if (accessMode === 'vip') return 6;
} 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) {
@ -172,19 +162,6 @@ 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 },
@ -242,7 +219,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. Ask Tanner.'; startErrorEl.textContent = 'Wrong password. Try 1027.';
return; return;
} }
@ -349,7 +326,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 state.waterGaps) { for (const gap of 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';
@ -377,7 +354,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 === 'vip' ? 'VIP MODE' : 'STANDARD MODE'; const modeLabel = gameConfig.accessMode === 'vipPlus' ? 'VIP+ MODE' : gameConfig.accessMode === 'vip' ? 'VIP MODE' : 'STANDARD';
ctx.fillText(modeLabel, 18, 122); ctx.fillText(modeLabel, 18, 122);
} }
@ -459,97 +436,6 @@ 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 });
} }
@ -620,7 +506,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 state.waterGaps.some((g) => centerX >= g.x && centerX <= g.x + g.width); return waterGaps.some((g) => centerX >= g.x && centerX <= g.x + g.width);
} }
function isOverLava(player) { function isOverLava(player) {
@ -815,7 +701,6 @@ 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);
@ -834,7 +719,6 @@ function tick(ts) {
drawMammoths(); drawMammoths();
drawPterodactyls(); drawPterodactyls();
drawMeteors(); drawMeteors();
drawPortal();
drawFloatingTexts(); drawFloatingTexts();
drawTRex(state.player); drawTRex(state.player);

View File

@ -40,14 +40,6 @@ 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;