ONO::FW::Edu::Apps::JS::SkiingScript

package ONO::FW::Edu::Apps::JS::SkiingScript;
################################################################################
# COPYRIGHT / LICENSE #
################################################################################
#
# This file is part of the ONO Software Project.
#
# Copyright (C) 2022-2026 Jos Kirps and The Joopita Project [ASBL]
# Copyright (C) 2011-2022 Jos Kirps and Joopita Research ASBL
# Copyright (C) 2006-2011 Jos Kirps and The Joopita Project
# Copyright (C) 2001-2006 Jos Kirps and The WobbleWolf Project
#
# This file, as well as many other parts of the ONO Software Project or
# related elements, are FREE SOFTWARE available under the ARTISTIC LICENSE 2.0.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#
# For the full license, see /ono/osr/license/LICENSE.txt, or write to
# jos_AT_kirps_DOT_com or contact_AT_joopita_DOT_com.
#
################################################################################
# END OF COPYRIGHT / LICENSE, HERE COMES THE CODE ... #
################################################################################

#: This file contains an auto-generated module that have been compiled using pure HTML / JS files created for the Morzino Project (morzino.com) and that have been released as Open Source software.


use strict;

# This is an auto-generated file created by ONO_FW_Apps_Dev
# DIR = ono/sys/ONO/Ext/Morzino/Apps/JS, APP = Skiing, MOD = ONO::FW::Edu::Apps::Modules::Skiing
#
# Make sure this will pass mksys error checking: disable_subcall_checking_file
#

# INPUT VAR : worldHeight
# INPUT VAR : maxSpeed
# INPUT VAR : minGateWidth
# INPUT VAR : maxGateWidth
# INPUT VAR : gateProbability
# INPUT VAR : treeProbability
# INPUT VAR : rockProbability
# INPUT VAR : finishText
# ACTION : alert_you_won^correct_next^
# FILEDIR search in: ono/sys/ONO/Ext/Morzino/Apps/JS/Skiing
# FILEDIR found: Script


