summaryrefslogtreecommitdiff
path: root/2026/games/pralkarz/dzem.js
diff options
context:
space:
mode:
authorLLLL Colonq <llll@colonq>2026-08-26 23:47:59 -0400
committerLLLL Colonq <llll@colonq>2026-08-26 23:47:59 -0400
commit0d2d8cd897e94bd94e0b3ae7c6ce9aa580a44a0a (patch)
treee9039d1c3aedca515d6cb499451d4b73aca4f5fd /2026/games/pralkarz/dzem.js
parent242943e5b023ce40eb3be826c418a478d4672f78 (diff)
Add a_tension_span
Diffstat (limited to '2026/games/pralkarz/dzem.js')
-rw-r--r--2026/games/pralkarz/dzem.js483
1 files changed, 0 insertions, 483 deletions
diff --git a/2026/games/pralkarz/dzem.js b/2026/games/pralkarz/dzem.js
deleted file mode 100644
index 5703577..0000000
--- a/2026/games/pralkarz/dzem.js
+++ /dev/null
@@ -1,483 +0,0 @@
-const WIDTH = 240;
-const HEIGHT = 160;
-
-const HOOK_WIDTH = 24;
-const HOOK_HEIGHT = 40;
-
-// TODO: Misters of size 24 are tweaking.
-const MISTER_SIZES = [16, 20, 28, 32];
-
-class AssetManager {
- /** @type {HTMLImageElement[]} */
- images = [];
- /** @type {HTMLAudioElement[]} */
- sounds = [];
-
- /** @type {HTMLImageElement} */
- green;
- /** @type {HTMLImageElement} */
- hook;
- /** @type {HTMLImageElement} */
- red;
- /** @type {HTMLImageElement} */
- water;
-
- /** @type {HTMLAudioElement} */
- lose;
- /** @type {HTMLAudioElement} */
- win;
-
- constructor() {
- this.green = new Image();
- this.green.src = "assets/green.webp";
- this.images.push(this.green);
-
- this.hook = new Image();
- this.hook.src = "assets/hook.png";
- this.images.push(this.hook);
-
- this.numbers = new Image();
- this.numbers.src = "assets/numbers.png";
- this.images.push(this.numbers);
-
- this.red = new Image();
- this.red.src = "assets/red.webp";
- this.images.push(this.red);
-
- this.water = new Image();
- this.water.src = "assets/water.png";
- this.images.push(this.water);
-
- this.lose = new Audio();
- this.lose.src = "assets/lose.wav";
- this.lose.load();
- this.sounds.push(this.lose);
-
- this.win = new Audio();
- this.win.src = "assets/win.wav";
- this.win.load();
- this.sounds.push(this.win);
- }
-}
-
-const state = {
- /** @type {?HTMLCanvasElement} */
- canvas: null,
- /** @type {?CanvasRenderingContext2D} */
- ctx: null,
-
- /** @type {boolean} */
- ready: false,
-
- /** @type {boolean} */
- started: false,
- /** @type {number} */
- difficulty: 0,
-
- /** @type {number} */
- frameCounter: 0,
- /** @type {number} */
- previousFrameTime: 0,
- /** @type {boolean} */
- resetPreviousFrameTime: false,
- /** @type {number} */
- targetFrameTime: 1000 / 60,
-
- /** @type {number} */
- mousePosX: 0,
- /** @type {boolean} */
- lmbPressed: false,
- /** @type {boolean} */
- rmbPressed: false,
-
- /** @type {AssetManager} */
- assetManager: new AssetManager(),
-
- /** @type {number} */
- hookX: 0,
- /** @type {number} */
- hookY: 0,
- /** @type {number} */
- hookVelocity: -1,
-
- /** @type {Mister[]} */
- misters: [],
- /** @type {boolean} */
- yoinkingMister: false,
- /** @type {boolean} */
- ceaseYoinkage: false,
-
- /** @type {number} */
- elapsed: 0,
- /** @type {number} */
- timer: 7,
-
- /** @type {boolean} */
- won: false,
- /** @type {number} */
- wonFrames: 0,
- /** @type {boolean} */
- lost: false,
- /** @type {number} */
- lostFrames: 0,
-};
-
-const resetState = () => {
- state.ceaseYoinkage = false;
- state.difficulty = 0;
- state.elapsed = 0;
- state.hookVelocity = -1;
- state.lmbPressed = false;
- state.lost = false;
- state.lostFrames = 0;
- state.misters = [];
- state.previousFrameTime = 0;
- state.rmbPressed = false;
- state.started = false;
- state.timer = 7;
- state.won = false;
- state.wonFrames = 0;
- state.yoinkingMister = false;
-};
-
-class Mister {
- /** @type {HTMLImageElement} */
- image;
-
- /** @type {boolean} */
- isBad;
-
- /** @type {number} */
- size;
-
- /** @type {number} */
- x;
- /** @type {number} */
- y;
-
- /** @type {number} */
- velocity;
-
- /** @type {boolean} */
- canBeYoinked;
- /** @type {number} */
- yoinkCooldown;
- /** @type {boolean} */
- isBeingYoinked;
-
- /** @type {boolean} */
- shouldBePurged;
-
- /**
- * @param {HTMLImageElement} image
- * @param {boolean} isBad
- */
- constructor(image, isBad) {
- this.image = image;
- this.isBad = isBad;
-
- this.size = MISTER_SIZES[Math.floor(Math.random() * MISTER_SIZES.length)];
-
- const left = Math.random() < 0.5;
- this.x = left ? -this.size : WIDTH;
-
- const minY = this.isBad ? HOOK_HEIGHT + 15 : HEIGHT - 64;
- this.y = getRandomInt(minY, HEIGHT - this.size / 2);
-
- const maxSpeed = 2 + Math.floor(state.difficulty / 5);
- const speed = getRandomInt(1, maxSpeed);
- this.velocity = left ? speed : -speed;
-
- this.canBeYoinked = true;
- this.yoinkCooldown = 0;
- this.isBeingYoinked = false;
-
- this.shouldBePurged = false;
- }
-
- draw() {
- state.ctx.drawImage(this.image, this.x, this.y, this.size, this.size);
- }
-
- async update() {
- this.yoinkCooldown -= 1;
- if (this.yoinkCooldown < 0) {
- this.canBeYoinked = true;
- }
-
- if (!this.isBeingYoinked) {
- this.x += this.velocity;
- } else {
- if (state.ceaseYoinkage) {
- state.yoinkingMister = false;
- state.ceaseYoinkage = false;
-
- this.canBeYoinked = false;
- this.yoinkCooldown = 60;
- this.isBeingYoinked = false;
-
- this.x += this.velocity;
- } else {
- this.x = state.hookX - this.size / 4;
- this.y = state.hookY + HOOK_HEIGHT / 2;
- }
- }
-
- if (this.isBeingYoinked && this.y <= -5) {
- if (this.isBad) {
- state.assetManager.lose.play();
- state.lost = true;
- state.lostFrames = 5;
- } else {
- state.assetManager.win.play();
- state.won = true;
- state.wonFrames = 5;
- }
-
- return;
- }
-
- if (
- !state.yoinkingMister &&
- this.canBeYoinked &&
- state.hookVelocity < 0 &&
- aabb(
- this.x,
- this.x + this.size,
- this.y,
- this.y + this.size,
- state.hookX,
- state.hookX + 15,
- state.hookY + 20,
- state.hookY + 30,
- )
- ) {
- state.yoinkingMister = true;
- this.isBeingYoinked = true;
- }
-
- this.shouldBePurged = this.x < 0 - this.size || this.x > WIDTH;
- }
-}
-
-const getRandom = (min, max) => {
- return Math.random() * (max - min) + min;
-};
-
-/**
- * @param {number} min
- * @param {number} max
- */
-const getRandomInt = (min, max) => {
- return Math.floor(Math.random() * (max - min + 1) + min);
-};
-
-/**
- * @param {number} left1
- * @param {number} right1
- * @param {number} top1
- * @param {number} bottom1
- * @param {number} left2
- * @param {number} right2
- * @param {number} top2
- * @param {number} bottom2
- */
-const aabb = (left1, right1, top1, bottom1, left2, right2, top2, bottom2) => {
- return left1 < right2 && right1 > left2 && top1 < bottom2 && bottom1 > top2;
-};
-
-/**
- * @param {number} currentTime
- */
-const update = (currentTime) => {
- state.frameCounter += 1;
-
- if (state.resetPreviousFrameTime) {
- state.previousFrameTime = currentTime;
- state.resetPreviousFrameTime = false;
- }
-
- state.elapsed += (currentTime - state.previousFrameTime) / 1000;
-
- if (state.won && state.wonFrames <= 0) {
- window.parent.postMessage({ op: "done", win: true });
- resetState();
- return;
- }
-
- if (state.lost && state.lostFrames <= 0) {
- window.parent.postMessage({ op: "done", win: false });
- resetState();
- return;
- }
-
- if (state.wonFrames > 0 || state.lostFrames > 0) {
- state.misters = [];
-
- if (state.frameCounter % 24 === 0) {
- state.wonFrames -= 1;
- state.lostFrames -= 1;
- }
-
- return;
- }
-
- if (state.timer - Math.floor(state.elapsed) <= 0) {
- state.assetManager.lose.play();
- state.lost = true;
- state.lostFrames = 5;
- return;
- }
-
- state.hookY = Math.max(
- Math.min(state.hookY + state.hookVelocity, 160 - HOOK_HEIGHT),
- -((HOOK_HEIGHT / 4) * 3),
- );
-
- const spawnRate = 0.03 + Math.min(state.difficulty, 10) * 0.01;
- if (Math.random() < 0.03 + spawnRate) {
- const isBad =
- Math.random() >
- 0.3 -
- (state.difficulty > 10 ? 0.1 : 0) +
- Math.floor(state.elapsed / 3) * 0.05;
- state.misters.push(
- new Mister(
- isBad ? state.assetManager.red : state.assetManager.green,
- isBad,
- ),
- );
- }
-
- state.misters.forEach((mister) => mister.update());
- state.misters = state.misters.filter((mister) => !mister.shouldBePurged);
-
- state.previousFrameTime = currentTime;
-};
-
-const draw = () => {
- state.ctx.drawImage(state.assetManager.water, 0, 0);
-
- if (state.lostFrames > 0 || state.wonFrames > 0) {
- const size =
- 16 * 2 ** (5 - (state.won ? state.wonFrames : state.lostFrames));
- state.ctx.drawImage(
- state.won ? state.assetManager.green : state.assetManager.red,
- WIDTH / 2 - size / 2,
- HEIGHT / 2 - size / 2,
- size,
- size,
- );
- return;
- }
-
- const hook = state.assetManager.hook;
- state.hookX = Math.max(state.mousePosX - hook.width / 2, 0);
- state.hookX = Math.floor(Math.min(WIDTH - hook.width, state.hookX));
- state.ctx.drawImage(hook, state.hookX, state.hookY);
-
- if (state.hookY > 0) {
- state.ctx.lineWidth = 1;
- state.ctx.beginPath();
- state.ctx.moveTo(state.hookX + 18 - 0.5, 0);
- state.ctx.lineTo(state.hookX + 18 - 0.5, state.hookY);
- state.ctx.stroke();
- }
-
- state.misters.forEach((mister) => mister.draw());
-
- const timerStr = (state.timer - Math.floor(state.elapsed)).toString();
-
- // The magic `4` is the padding between digits so that they're not glued together.
- let x = WIDTH - 4 - (timerStr.length * 16 + (timerStr.length - 1) * 4);
- for (const strDigit of timerStr) {
- const digit = Number(strDigit);
- state.ctx.drawImage(
- state.assetManager.numbers,
- digit * 16,
- 0,
- 16,
- 24,
- x,
- 4,
- 16,
- 24,
- );
- x += 16 + 4;
- }
-};
-
-/**
- * @param {number} currentTime
- */
-const frame = (currentTime) => {
- if (
- !state.ready &&
- state.assetManager.images.every((img) => img.complete) &&
- state.assetManager.sounds.every((sound) => sound.readyState === 4)
- ) {
- window.parent.postMessage({ op: "ready" });
- state.ready = true;
-
- state.previousFrameTime = currentTime;
- }
-
- if (state.started) {
- if (currentTime - state.previousFrameTime >= state.targetFrameTime) {
- update(currentTime);
- }
-
- draw();
- }
-
- requestAnimationFrame(frame);
-};
-
-const initialize = () => {
- window.addEventListener("message", (ev) => {
- switch (ev.data.op) {
- case "start":
- state.started = true;
- state.difficulty = ev.data.difficulty;
- state.timer = 10 + Math.floor(state.difficulty / 5);
- // This makes sure the countdown is correct. Ugly and there's probably a better solution.
- state.resetPreviousFrameTime = true;
-
- window.parent.postMessage({ op: "started", verb: "yoink!" });
- break;
- default:
- console.log(`[dżem] Unexpected event: ${ev}`);
- break;
- }
- });
-
- state.canvas = document.getElementById("dzem");
- state.ctx = state.canvas.getContext("2d");
- state.ctx.imageSmoothingEnabled = false;
-
- state.canvas.addEventListener("mousemove", (e) => {
- state.mousePosX = e.clientX;
- });
-
- state.canvas.addEventListener("mousedown", (e) => {
- if (e.button === 0) {
- state.hookVelocity = 2;
- }
- });
-
- state.canvas.addEventListener("mouseup", (e) => {
- if (e.button === 0) {
- state.hookVelocity = -2;
- } else if (e.button === 2) {
- state.ceaseYoinkage = true;
- }
- });
-
- state.canvas.addEventListener("contextmenu", (e) => {
- e.preventDefault();
- });
-
- frame(0);
-};
-
-initialize();