Compare commits
No commits in common. "d57951466ee431b21fa096dc6185739d3e335d4c" and "85e2bbb48bfcc5490ed89a3e0bbab03f002dd5b0" have entirely different histories.
d57951466e
...
85e2bbb48b
@ -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 |
22
index.php
22
index.php
@ -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,18 +23,17 @@
|
|||||||
|
|
||||||
<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>
|
||||||
|
|||||||
132
js/game.js
132
js/game.js
@ -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);
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user