sub code {

my (
$self,
$worldHeight,
$maxSpeed,
$minGateWidth,
$maxGateWidth,
$gateProbability,
$treeProbability,
$rockProbability,
$finishText
) = @_;


return qq~



<canvas id="gameCanvas" width="920" height="480" tabindex="0"></canvas>
<script>
var isMobile = true; // Mobile control flag, true by default
var canvas = document.getElementById("gameCanvas");
var ctx = canvas.getContext("2d");
// Focus the canvas on page load
canvas.focus();
const worldHeight = $worldHeight;
const maxSpeed = $maxSpeed;
const finishText = '$finishText';
const gateProbability = $gateProbability;
const treeProbability = $treeProbability;
const rockProbability = $rockProbability;
const minGateWidth = $minGateWidth;
const maxGateWidth = $maxGateWidth;
var width = 920;
var height = 480;
var midX = width / 2; // 460
var midY = height / 2 - 100; // 140, player moved up by 100px
var trees = \[\];
var rocks = \[\];
var gates = \[\];
var keys = {};
var moving = false;
var lastHitTree = null;
var lastHitRock = null;
var angle = 0;
var startTime = null;
var endTime = null;
var lastDeductTime = null;
var score = 0;
var currentSpeed = 0;
var worldY = 0;
var finalGateAdded = false;
var finalGateY = null;
var endLineY = null;
var timerStopped = false;
var collisionFlash = 0;
var lastTime = 0;
// Add mobile control divs if isMobile is true
if (isMobile) {
var leftControl = document.createElement("div");
leftControl.id = "leftControl";
leftControl.className = "mobile-control";
canvas.parentNode.appendChild(leftControl);
var rightControl = document.createElement("div");
rightControl.id = "rightControl";
rightControl.className = "mobile-control";
canvas.parentNode.appendChild(rightControl);
// Event listeners for mobile controls
leftControl.addEventListener("mousedown", function() {
keys\["ArrowLeft"\] = true;
moving = true;
});
leftControl.addEventListener("mouseup", function() {
keys\["ArrowLeft"\] = false;
});
rightControl.addEventListener("mousedown", function() {
keys\["ArrowRight"\] = true;
moving = true;
});
rightControl.addEventListener("mouseup", function() {
keys\["ArrowRight"\] = false;
});
}

function ono_button_left() {
keys\["ArrowLeft"\] = true;
moving = true;
setTimeout(function() {
keys\["ArrowLeft"\] = false;
}, 250); // Simulate brief press
}

function ono_button_right() {
keys\["ArrowRight"\] = true;
moving = true;
setTimeout(function() {
keys\["ArrowRight"\] = false;
}, 250); // Simulate brief press
}

// Initialize game objects
function init() {
let fullWidth = minGateWidth + Math.random() * (maxGateWidth - minGateWidth);
let halfWidth = fullWidth / 2;
gates.push({
x: midX,
y: midY + 50,
passed: false,
halfWidth: halfWidth
});
for (let i = 0; i < 5; i++) {
let scale, x, y, lightness, hue, sat, light;
let attempts = 0;
const maxAttempts = 10;
do {
scale = 0.5 + Math.random();
x = Math.random() * width;
let treeFullHeight = 100 * scale;
let minY = -treeFullHeight;
let maxY = midY - 50 - treeFullHeight;
y = minY + Math.random() * (maxY - minY);
lightness = Math.floor(Math.random() * 20 + 40);
hue = Math.random() * 60;
sat = 50 + Math.random() * 50;
light = 20 + Math.random() * 20;
attempts++;
} while ((isTreeOverlapping(x, y, scale) || isTreeOverlappingGate(x, y, scale) || isRockOverlapping(x, y, scale)) && attempts < maxAttempts); // disable_whilecounter_protection
if (attempts < maxAttempts) {
trees.push({
x: x,
y: y,
color: `hsl(120, 100\%, \${lightness}\%)`,
trunkColor: `hsl(\${hue}, \${sat}\%, \${light}\%)`,
scale: scale
});
}
}
for (let i = 0; i < 3; i++) {
let scale, x, y, lightness, hue, sat, light;
let attempts = 0;
const maxAttempts = 20;
do {
scale = 0.5 + Math.random();
x = 50 + (Math.random() - 0.5) * 20;
let treeFullHeight = 100 * scale;
let minY = -treeFullHeight;
let maxY = midY - 50 - treeFullHeight;
y = minY + Math.random() * (maxY - minY);
lightness = Math.floor(Math.random() * 20 + 40);
hue = Math.random() * 60;
sat = 50 + Math.random() * 50;
light = 20 + Math.random() * 20;
attempts++;
} while ((isTreeOverlapping(x, y, scale) || isTreeOverlappingGate(x, y, scale) || isRockOverlapping(x, y, scale)) && attempts < maxAttempts); // disable_whilecounter_protection
if (attempts < maxAttempts) {
trees.push({
x: x,
y: y,
color: `hsl(120, 100\%, \${lightness}\%)`,
trunkColor: `hsl(\${hue}, \${sat}\%, \${light}\%)`,
scale: scale
});
} else {
trees.push({
x: x,
y: y,
color: `hsl(120, 100\%, \${lightness}\%)`,
trunkColor: `hsl(\${hue}, \${sat}\%, \${light}\%)`,
scale: scale
});
}
attempts = 0;
do {
scale = 0.5 + Math.random();
x = 870 + (Math.random() - 0.5) * 20;
let treeFullHeight = 100 * scale;
let minY = -treeFullHeight;
let maxY = midY - 50 - treeFullHeight;
y = minY + Math.random() * (maxY - minY);
lightness = Math.floor(Math.random() * 20 + 40);
hue = Math.random() * 60;
sat = 50 + Math.random() * 50;
light = 20 + Math.random() * 20;
attempts++;
} while ((isTreeOverlapping(x, y, scale) || isTreeOverlappingGate(x, y, scale) || isRockOverlapping(x, y, scale)) && attempts < maxAttempts); // disable_whilecounter_protection
if (attempts < maxAttempts) {
trees.push({
x: x,
y: y,
color: `hsl(120, 100\%, \${lightness}\%)`,
trunkColor: `hsl(\${hue}, \${sat}\%, \${light}\%)`,
scale: scale
});
} else {
trees.push({
x: x,
y: y,
color: `hsl(120, 100\%, \${lightness}\%)`,
trunkColor: `hsl(\${hue}, \${sat}\%, \${light}\%)`,
scale: scale
});
}
}
}
init(); // Call init directly since font loading is removed
document.addEventListener("keydown", function(e) {
keys\[e.key\] = true;
if (e.key === "ArrowLeft" || e.key === "ArrowRight" || e.key === " ") {
moving = true;
}
});
document.addEventListener("keyup", function(e) {
keys\[e.key\] = false;
});
function isTreeOverlapping(newX, newY, newScale) {
var newHalfWidth = 22.5 * newScale;
var newHeight = 90 * newScale;
var newTrunkHeight = 10 * newScale;
var newLeft = newX - newHalfWidth;
var newRight = newX + newHalfWidth;
var newTop = newY;
var newBottom = newY + newHeight + newTrunkHeight;
for (var tree of trees) {
var halfWidth = 22.5 * tree.scale;
var treeHeight = 90 * tree.scale;
var trunkHeight = 10 * tree.scale;
var treeLeft = tree.x - halfWidth;
var treeRight = tree.x + halfWidth;
var treeTop = tree.y;
var treeBottom = tree.y + treeHeight + trunkHeight;
if (newLeft < treeRight &&
newRight > treeLeft &&
newTop < treeBottom &&
newBottom > treeTop) {
return true;
}
}
return false;
}
function isRockOverlapping(newX, newY, newScale) {
var newHalfWidth = 15 * newScale;
var newHeight = 15 * newScale;
var newLeft = newX - newHalfWidth;
var newRight = newX + newHalfWidth;
var newTop = newY;
var newBottom = newY + newHeight;
for (var rock of rocks) {
var halfWidth = 15 * rock.scale;
var rockHeight = 15 * rock.scale;
var rockLeft = rock.x - halfWidth;
var rockRight = rock.x + halfWidth;
var rockTop = rock.y;
var rockBottom = rock.y + rockHeight;
if (newLeft < rockRight &&
newRight > rockLeft &&
newTop < rockBottom &&
newBottom > rockTop) {
return true;
}
}
return false;
}
function isTreeOverlappingGate(newX, newY, newScale) {
var newHalfWidth = 22.5 * newScale;
var newHeight = 90 * newScale;
var newTrunkHeight = 10 * newScale;
var newLeft = newX - newHalfWidth;
var newRight = newX + newHalfWidth;
var newTop = newY;
var newBottom = newY + newHeight + newTrunkHeight;
for (var gate of gates) {
var gateHalfWidth = gate.isFinal ? 150 : gate.halfWidth;
var gateHeight = gate.isFinal ? 60 : 30;
var gateLeft = gate.x - gateHalfWidth;
var gateRight = gate.x + gateHalfWidth;
var gateTop = gate.y;
var gateBottom = gate.y + gateHeight;
if (newLeft < gateRight &&
newRight > gateLeft &&
newTop < gateBottom &&
newBottom > gateTop) {
return true;
}
}
return false;
}
function isRockOverlappingGate(newX, newY, newScale) {
var newHalfWidth = 15 * newScale;
var newHeight = 15 * newScale;
var newLeft = newX - newHalfWidth;
var newRight = newX + newHalfWidth;
var newTop = newY;
var newBottom = newY + newHeight;
for (var gate of gates) {
var gateHalfWidth = gate.isFinal ? 150 : gate.halfWidth;
var gateHeight = gate.isFinal ? 60 : 30;
var gateLeft = gate.x - gateHalfWidth;
var gateRight = gate.x + gateHalfWidth;
var gateTop = gate.y;
var gateBottom = gate.y + gateHeight;
if (newLeft < gateRight &&
newRight > gateLeft &&
newTop < gateBottom &&
newBottom > gateTop) {
return true;
}
}
return false;
}
function isGateOverlappingTree(newX, newY, halfWidth, isFinal) {
var gateHalfWidth = halfWidth;
var gateHeight = isFinal ? 60 : 30;
var gateLeft = newX - gateHalfWidth;
var gateRight = newX + gateHalfWidth;
var gateTop = newY;
var gateBottom = newY + gateHeight;
for (var tree of trees) {
var halfWidth = 22.5 * tree.scale;
var treeHeight = 90 * tree.scale;
var trunkWidth = 10 * tree.scale;
var trunkHeight = 10 * tree.scale;
var treeLeft = tree.x - halfWidth;
var treeRight = tree.x + halfWidth;
var treeTop = tree.y;
var treeBottom = tree.y + treeHeight + trunkHeight;
if (gateLeft < treeRight &&
gateRight > treeLeft &&
gateTop < treeBottom &&
gateBottom > treeTop) {
return true;
}
}
return false;
}
function isGateOverlappingRock(newX, newY, halfWidth, isFinal) {
var gateHalfWidth = halfWidth;
var gateHeight = isFinal ? 60 : 30;
var gateLeft = newX - gateHalfWidth;
var gateRight = newX + gateHalfWidth;
var gateTop = newY;
var gateBottom = newY + gateHeight;
for (var rock of rocks) {
var halfWidth = 15 * rock.scale;
var rockHeight = 15 * rock.scale;
var rockLeft = rock.x - halfWidth;
var rockRight = rock.x + halfWidth;
var rockTop = rock.y;
var rockBottom = rock.y + rockHeight;
if (gateLeft < rockRight &&
gateRight > rockLeft &&
gateTop < rockBottom &&
gateBottom > rockTop) {
return true;
}
}
return false;
}
function isGateOverlappingVertically(newY, isFinal) {
for (var gate of gates) {
if (!isFinal && gate.isFinal) continue;
if (Math.abs(newY - gate.y) < 180) {
return true;
}
}
return false;
}
function isGateTooFarHorizontally(newX, newY) {
if (gates.length > 0) {
var lastGate = gates\[gates.length - 1\];
if (lastGate.isFinal) return false;
var horizontalDist = Math.abs(newX - lastGate.x);
var verticalDist = Math.abs(newY - lastGate.y);
if (horizontalDist > verticalDist) {
return true;
}
}
return false;
}
function addRowOfTrees(yPosition) {
const numTrees = Math.floor(width / 100);
for (let i = 0; i < numTrees; i++) {
let scale = 0.5 + Math.random();
let x = (i + 0.5) * width / numTrees + (Math.random() - 0.5) * 50;
let lightness = Math.floor(Math.random() * 20 + 40);
let hue = Math.random() * 60;
let sat = 50 + Math.random() * 50;
let light = 20 + Math.random() * 20;
trees.push({
x: x,
y: yPosition,
color: `hsl(120, 100\%, \${lightness}\%)`,
trunkColor: `hsl(\${hue}, \${sat}\%, \${light}\%)`,
scale: scale
});
}
}
function isCollidingWithTree() {
var skiDistance = 10 * Math.cos(angle);
var skiWidth = 10;
var skiLength = 30;
var halfSkiDistance = skiDistance / 2;
var leftSkiLeft = midX - halfSkiDistance - skiWidth / 2;
var leftSkiRight = midX - halfSkiDistance + skiWidth / 2;
var leftSkiTop = midY - skiLength / 2;
var leftSkiBottom = midY + skiLength / 2;
var rightSkiLeft = midX + halfSkiDistance - skiWidth / 2;
var rightSkiRight = midX + halfSkiDistance + skiWidth / 2;
var rightSkiTop = midY - skiLength / 2;
var rightSkiBottom = midY + skiLength / 2;
for (var tree of trees) {
var halfWidth = 22.5 * tree.scale;
var treeHeight = 90 * tree.scale;
var trunkWidth = 10 * tree.scale;
var trunkHeight = 10 * tree.scale;
var collisionTop = tree.y + treeHeight;
var collisionBottom = tree.y + treeHeight + trunkHeight;
var treeLeft = tree.x - (trunkWidth / 2);
var treeRight = tree.x + (trunkWidth / 2);
var leftSkiColliding = leftSkiLeft < treeRight &&
leftSkiRight > treeLeft &&
leftSkiTop < collisionBottom &&
leftSkiBottom > collisionTop;
var rightSkiColliding = rightSkiLeft < treeRight &&
rightSkiRight > treeLeft &&
rightSkiTop < collisionBottom &&
rightSkiBottom > collisionTop;
if ((leftSkiColliding || rightSkiColliding) && tree !== lastHitTree) {
lastHitTree = tree;
return true;
}
}
if (lastHitTree) {
var trunkWidth = 10 * lastHitTree.scale;
var trunkHeight = 10 * lastHitTree.scale;
var treeHeight = 90 * lastHitTree.scale;
var collisionTop = lastHitTree.y + treeHeight;
var collisionBottom = lastHitTree.y + treeHeight + trunkHeight;
var treeLeft = lastHitTree.x - (trunkWidth / 2);
var treeRight = lastHitTree.x + (trunkWidth / 2);
var leftSkiColliding = leftSkiLeft < treeRight &&
leftSkiRight > treeLeft &&
leftSkiTop < collisionBottom &&
leftSkiBottom > collisionTop;
var rightSkiColliding = rightSkiLeft < treeRight &&
rightSkiRight > treeLeft &&
rightSkiTop < collisionBottom &&
rightSkiBottom > collisionTop;
if (!(leftSkiColliding || rightSkiColliding)) {
lastHitTree = null;
}
}
return false;
}
function isCollidingWithRock() {
var skiDistance = 10 * Math.cos(angle);
var skiWidth = 10;
var skiLength = 30;
var halfSkiDistance = skiDistance / 2;
var leftSkiLeft = midX - halfSkiDistance - skiWidth / 2;
var leftSkiRight = midX - halfSkiDistance + skiWidth / 2;
var leftSkiTop = midY - skiLength / 2;
var leftSkiBottom = midY + skiLength / 2;
var rightSkiLeft = midX + halfSkiDistance - skiWidth / 2;
var rightSkiRight = midX + halfSkiDistance + skiWidth / 2;
var rightSkiTop = midY - skiLength / 2;
var rightSkiBottom = midY + skiLength / 2;
for (var rock of rocks) {
var rockHeight = 15 * rock.scale;
var collHalfWidth = 15 * rock.scale + 3;
var rockLeft = rock.x - collHalfWidth;
var rockRight = rock.x + collHalfWidth;
var rockTop = rock.y + rockHeight - 2;
var rockBottom = rock.y + rockHeight;
var leftSkiColliding = leftSkiLeft < rockRight &&
leftSkiRight > rockLeft &&
leftSkiTop < rockBottom &&
leftSkiBottom > rockTop;
var rightSkiColliding = rightSkiLeft < rockRight &&
rightSkiRight > rockLeft &&
rightSkiTop < rockBottom &&
rightSkiBottom > rockTop;
if ((leftSkiColliding || rightSkiColliding) && rock !== lastHitRock) {
lastHitRock = rock;
return true;
}
}
if (lastHitRock) {
var rockHeight = 15 * lastHitRock.scale;
var collHalfWidth = 15 * lastHitRock.scale + 3;
var rockLeft = lastHitRock.x - collHalfWidth;
var rockRight = lastHitRock.x + collHalfWidth;
var rockTop = lastHitRock.y + rockHeight - 2;
var rockBottom = lastHitRock.y + rockHeight;
var leftSkiColliding = leftSkiLeft < rockRight &&
leftSkiRight > rockLeft &&
leftSkiTop < rockBottom &&
leftSkiBottom > rockTop;
var rightSkiColliding = rightSkiLeft < rockRight &&
rightSkiRight > rockLeft &&
rightSkiTop < rockBottom &&
rightSkiBottom > rockTop;
if (!(leftSkiColliding || rightSkiColliding)) {
lastHitRock = null;
}
}
return false;
}
function isCollidingWithGateSocket() {
var skiDistance = 10 * Math.cos(angle);
var skiWidth = 10;
var skiLength = 30;
var halfSkiDistance = skiDistance / 2;
var leftSkiLeft = midX - halfSkiDistance - skiWidth / 3;
var leftSkiRight = midX - halfSkiDistance + skiWidth / 3;
var leftSkiTop = midY - skiLength / 3;
var leftSkiBottom = midY + skiLength / 3;
var rightSkiLeft = midX + halfSkiDistance - skiWidth / 3;
var rightSkiRight = midX + halfSkiDistance + skiWidth / 3;
var rightSkiTop = midY - skiLength / 3;
var rightSkiBottom = midY + skiLength / 3;
for (var gate of gates) {
var socketSize = 8;
var gateHalfWidth = gate.isFinal ? 150 : gate.halfWidth;
var poleHeight = gate.isFinal ? 60 : 30;
var poleDistance = gateHalfWidth;
var leftSocket1Left = gate.x - poleDistance - 7.5 - 1.5;
var leftSocket1Right = gate.x - poleDistance - 7.5 + 1.5;
var leftSocket1Top = gate.y + poleHeight - 4;
var leftSocket1Bottom = gate.y + poleHeight + 4;
var leftSocket2Left = gate.x - poleDistance + 7.5 - 1.5;
var leftSocket2Right = gate.x - poleDistance + 7.5 + 1.5;
var leftSocket2Top = gate.y + poleHeight - 4;
var leftSocket2Bottom = gate.y + poleHeight + 4;
var rightSocket1Left = gate.x + poleDistance - 7.5 - 1.5;
var rightSocket1Right = gate.x + poleDistance - 7.5 + 1.5;
var rightSocket1Top = gate.y + poleHeight - 4;
var rightSocket1Bottom = gate.y + poleHeight + 4;
var rightSocket2Left = gate.x + poleDistance + 7.5 - 1.5;
var rightSocket2Right = gate.x + poleDistance + 7.5 + 1.5;
var rightSocket2Top = gate.y + poleHeight - 4;
var rightSocket2Bottom = gate.y + poleHeight + 4;
var leftSkiSocketCollision = (
(leftSkiLeft < leftSocket1Right && leftSkiRight > leftSocket1Left &&
leftSkiTop < leftSocket1Bottom && leftSkiBottom > leftSocket1Top) ||
(leftSkiLeft < leftSocket2Right && leftSkiRight > leftSocket2Left &&
leftSkiTop < leftSocket2Bottom && leftSkiBottom > leftSocket2Top) ||
(leftSkiLeft < rightSocket1Right && leftSkiRight > rightSocket1Left &&
leftSkiTop < rightSocket1Bottom && leftSkiBottom > rightSocket1Top) ||
(leftSkiLeft < rightSocket2Right && leftSkiRight > rightSocket2Left &&
leftSkiTop < rightSocket2Bottom && leftSkiBottom > rightSocket2Top)
);
var rightSkiSocketCollision = (
(rightSkiLeft < leftSocket1Right && rightSkiRight > leftSocket1Left &&
rightSkiTop < leftSocket1Bottom && rightSkiBottom > rightSocket1Top) ||
(rightSkiLeft < leftSocket2Right && rightSkiRight > leftSocket2Left &&
rightSkiTop < leftSocket2Bottom && rightSkiBottom > rightSocket2Top) ||
(rightSkiLeft < rightSocket1Right && rightSkiRight > rightSocket1Left &&
rightSkiTop < rightSocket1Bottom && rightSkiBottom > rightSocket1Top) ||
(rightSkiLeft < rightSocket2Right && rightSkiRight > rightSocket2Left &&
rightSkiTop < rightSocket2Bottom && rightSkiBottom > rightSocket2Top)
);
if (leftSkiSocketCollision || rightSkiSocketCollision) {
return true;
}
}
return false;
}
function update(deltaTime) {
var factor = deltaTime * 60;
if (factor > 6) factor = 6; // Cap wild deltas (e.g., tab switch)
if (keys\["ArrowLeft"\] && !timerStopped) {
angle += 0.0253125 * factor;
moving = true;
}
if (keys\["ArrowRight"\] && !timerStopped) {
angle -= 0.0253125 * factor;
moving = true;
}
angle = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, angle));
var rotationFactor = Math.cos(angle);
var targetSpeed = maxSpeed * rotationFactor;
if (Math.abs(angle) > (Math.PI / 2 - 0.01)) {
targetSpeed = 0;
}
currentSpeed += (targetSpeed - currentSpeed) * 0.005 * factor;
if (moving && startTime === null) {
startTime = Date.now();
lastDeductTime = startTime;
}
if (!moving || timerStopped) return;
if (startTime !== null && !timerStopped) {
let now = Date.now();
let secondsPassed = Math.floor((now - lastDeductTime) / 1000);
if (secondsPassed > 0) {
score = Math.max(0, score - 50 * secondsPassed);
lastDeductTime += secondsPassed * 1000;
}
}
if (isCollidingWithTree() || isCollidingWithRock() || isCollidingWithGateSocket()) {
collisionFlash = 10;
currentSpeed *= 0.1;
score = Math.max(0, score - 500);
for (var tree of trees) {
tree.y -= 10;
}
for (var rock of rocks) {
rock.y -= 10;
}
for (var gate of gates) {
gate.y -= 10;
}
worldY += 10;
console.log("Collision, worldY:", worldY);
return;
}
var vy_per_frame = -currentSpeed;
var vx_per_frame = currentSpeed * Math.sin(angle);
if (Math.abs(angle) > Math.PI / 4) {
var t = (Math.abs(angle) - Math.PI / 4) / (Math.PI / 2 - Math.PI / 4);
var multiplier = 1 + 0.5 * t;
vx_per_frame *= multiplier;
}
var dy = vy_per_frame * factor;
var dx = vx_per_frame * factor;
worldY += Math.abs(dy);
if (worldY < worldHeight) {
if (Math.random() < treeProbability * factor) {
let scale = 0.5 + Math.random();
let treeHeight = 90 * scale;
let lightness = Math.floor(Math.random() * 20 + 40);
let hue = Math.random() * 60;
let sat = 50 + Math.random() * 50;
let light = 20 + Math.random() * 20;
let x = Math.random() * width;
let y = height + treeHeight;
if (!isTreeOverlapping(x, y, scale) && !isTreeOverlappingGate(x, y, scale) && !isRockOverlapping(x, y, scale)) {
trees.push({
x: x,
y: y,
color: `hsl(120, 100\%, \${lightness}\%)`,
trunkColor: `hsl(\${hue}, \${sat}\%, \${light}\%)`,
scale: scale
});
}
}
if (Math.random() < rockProbability * factor) {
let scale = 0.5 + Math.random();
let rockHeight = 15 * scale;
let mainLightness = Math.floor(Math.random() * 40 + 30);
let smallLightness = mainLightness + (Math.random() < 0.5 ? -30 : 30);
smallLightness = Math.max(30, Math.min(70, smallLightness));
let x = Math.random() * width;
let y = height + rockHeight;
let offsetDir = Math.random() < 0.5 ? -1 : 1;
if (!isRockOverlapping(x, y, scale) && !isRockOverlappingGate(x, y, scale) && !isTreeOverlapping(x, y, scale)) {
rocks.push({
x: x,
y: y,
color: `hsl(0, 0\%, \${mainLightness}\%)`,
smallColor: `hsl(0, 0\%, \${smallLightness}\%)`,
scale: scale,
offsetDir: offsetDir
});
}
}
if (Math.random() < gateProbability * factor) {
let attempts = 0;
const maxAttempts = 10;
let new_x, new_y, halfWidth;
do {
let fullWidth = minGateWidth + Math.random() * (maxGateWidth - minGateWidth);
halfWidth = fullWidth / 2;
new_x = halfWidth + Math.random() * (width - 2 * halfWidth);
new_y = height + 30;
attempts++;
} while ((isGateOverlappingVertically(new_y, false) || // disable_whilecounter_protection
isGateOverlappingTree(new_x, new_y, halfWidth, false) ||
isGateOverlappingRock(new_x, new_y, halfWidth, false) ||
isGateTooFarHorizontally(new_x, new_y)) &&
attempts < maxAttempts);
if (attempts < maxAttempts) {
gates.push({
x: new_x,
y: new_y,
passed: false,
halfWidth: halfWidth
});
console.log("Added regular gate at worldY:", worldY, "y:", new_y);
}
}
}
if (worldY >= worldHeight && !finalGateAdded) {
let attempts = 0;
const maxAttempts = 10;
let new_x, new_y;
let lastBlueGateY = null;
for (let i = gates.length - 1; i >= 0; i--) {
if (!gates\[i\].isFinal) {
lastBlueGateY = gates\[i\].y;
break;
}
}
let halfWidth = 150;
do {
new_x = halfWidth + Math.random() * (width - 2 * halfWidth);
new_y = lastBlueGateY !== null ? Math.max(height + 30, lastBlueGateY + 200) : height + 30;
attempts++;
} while ((isGateOverlappingVertically(new_y, true) || // disable_whilecounter_protection
isGateOverlappingTree(new_x, new_y, halfWidth, true) ||
isGateOverlappingRock(new_x, new_y, halfWidth, true)) &&
attempts < maxAttempts);
if (attempts >= maxAttempts) {
do {
new_x = halfWidth + Math.random() * (width - 2 * halfWidth);
new_y = lastBlueGateY !== null ? Math.max(height + 30, lastBlueGateY + 200) : height + 30;
} while (isGateOverlappingTree(new_x, new_y, halfWidth, true) || isGateOverlappingRock(new_x, new_y, halfWidth, true)); // disable_whilecounter_protection
}
gates.push({
x: new_x,
y: new_y,
passed: false,
isFinal: true
});
finalGateAdded = true;
finalGateY = worldY + (new_y - midY);
addRowOfTrees(new_y + 200);
endLineY = new_y + 400;
console.log("Added final red gate at worldY:", worldY, "canvas y:", new_y, "finalGateY:", finalGateY);
}
if (finalGateAdded && finalGateY !== null && worldY >= finalGateY + 75) {
if (!timerStopped) {
timerStopped = true;
endTime = Date.now();
}
if (worldY >= finalGateY + 180) {
moving = false;
currentSpeed = 0;
console.log("Stopped all movement at worldY:", worldY, "finalGateY + 460:", finalGateY + 460);
}
return;
}
for (var i = trees.length - 1; i >= 0; i--) {
trees\[i\].x += dx;
trees\[i\].y += dy;
if (trees\[i\].y < - (90 * trees\[i\].scale)) {
trees.splice(i, 1);
}
}
for (var i = rocks.length - 1; i >= 0; i--) {
rocks\[i\].x += dx;
rocks\[i\].y += dy;
if (rocks\[i\].y < - (15 * rocks\[i\].scale)) {
rocks.splice(i, 1);
}
}
for (var i = gates.length - 1; i >= 0; i--) {
gates\[i\].x += dx;
gates\[i\].y += dy;
if (gates\[i\].y < midY + 15 && gates\[i\].y > midY - 15) {
var gateHalfWidth = gates\[i\].isFinal ? 150 : gates\[i\].halfWidth;
if (midX > gates\[i\].x - gateHalfWidth && midX < gates\[i\].x + gateHalfWidth && !gates\[i\].passed) {
score += gates\[i\].isFinal ? 2000 : 1000;
score = Math.max(0, score);
gates\[i\].passed = true;
console.log("Passed gate at worldY:", worldY, "isFinal:", gates\[i\].isFinal);
}
} else if (gates\[i\].y < midY - 15 && !gates\[i\].passed) {
score = Math.max(0, score - (gates\[i\].isFinal ? 1000 : 250));
gates\[i\].passed = true;
console.log("Missed gate at worldY:", worldY, "isFinal:", gates\[i\].isFinal);
}
if (gates\[i\].y < - (gates\[i\].isFinal ? 60 : 30) && (!gates\[i\].isFinal || worldY >= (finalGateY || worldHeight) + 460)) {
console.log("Removing gate at worldY:", worldY, "isFinal:", gates\[i\].isFinal, "y:", gates\[i\].y);
gates.splice(i, 1);
}
}
}
function draw() {
ctx.fillStyle = collisionFlash > 0 ? "rgba(255, 0, 0, 0.3)" : "white";
ctx.fillRect(0, 0, width, height);
if (collisionFlash > 0) collisionFlash--;
for (var rock of rocks) {
var halfWidth = 15 * rock.scale;
var rockHeight = 15 * rock.scale;
var smallerScale = rock.scale * 0.8;
var smallerHalfWidth = 15 * smallerScale;
var smallerHeight = 15 * smallerScale;
var offsetX = rock.offsetDir * 5 * rock.scale;
var offsetY = 7;
ctx.fillStyle = rock.color;
ctx.beginPath();
ctx.moveTo(rock.x - halfWidth - 5, rock.y + rockHeight);
ctx.lineTo(rock.x, rock.y - 3);
ctx.lineTo(rock.x + halfWidth + 5, rock.y + rockHeight);
ctx.closePath();
ctx.fill();
ctx.fillStyle = rock.smallColor;
ctx.beginPath();
ctx.moveTo(rock.x + offsetX - smallerHalfWidth - 3, rock.y + offsetY + smallerHeight);
ctx.lineTo(rock.x + offsetX, rock.y + offsetY);
ctx.lineTo(rock.x + offsetX + smallerHalfWidth + 3, rock.y + offsetY + smallerHeight);
ctx.closePath();
ctx.fill();
}
ctx.save();
ctx.translate(midX, midY);
ctx.strokeStyle = "red";
ctx.lineWidth = 3;
let skiLength = 30;
let skiHalfLength = skiLength / 2;
let skiDistance = 10 * Math.cos(angle);
let halfSkiDistance = skiDistance / 2;
ctx.save();
ctx.translate(-halfSkiDistance, 0);
ctx.rotate(angle);
ctx.beginPath();
ctx.moveTo(0, -skiHalfLength);
ctx.lineTo(0, skiHalfLength);
ctx.stroke();
ctx.restore();
ctx.save();
ctx.translate(halfSkiDistance, 0);
ctx.rotate(angle);
ctx.beginPath();
ctx.moveTo(0, -skiHalfLength);
ctx.lineTo(0, skiHalfLength);
ctx.stroke();
ctx.restore();
ctx.restore();
ctx.save();
ctx.strokeStyle = "black";
ctx.lineWidth = 3;
let legDistance = skiDistance;
let halfLegDistance = legDistance / 2;
ctx.beginPath();
ctx.moveTo(midX - halfLegDistance, midY - 5);
ctx.lineTo(midX - halfLegDistance, midY);
ctx.moveTo(midX + halfLegDistance, midY - 5);
ctx.lineTo(midX + halfLegDistance, midY);
ctx.stroke();
ctx.fillStyle = "black";
ctx.fillRect(midX - 6, midY - 20, 12, 15); // Wider by 1px on each side (10 -> 12)
ctx.strokeStyle = "black";
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(midX - 6, midY - 20);
ctx.lineTo(midX - 15, midY - 20);
ctx.moveTo(midX + 6, midY - 20);
ctx.lineTo(midX + 15, midY - 20);
ctx.stroke();
ctx.strokeStyle = "#C0C0C0";
ctx.lineWidth = 1;
ctx.beginPath();
ctx.save();
ctx.translate(midX + 15, midY - 20);
ctx.rotate(keys\["ArrowLeft"\] ? -angle : 0);
ctx.moveTo(0, 0);
ctx.lineTo(0, 16.5);
ctx.stroke();
ctx.restore();
ctx.save();
ctx.translate(midX - 15, midY - 20);
ctx.rotate(keys\["ArrowRight"\] ? angle : 0);
ctx.moveTo(0, 0);
ctx.lineTo(0, 16.5);
ctx.stroke();
ctx.restore();
ctx.fillStyle = "#e0ac69";
ctx.beginPath();
ctx.arc(midX, midY - 25, 6, 0, 2 * Math.PI);
ctx.fill();
ctx.strokeStyle = "black";
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(midX - 2, midY - 25 - 1);
ctx.lineTo(midX - 2, midY - 25 + 1);
ctx.moveTo(midX + 2, midY - 25 - 1);
ctx.lineTo(midX + 2, midY - 25 + 1);
ctx.moveTo(midX - 2, midY - 25 + 3);
ctx.lineTo(midX + 2, midY - 25 + 3);
ctx.stroke();
ctx.restore();
ctx.fillStyle = "black";
ctx.fillRect(midX - 8, midY - 33, 16, 2);
ctx.fillRect(midX - 6, midY - 31, 12, 2);
ctx.strokeStyle = "#808080";
ctx.lineWidth = 3;
for (var gate of gates) {
var gateHalfWidth = gate.isFinal ? 150 : gate.halfWidth;
var gateHeight = gate.isFinal ? 60 : 30;
var poleDistance = gateHalfWidth;
var rectangleHeight = gate.isFinal ? 20 : 10;
ctx.beginPath();
ctx.moveTo(gate.x - poleDistance - 7.5, gate.y);
ctx.lineTo(gate.x - poleDistance - 7.5, gate.y + gateHeight);
ctx.moveTo(gate.x - poleDistance + 7.5, gate.y);
ctx.lineTo(gate.x - poleDistance + 7.5, gate.y + gateHeight);
ctx.moveTo(gate.x + poleDistance - 7.5, gate.y);
ctx.lineTo(gate.x + poleDistance - 7.5, gate.y + gateHeight);
ctx.moveTo(gate.x + poleDistance + 7.5, gate.y);
ctx.lineTo(gate.x + poleDistance + 7.5, gate.y + gateHeight);
ctx.stroke();
ctx.fillStyle = gate.isFinal ? "#FF0000" : "#0000FF";
ctx.fillRect(gate.x - poleDistance - 7.5, gate.y, 15, rectangleHeight);
ctx.fillRect(gate.x + poleDistance - 7.5, gate.y, 15, rectangleHeight);
if (gate.isFinal) {
ctx.fillStyle = "#FF0000";
ctx.fillRect(gate.x - poleDistance + 7.5, gate.y, poleDistance * 2 - 15, rectangleHeight);
ctx.fillStyle = "white";
ctx.font = "bold 20px Arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(finishText, gate.x, gate.y + rectangleHeight / 2);
ctx.textAlign = "left";
ctx.textBaseline = "alphabetic";
}
ctx.fillStyle = "#404040";
ctx.fillRect(gate.x - poleDistance - 7.5 - 1.5, gate.y + gateHeight - 4, 3, 8);
ctx.fillRect(gate.x - poleDistance + 7.5 - 1.5, gate.y + gateHeight - 4, 3, 8);
ctx.fillRect(gate.x + poleDistance - 7.5 - 1.5, gate.y + gateHeight - 4, 3, 8);
ctx.fillRect(gate.x + poleDistance + 7.5 - 1.5, gate.y + gateHeight - 4, 3, 8);
}
for (var tree of trees) {
var halfWidth = 22.5 * tree.scale;
var treeHeight = 90 * tree.scale;
var trunkWidth = 10 * tree.scale;
var trunkHeight = 10 * tree.scale;
ctx.fillStyle = tree.color;
ctx.beginPath();
ctx.moveTo(tree.x - halfWidth, tree.y + treeHeight);
ctx.lineTo(tree.x, tree.y);
ctx.lineTo(tree.x + halfWidth, tree.y + treeHeight);
ctx.closePath();
ctx.fill();
ctx.fillStyle = tree.trunkColor;
ctx.fillRect(tree.x - (trunkWidth / 2), tree.y + treeHeight, trunkWidth, trunkHeight);
}
if (endLineY !== null) {
ctx.fillStyle = "#FF0000";
ctx.fillRect(0, endLineY - worldY + midY, width, 10);
}
var displayTime = timerStopped ? (endTime - startTime) : (startTime !== null ? Date.now() - startTime : 0);
var minutes = Math.floor(displayTime / 60000);
var seconds = Math.floor((displayTime \% 60000) / 1000);
ctx.fillStyle = "black";
ctx.fillRect(0, 0, 120, 50);
ctx.fillStyle = "red";
ctx.font = "40px Arial";
ctx.fillText(minutes + ":" + (seconds < 10 ? "0" : "") + seconds, 30, 40);
ctx.fillStyle = "black";
ctx.fillRect(width - 150, 0, 150, 50);
ctx.fillStyle = "red";
ctx.font = "40px Arial";
ctx.textAlign = "right";
ctx.fillText(score, width - 20, 40);
ctx.textAlign = "left";
if (timerStopped) {
correct_next();
}
}
function gameLoop(timeStamp) {
var deltaTime = lastTime ? (timeStamp - lastTime) / 1000 : 0;
lastTime = timeStamp || performance.now();
update(deltaTime);
draw();
requestAnimationFrame(gameLoop);
}
requestAnimationFrame(gameLoop);
</script>



~;

}

1;

__END__