add byte beasties escape webtoy
BIN
src/assets/_root/webtoys/bbe/beasties/blinker_shadow_red.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
src/assets/_root/webtoys/bbe/beasties/bobbler_shadow_yellow.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
src/assets/_root/webtoys/bbe/beasties/chomper_shadow_blue.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
src/assets/_root/webtoys/bbe/beasties/creeper_shadow_red.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
src/assets/_root/webtoys/bbe/beasties/diver_shadow_blue.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
src/assets/_root/webtoys/bbe/beasties/gawker_shadow_yellow.png
Normal file
After Width: | Height: | Size: 8.4 KiB |
BIN
src/assets/_root/webtoys/bbe/beasties/grabber_shadow_red.png
Normal file
After Width: | Height: | Size: 7.8 KiB |
BIN
src/assets/_root/webtoys/bbe/beasties/puffer_shadow_green.png
Normal file
After Width: | Height: | Size: 7.9 KiB |
BIN
src/assets/_root/webtoys/bbe/beasties/reacher_shadow_yellow.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
src/assets/_root/webtoys/bbe/beasties/scanner_shadow_red.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
src/assets/_root/webtoys/bbe/beasties/seer_shadow_green.png
Normal file
After Width: | Height: | Size: 4.3 KiB |
BIN
src/assets/_root/webtoys/bbe/beasties/shuffler_shadow_yellow.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
src/assets/_root/webtoys/bbe/beasties/snapper_shadow_blue.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
src/assets/_root/webtoys/bbe/beasties/spooker_shadow_green.png
Normal file
After Width: | Height: | Size: 7.4 KiB |
BIN
src/assets/_root/webtoys/bbe/beasties/thinker_shadow_green.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
src/assets/_root/webtoys/bbe/beasties/walker_shadow_blue.png
Normal file
After Width: | Height: | Size: 11 KiB |
74
src/assets/_root/webtoys/bbe/index.html
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<head>
|
||||||
|
<title>Byte Beasties: Escape!</title>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="content" id="content">
|
||||||
|
<h1>Byte Beasties: Escape!</h1>
|
||||||
|
<noscript>
|
||||||
|
Sorry, this game requires JS to function.
|
||||||
|
</noscript>
|
||||||
|
|
||||||
|
<div class="game" id="game">
|
||||||
|
<div class="settings" id="settings">
|
||||||
|
<div>
|
||||||
|
<h2>Story</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
The Byte Beasties are on the run from the GTP (General Thinking Program),
|
||||||
|
an evil AI that wants to absorb them into its software.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Help the Byte Beasties escape GTP's yellow absorption wave by
|
||||||
|
hitting the green logic gates and avoiding the red NULL gates.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<form id="settingsForm" name="settings"" method="dialog"">
|
||||||
|
<h2>Settings</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<label>Mode:
|
||||||
|
<select name="mode" id="settingsMode">
|
||||||
|
<option value="easy">Easy</option>
|
||||||
|
<option value="medium">Medium</option>
|
||||||
|
<option value="hard">Hard</option>
|
||||||
|
<option value="endless">Endless</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="beastiePickerWrapper">
|
||||||
|
Beastie:
|
||||||
|
<div class="settingsBeastie" id="settingsBeastie"></div>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="settingsFooter">
|
||||||
|
<button id="cancelSettings">Cancel</button>
|
||||||
|
<button id="applySettings">Apply & Reset</button>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>Controls</h2>
|
||||||
|
<ul class="controls">
|
||||||
|
<li><strong>← OR →</strong>: Move the beastie</dd>
|
||||||
|
<li><strong>P</strong>: Pause</li>
|
||||||
|
<li><strong>R</strong> Restart game</li>
|
||||||
|
<li><strong>S</strong>: Show Settings</dd>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!-- We load this library via "module" script-tag to guarantee ES6 minimum functionality -->
|
||||||
|
|
||||||
|
<script type="module" src="scripts/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
656
src/assets/_root/webtoys/bbe/scripts/bbe.js
Normal file
@ -0,0 +1,656 @@
|
|||||||
|
import { createLightning, getMousePos } from "./lightning.js";
|
||||||
|
|
||||||
|
const startGame = (canvas) => {
|
||||||
|
const ctx = canvas.getContext("2d"),
|
||||||
|
BEASTIE_IMAGES = [
|
||||||
|
"blinker_shadow_red.png",
|
||||||
|
"creeper_shadow_red.png",
|
||||||
|
"grabber_shadow_red.png",
|
||||||
|
"scanner_shadow_red.png",
|
||||||
|
"bobbler_shadow_yellow.png",
|
||||||
|
"gawker_shadow_yellow.png",
|
||||||
|
"reacher_shadow_yellow.png",
|
||||||
|
"shuffler_shadow_yellow.png",
|
||||||
|
"puffer_shadow_green.png",
|
||||||
|
"seer_shadow_green.png",
|
||||||
|
"spooker_shadow_green.png",
|
||||||
|
"thinker_shadow_green.png",
|
||||||
|
"chomper_shadow_blue.png",
|
||||||
|
"diver_shadow_blue.png",
|
||||||
|
"snapper_shadow_blue.png",
|
||||||
|
"walker_shadow_blue.png",
|
||||||
|
],
|
||||||
|
GATE = {
|
||||||
|
HEIGHT: 20,
|
||||||
|
MARGIN: 25,
|
||||||
|
MAX_SPACE: 140,
|
||||||
|
MIN_SPACE: 60,
|
||||||
|
START: canvas.height - 200,
|
||||||
|
WIDTH: 65,
|
||||||
|
},
|
||||||
|
game = {
|
||||||
|
interval: 1000 / 90, // ms/f = 1000 ms/s / 90 f/s
|
||||||
|
winScore: 15000,
|
||||||
|
winScores: {
|
||||||
|
easy: 15000,
|
||||||
|
medium: 30000,
|
||||||
|
hard: 50000,
|
||||||
|
endless: 0,
|
||||||
|
},
|
||||||
|
blockerFrequency: 0.3,
|
||||||
|
blockerFrequencies: {
|
||||||
|
easy: 0.3,
|
||||||
|
medium: 0.4,
|
||||||
|
hard: 0.5,
|
||||||
|
endless: 0.3,
|
||||||
|
},
|
||||||
|
hasBlockers: true,
|
||||||
|
isOver: false,
|
||||||
|
isPaused: false,
|
||||||
|
showRestartModal: false,
|
||||||
|
maxLead: 400,
|
||||||
|
maxSpeed: -8,
|
||||||
|
|
||||||
|
speed: -1,
|
||||||
|
speedBoost: {
|
||||||
|
duration: 1,
|
||||||
|
multiplier: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
drag = 0.3,
|
||||||
|
beastie = {
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
x: canvas.width / 2 - 20,
|
||||||
|
y: GATE.START + 100,
|
||||||
|
|
||||||
|
// velocity
|
||||||
|
dx: 0,
|
||||||
|
dy: 0,
|
||||||
|
|
||||||
|
// for tracking speed boosts
|
||||||
|
boostEnd: 0,
|
||||||
|
|
||||||
|
score: 0,
|
||||||
|
image: null,
|
||||||
|
},
|
||||||
|
player = {
|
||||||
|
dx: 0,
|
||||||
|
dy: 0,
|
||||||
|
},
|
||||||
|
enemy = {
|
||||||
|
width: canvas.width,
|
||||||
|
height: canvas.height,
|
||||||
|
x: 0,
|
||||||
|
y: canvas.height - 50,
|
||||||
|
delaySeconds: 5,
|
||||||
|
speed: -2,
|
||||||
|
},
|
||||||
|
random = (min, max) => Math.random() * (max - min) + min,
|
||||||
|
images = BEASTIE_IMAGES.reduce((acc, name) => {
|
||||||
|
const tempImg = new Image();
|
||||||
|
tempImg.src = `/webtoys/bbe/beasties/${name}`;
|
||||||
|
acc[name] = tempImg;
|
||||||
|
return acc;
|
||||||
|
}, {}),
|
||||||
|
img = new Image(),
|
||||||
|
hasCollision = (gate) =>
|
||||||
|
// Collision check: AAB (Axis-Aligned Bounding Box)
|
||||||
|
beastie.x < gate.x + GATE.WIDTH &&
|
||||||
|
beastie.x > gate.x - beastie.width &&
|
||||||
|
beastie.y < gate.y + GATE.HEIGHT &&
|
||||||
|
beastie.y > gate.y - beastie.height,
|
||||||
|
settingsBeastie = document.getElementById("settingsBeastie");
|
||||||
|
|
||||||
|
let // vertical space between gates
|
||||||
|
minPlatformSpace = GATE.MIN_SPACE,
|
||||||
|
maxPlatformSpace = GATE.MAX_SPACE,
|
||||||
|
gates = [
|
||||||
|
{
|
||||||
|
x: canvas.width / 2 - GATE.WIDTH / 2,
|
||||||
|
y: GATE.START,
|
||||||
|
touched: false,
|
||||||
|
blocker: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
y = GATE.START,
|
||||||
|
keydown = false,
|
||||||
|
runtime = 0,
|
||||||
|
lastTimestamp;
|
||||||
|
|
||||||
|
img.onload = () => {
|
||||||
|
ctx.drawImage(img, beastie.x, beastie.y);
|
||||||
|
};
|
||||||
|
// img.src = "/webtoys/bbe/beasties/blinker_shadow_red.png";
|
||||||
|
|
||||||
|
Object.keys(images).forEach((key, i) => {
|
||||||
|
const input = document.createElement("input"),
|
||||||
|
label = document.createElement("label"),
|
||||||
|
bImage = images[key];
|
||||||
|
label.className = "beastieImageInputWrapper";
|
||||||
|
input.name = "beastie";
|
||||||
|
input.type = "radio";
|
||||||
|
// TODO: Add Cookie support
|
||||||
|
input.checked = i === 0;
|
||||||
|
input.value = key;
|
||||||
|
label.appendChild(input);
|
||||||
|
bImage.className = "beastieImage";
|
||||||
|
label.appendChild(bImage);
|
||||||
|
settingsBeastie.appendChild(label);
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderLightning = () => {
|
||||||
|
var color = "hsla(60, 100%, 50%, .7)";
|
||||||
|
|
||||||
|
ctx.save();
|
||||||
|
ctx.globalCompositeOperation = "lighter";
|
||||||
|
|
||||||
|
ctx.strokeStyle = color;
|
||||||
|
ctx.shadowColor = color;
|
||||||
|
|
||||||
|
ctx.fillStyle = color;
|
||||||
|
ctx.fillRect(enemy.x, enemy.y, enemy.width, canvas.height);
|
||||||
|
ctx.fillStyle = "hsla(0, 0%, 10%, 0.2)";
|
||||||
|
ctx.shadowBlur = 0;
|
||||||
|
ctx.globalCompositeOperation = "source-over";
|
||||||
|
ctx.fillRect(enemy.x, enemy.y, enemy.width, canvas.height);
|
||||||
|
ctx.globalCompositeOperation = "lighter";
|
||||||
|
ctx.shadowBlur = 15;
|
||||||
|
|
||||||
|
const lightning = createLightning(canvas, enemy);
|
||||||
|
ctx.beginPath();
|
||||||
|
for (let i = 0; i < lightning.length; i++) {
|
||||||
|
ctx.lineTo(lightning[i].x, Math.max(lightning[i].y, enemy.y));
|
||||||
|
}
|
||||||
|
ctx.stroke();
|
||||||
|
// requestAnimationFrame(renderLightning);
|
||||||
|
ctx.restore();
|
||||||
|
};
|
||||||
|
|
||||||
|
const restartGame = () => {
|
||||||
|
const whichBeastie = document.querySelector(
|
||||||
|
'input[name="beastie"]:checked'
|
||||||
|
).value,
|
||||||
|
settingsMode = document.getElementById("settingsMode");
|
||||||
|
|
||||||
|
if (settingsMode) {
|
||||||
|
const { value } = settingsMode;
|
||||||
|
game.winScore = game.winScores[value];
|
||||||
|
game.blockerFrequency = game.blockerFrequencies[value];
|
||||||
|
}
|
||||||
|
|
||||||
|
img.src = `/webtoys/bbe/beasties/${whichBeastie}`;
|
||||||
|
|
||||||
|
game.isPaused = false;
|
||||||
|
game.isOver = false;
|
||||||
|
game.showRestartModal = false;
|
||||||
|
y = GATE.START;
|
||||||
|
minPlatformSpace = GATE.MIN_SPACE;
|
||||||
|
maxPlatformSpace = GATE.MAX_SPACE;
|
||||||
|
// all the gates - starts in the bottom middle
|
||||||
|
gates = [
|
||||||
|
{
|
||||||
|
x: canvas.width / 2 - GATE.WIDTH / 2,
|
||||||
|
y: GATE.START,
|
||||||
|
touched: false,
|
||||||
|
blocker: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
beastie.dx = 0;
|
||||||
|
beastie.dy = 0;
|
||||||
|
beastie.boostEnd = 0;
|
||||||
|
beastie.score = 0;
|
||||||
|
beastie.x = canvas.width / 2 - 20;
|
||||||
|
beastie.y = canvas.height - 50;
|
||||||
|
|
||||||
|
player.dx = 0;
|
||||||
|
player.dy = 0;
|
||||||
|
|
||||||
|
enemy.y = canvas.height - 50;
|
||||||
|
|
||||||
|
runtime = 0;
|
||||||
|
|
||||||
|
while (y > 0) {
|
||||||
|
// the next gate can be placed above the previous one with a space
|
||||||
|
// somewhere between the min and max space
|
||||||
|
y -= GATE.HEIGHT + random(minPlatformSpace, maxPlatformSpace);
|
||||||
|
|
||||||
|
gates.push({
|
||||||
|
x: random(
|
||||||
|
GATE.MARGIN,
|
||||||
|
canvas.width - GATE.MARGIN - GATE.WIDTH
|
||||||
|
),
|
||||||
|
y,
|
||||||
|
touched: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// game loop
|
||||||
|
loop = (timestamp) => {
|
||||||
|
requestAnimationFrame(loop);
|
||||||
|
|
||||||
|
// limit FPS
|
||||||
|
if (timestamp - lastTimestamp <= game.interval) return;
|
||||||
|
|
||||||
|
if (!(game.isOver || game.isPaused))
|
||||||
|
runtime += timestamp - lastTimestamp;
|
||||||
|
lastTimestamp = timestamp;
|
||||||
|
|
||||||
|
if (game.isOver) {
|
||||||
|
ctx.save();
|
||||||
|
ctx.fillStyle = "yellow";
|
||||||
|
ctx.fillRect(
|
||||||
|
canvas.width / 4,
|
||||||
|
canvas.height / 4,
|
||||||
|
enemy.width / 2,
|
||||||
|
canvas.height / 2
|
||||||
|
);
|
||||||
|
ctx.fillStyle = "black";
|
||||||
|
ctx.font = "32px sans";
|
||||||
|
ctx.fillText(
|
||||||
|
beastie.y <= 0 ? "You Win!" : "Game Over",
|
||||||
|
canvas.width / 3,
|
||||||
|
canvas.height / 3,
|
||||||
|
canvas.width / 3
|
||||||
|
);
|
||||||
|
ctx.fillText(
|
||||||
|
`Score:
|
||||||
|
${beastie.score}`,
|
||||||
|
canvas.width / 3,
|
||||||
|
(canvas.height / 3) * 2,
|
||||||
|
canvas.width / 3
|
||||||
|
);
|
||||||
|
ctx.restore();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (game.showRestartModal) {
|
||||||
|
ctx.save();
|
||||||
|
ctx.fillStyle = "yellow";
|
||||||
|
ctx.fillRect(
|
||||||
|
canvas.width / 4,
|
||||||
|
canvas.height / 4,
|
||||||
|
enemy.width / 2,
|
||||||
|
canvas.height / 2
|
||||||
|
);
|
||||||
|
ctx.fillStyle = "black";
|
||||||
|
ctx.font = "48px sans";
|
||||||
|
ctx.fillText(
|
||||||
|
`Restart? (Y / N)`,
|
||||||
|
canvas.width / 3,
|
||||||
|
canvas.height / 3,
|
||||||
|
canvas.width / 3
|
||||||
|
);
|
||||||
|
ctx.restore();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (game.isPaused) {
|
||||||
|
ctx.save();
|
||||||
|
ctx.fillStyle = "yellow";
|
||||||
|
ctx.fillRect(
|
||||||
|
canvas.width / 4,
|
||||||
|
canvas.height / 3,
|
||||||
|
canvas.width / 2,
|
||||||
|
canvas.height / 3
|
||||||
|
);
|
||||||
|
ctx.fillStyle = "black";
|
||||||
|
ctx.font = "40px sans";
|
||||||
|
ctx.fillText(`Paused`, canvas.width / 3 - 5, canvas.height / 2);
|
||||||
|
ctx.restore();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
// if beastie reaches the middle of the screen, move the gates down
|
||||||
|
// instead of beastie up to make it look like beastie is going up
|
||||||
|
if (
|
||||||
|
beastie.y < canvas.height - canvas.height / 3 &&
|
||||||
|
beastie.dy < 0 &&
|
||||||
|
(beastie.score <= game.winScore || !game.winScore)
|
||||||
|
) {
|
||||||
|
gates = gates.map(({ y, ...otherProps }) => ({
|
||||||
|
...otherProps,
|
||||||
|
y: y - beastie.dy,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// add more gates to the top of the screen as beastie moves up
|
||||||
|
while (gates[gates.length - 1].y > 0) {
|
||||||
|
gates.push({
|
||||||
|
x: random(25, canvas.width - 25 - GATE.WIDTH),
|
||||||
|
y:
|
||||||
|
gates[gates.length - 1].y -
|
||||||
|
(GATE.HEIGHT +
|
||||||
|
random(minPlatformSpace, maxPlatformSpace)),
|
||||||
|
touched: false,
|
||||||
|
blocker:
|
||||||
|
game.hasBlockers &&
|
||||||
|
gates.filter(({ blocker }) => blocker).length /
|
||||||
|
gates.length <
|
||||||
|
game.blockerFrequency &&
|
||||||
|
Math.random() <= game.blockerFrequency
|
||||||
|
? true
|
||||||
|
: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// add a bit to the min/max gate space as the player goes up
|
||||||
|
minPlatformSpace += 0.5;
|
||||||
|
maxPlatformSpace += 0.5;
|
||||||
|
|
||||||
|
// cap max space
|
||||||
|
maxPlatformSpace = Math.min(
|
||||||
|
maxPlatformSpace,
|
||||||
|
canvas.height / 2
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// move enemy backwards
|
||||||
|
enemy.y -= beastie.dy;
|
||||||
|
} else {
|
||||||
|
if (beastie.y <= 0) beastie.dy = -10;
|
||||||
|
beastie.y += beastie.dy;
|
||||||
|
}
|
||||||
|
|
||||||
|
// apply drag when key not pressed
|
||||||
|
if (!keydown) {
|
||||||
|
if (player.dx < 0) {
|
||||||
|
beastie.dx += drag;
|
||||||
|
|
||||||
|
// don't let dx go above 0
|
||||||
|
if (beastie.dx > 0) {
|
||||||
|
beastie.dx = 0;
|
||||||
|
player.dx = 0;
|
||||||
|
}
|
||||||
|
} else if (player.dx > 0) {
|
||||||
|
beastie.dx -= drag;
|
||||||
|
|
||||||
|
if (beastie.dx < 0) {
|
||||||
|
beastie.dx = 0;
|
||||||
|
player.dx = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
player.dy === 0 ||
|
||||||
|
(player.dy < 0 && Date.now() > beastie.boostEnd)
|
||||||
|
) {
|
||||||
|
beastie.dy += drag;
|
||||||
|
|
||||||
|
// don't let dx go above 0
|
||||||
|
if (beastie.dy > game.speed) {
|
||||||
|
beastie.dy = game.speed;
|
||||||
|
player.dy = game.speed;
|
||||||
|
}
|
||||||
|
} else if (player.dy > 0) {
|
||||||
|
beastie.dy -= drag;
|
||||||
|
|
||||||
|
if (beastie.dy < 0) {
|
||||||
|
beastie.dy = 0;
|
||||||
|
player.dy = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
beastie.x += beastie.dx;
|
||||||
|
|
||||||
|
// make beastie wrap the screen
|
||||||
|
if (beastie.x + beastie.width < 0) {
|
||||||
|
beastie.x = canvas.width;
|
||||||
|
} else if (beastie.x > canvas.width) {
|
||||||
|
beastie.x = -beastie.width;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.fillStyle = "green";
|
||||||
|
gates = gates.map((gate) => {
|
||||||
|
// draw gates
|
||||||
|
ctx.save();
|
||||||
|
if (gate.blocker) ctx.fillStyle = "red";
|
||||||
|
else ctx.fillStyle = gate.touched ? "lime" : "green";
|
||||||
|
ctx.fillRect(gate.x, gate.y, GATE.WIDTH, GATE.HEIGHT);
|
||||||
|
if (gate.blocker) {
|
||||||
|
ctx.save();
|
||||||
|
ctx.lineWidth = 4;
|
||||||
|
ctx.strokeStyle = "black";
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(gate.x, gate.y);
|
||||||
|
ctx.lineTo(gate.x + GATE.WIDTH, gate.y + GATE.HEIGHT);
|
||||||
|
ctx.moveTo(gate.x + GATE.WIDTH, gate.y);
|
||||||
|
ctx.lineTo(gate.x, gate.y + GATE.HEIGHT);
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
// make beastie stop if it collides with a blocking gate
|
||||||
|
if (gate.blocker && hasCollision(gate)) {
|
||||||
|
beastie.dy = game.speed * 0.5;
|
||||||
|
gate.touched = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// make beastie jump if it collides with a gate from above
|
||||||
|
if (
|
||||||
|
// beastie is falling
|
||||||
|
// beastie.dy > 0 &&
|
||||||
|
// beastie was previous above the gate
|
||||||
|
// prevDoodleY + doodle.height <= gate.y &&
|
||||||
|
// doodle collides with gate
|
||||||
|
|
||||||
|
!gate.blocker &&
|
||||||
|
// !gate.touched &&
|
||||||
|
hasCollision(gate)
|
||||||
|
) {
|
||||||
|
// reset beastie position so it's on top of the gate
|
||||||
|
// beastie.y = gate.y - beastie.height;
|
||||||
|
if (!gate.touched) {
|
||||||
|
beastie.dy = Math.max(
|
||||||
|
game.maxSpeed,
|
||||||
|
game.speedBoost.multiplier *
|
||||||
|
Math.min(beastie.dy, game.speed)
|
||||||
|
);
|
||||||
|
beastie.score += 100;
|
||||||
|
}
|
||||||
|
beastie.boostEnd =
|
||||||
|
Date.now() + game.speedBoost.duration * 1000;
|
||||||
|
gate.touched = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return gate;
|
||||||
|
});
|
||||||
|
|
||||||
|
// check on enemy
|
||||||
|
if (!runtime) runtime = 0;
|
||||||
|
|
||||||
|
if (runtime > enemy.delaySeconds * 1000)
|
||||||
|
enemy.y = enemy.y + enemy.speed;
|
||||||
|
if (enemy.y < 1) {
|
||||||
|
enemy.y = 0;
|
||||||
|
game.isOver = true;
|
||||||
|
} else if (enemy.y > beastie.y + game.maxLead)
|
||||||
|
enemy.y = beastie.y + game.maxLead;
|
||||||
|
|
||||||
|
// draw beastie
|
||||||
|
ctx.drawImage(
|
||||||
|
img,
|
||||||
|
beastie.x,
|
||||||
|
beastie.y,
|
||||||
|
beastie.width,
|
||||||
|
beastie.height
|
||||||
|
);
|
||||||
|
|
||||||
|
// +100 points for each offscreen blocker that wasn't touched
|
||||||
|
beastie.score +=
|
||||||
|
100 *
|
||||||
|
gates.filter(
|
||||||
|
(gate) =>
|
||||||
|
gate.y > canvas.height && gate.blocker && !gate.touched
|
||||||
|
).length;
|
||||||
|
// remove any offscreen gates
|
||||||
|
gates = gates.filter((gate) => gate.y <= canvas.height);
|
||||||
|
|
||||||
|
beastie.score += Math.floor(Math.abs(beastie.dy));
|
||||||
|
ctx.fillStyle = "white";
|
||||||
|
ctx.font = "12px sans";
|
||||||
|
ctx.fillText(`Score: ${beastie.score}`, 10, 20);
|
||||||
|
|
||||||
|
renderLightning();
|
||||||
|
};
|
||||||
|
|
||||||
|
// listen to keyboard events to move beastie
|
||||||
|
document.addEventListener("keydown", (e) => {
|
||||||
|
if (keydown) return;
|
||||||
|
|
||||||
|
if (e.code === "ArrowLeft") {
|
||||||
|
keydown = true;
|
||||||
|
player.dx = -1;
|
||||||
|
beastie.dx = -3;
|
||||||
|
} else if (e.code === "ArrowRight") {
|
||||||
|
keydown = true;
|
||||||
|
player.dx = 1;
|
||||||
|
beastie.dx = 3;
|
||||||
|
} else if (e.code === "KeyP") {
|
||||||
|
keydown = true;
|
||||||
|
game.isPaused = !game.isPaused;
|
||||||
|
} else if (e.code === "KeyR") {
|
||||||
|
keydown = true;
|
||||||
|
if (game.isOver) {
|
||||||
|
restartGame();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
game.isPaused = true;
|
||||||
|
game.showRestartModal = true;
|
||||||
|
} else if (e.code === "KeyS") {
|
||||||
|
keydown = true;
|
||||||
|
game.isPaused = true;
|
||||||
|
document.getElementById("settings").classList.add("showSettings");
|
||||||
|
} else if (e.code === "KeyY") {
|
||||||
|
keydown = true;
|
||||||
|
if (game.showRestartModal) restartGame();
|
||||||
|
} else if (e.code === "KeyN") {
|
||||||
|
keydown = true;
|
||||||
|
if (game.showRestartModal) {
|
||||||
|
game.showRestartModal = false;
|
||||||
|
game.isPaused = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
} else if (e.code === "ArrowDown") {
|
||||||
|
keydown = true;
|
||||||
|
player.dy = 1;
|
||||||
|
beastie.dy = 3;
|
||||||
|
} else if (e.code === "ArrowUp") {
|
||||||
|
keydown = true;
|
||||||
|
player.dy = -1;
|
||||||
|
beastie.dy = -3;
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("keyup", () => {
|
||||||
|
keydown = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
canvas.addEventListener(
|
||||||
|
"mousedown",
|
||||||
|
(e) => {
|
||||||
|
keydown = true;
|
||||||
|
const result = getMousePos(canvas, e);
|
||||||
|
if (result.x < canvas.width / 2) {
|
||||||
|
player.dx = -1;
|
||||||
|
beastie.dx = -3;
|
||||||
|
} else {
|
||||||
|
player.dx = 1;
|
||||||
|
beastie.dx = 3;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
canvas.addEventListener(
|
||||||
|
"mousemove",
|
||||||
|
(e) => {
|
||||||
|
if (!keydown) return;
|
||||||
|
const result = getMousePos(canvas, e);
|
||||||
|
if (result.x < beastie.x) {
|
||||||
|
player.dx = -1;
|
||||||
|
beastie.dx = -3;
|
||||||
|
} else {
|
||||||
|
player.dx = 1;
|
||||||
|
beastie.dx = 3;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
canvas.addEventListener(
|
||||||
|
"mouseup",
|
||||||
|
() => {
|
||||||
|
keydown = false;
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
canvas.addEventListener(
|
||||||
|
"touchstart",
|
||||||
|
(e) => {
|
||||||
|
mousePos = getTouchPos(canvas, e);
|
||||||
|
const touch = e.touches[0],
|
||||||
|
mouseEvent = new MouseEvent("mousedown", {
|
||||||
|
clientX: touch.clientX,
|
||||||
|
clientY: touch.clientY,
|
||||||
|
});
|
||||||
|
canvas.dispatchEvent(mouseEvent);
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
canvas.addEventListener(
|
||||||
|
"touchend",
|
||||||
|
() => {
|
||||||
|
var mouseEvent = new MouseEvent("mouseup", {});
|
||||||
|
canvas.dispatchEvent(mouseEvent);
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
canvas.addEventListener(
|
||||||
|
"touchmove",
|
||||||
|
(e) => {
|
||||||
|
const touch = e.touches[0];
|
||||||
|
keydown = true;
|
||||||
|
if (touch.clientX < beastie.x) {
|
||||||
|
player.dx = -1;
|
||||||
|
beastie.dx = -3;
|
||||||
|
} else {
|
||||||
|
player.dx = 1;
|
||||||
|
beastie.dx = 3;
|
||||||
|
}
|
||||||
|
canvas.dispatchEvent(mouseEvent);
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
document.getElementById("cancelSettings").addEventListener("click", (e) => {
|
||||||
|
// close modal
|
||||||
|
document.getElementById("settings").classList.remove("showSettings");
|
||||||
|
// leave game paused
|
||||||
|
});
|
||||||
|
document.getElementById("applySettings").addEventListener("click", (e) => {
|
||||||
|
// close modal
|
||||||
|
document.getElementById("settings").classList.remove("showSettings");
|
||||||
|
// restart game
|
||||||
|
restartGame();
|
||||||
|
});
|
||||||
|
document
|
||||||
|
.getElementById("toggleSettings")
|
||||||
|
?.addEventListener("click", (e) => {
|
||||||
|
// pause game
|
||||||
|
game.isPaused = true;
|
||||||
|
|
||||||
|
// show settings
|
||||||
|
document
|
||||||
|
.getElementById("settings")
|
||||||
|
.classList.toggle("showSettings");
|
||||||
|
});
|
||||||
|
|
||||||
|
// start the game
|
||||||
|
restartGame();
|
||||||
|
|
||||||
|
requestAnimationFrame(loop);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { startGame };
|
45
src/assets/_root/webtoys/bbe/scripts/lightning.js
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
export const createLightning = (canvas, enemy) => {
|
||||||
|
const { y: size } = enemy;
|
||||||
|
var center = { x: canvas.width / 2, y: canvas.height };
|
||||||
|
var minSegmentHeight = 5;
|
||||||
|
var roughness = 2;
|
||||||
|
var maxDifference = size / 5;
|
||||||
|
|
||||||
|
let lightning = [];
|
||||||
|
let segmentHeight = size / 3;
|
||||||
|
lightning.push({
|
||||||
|
x: center.x,
|
||||||
|
y: center.y + 200,
|
||||||
|
});
|
||||||
|
lightning.push({
|
||||||
|
x: Math.random() * (canvas.width - 100) + 50,
|
||||||
|
y: Math.abs((Math.random() - 0.9) * 100),
|
||||||
|
});
|
||||||
|
let currDiff = maxDifference;
|
||||||
|
while (segmentHeight > minSegmentHeight) {
|
||||||
|
const newSegments = [];
|
||||||
|
for (var i = 0; i < lightning.length - 1; i++) {
|
||||||
|
const start = lightning[i],
|
||||||
|
end = lightning[i + 1],
|
||||||
|
midX = (start.x + end.x) / 2,
|
||||||
|
newX = midX + (Math.random() * 2 - 1) * currDiff;
|
||||||
|
newSegments.push(start, { x: newX, y: (start.y + end.y) / 2 });
|
||||||
|
}
|
||||||
|
|
||||||
|
newSegments.push(lightning.pop());
|
||||||
|
lightning = newSegments;
|
||||||
|
|
||||||
|
currDiff /= roughness;
|
||||||
|
segmentHeight /= 2;
|
||||||
|
}
|
||||||
|
return lightning;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get the position of the mouse relative to the canvas
|
||||||
|
export const getMousePos = (canvasDom, mouseEvent) => {
|
||||||
|
var rect = canvasDom.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
x: mouseEvent.clientX - rect.left,
|
||||||
|
y: mouseEvent.clientY - rect.top,
|
||||||
|
};
|
||||||
|
};
|
32
src/assets/_root/webtoys/bbe/scripts/main.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
// @license magnet:?xt=urn:btih:90dc5c0be029de84e523b9b3922520e79e0e6f08&dn=cc0.txt CC0-1.0
|
||||||
|
/****************************************************************************
|
||||||
|
* Planar Vagabond's Guide to the Multiverse (planarvagabond.com)
|
||||||
|
*
|
||||||
|
* Copyright 2023-2024 Eric Woodward
|
||||||
|
* Source released under CC0 Public Domain License v1.0
|
||||||
|
* https://www.planarvagabond.com/licenses/cc0/
|
||||||
|
* http://creativecommons.org/publicdomain/zero/1.0/
|
||||||
|
****************************************************************************/
|
||||||
|
|
||||||
|
import { startGame } from "./bbe.js";
|
||||||
|
|
||||||
|
export default (() => {
|
||||||
|
// we load this library as a module to guarantee baseline ES6 functionality
|
||||||
|
|
||||||
|
// Indicate JS is loaded
|
||||||
|
document.documentElement.className =
|
||||||
|
document.documentElement.className.replace("no-js", "js");
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
const canvas = document.createElement("canvas"),
|
||||||
|
contentDiv = document.getElementById("game");
|
||||||
|
|
||||||
|
canvas.setAttribute("width", "375");
|
||||||
|
canvas.setAttribute("height", "667");
|
||||||
|
canvas.setAttribute("id", "game");
|
||||||
|
contentDiv.appendChild(canvas);
|
||||||
|
|
||||||
|
startGame(canvas);
|
||||||
|
}, 1);
|
||||||
|
})();
|
||||||
|
// @license-end
|
82
src/assets/_root/webtoys/bbe/styles.css
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
html, body {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: #333;
|
||||||
|
color: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
border: 1px solid black;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.beastieImage {
|
||||||
|
max-width: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.beastieImageInputWrapper {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
padding: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-width: 375px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings {
|
||||||
|
background-color: rgba(33, 33, 33, .95);
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
opacity: 0;
|
||||||
|
padding: 0 1rem 1rem;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
transition: opacity ease 0.3s;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings.showSettings {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settingsBeastie {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settingsBeastie label {
|
||||||
|
padding: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settingsFooter {
|
||||||
|
margin: 1rem;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggleSettings {
|
||||||
|
background-color: rgba(33, 33, 33, .9);
|
||||||
|
border: 1px solid rgba(33, 33, 33, .9);
|
||||||
|
cursor: pointer;
|
||||||
|
position: absolute;
|
||||||
|
right: 10px;
|
||||||
|
top: 5px;
|
||||||
|
z-index: 2;
|
||||||
|
}
|