<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<title>猩球大战</title>
<style>
@import url("https://fonts.googleapis.com/css2?family=Inconsolata:wght@400;700&display=swap");
body {
margin: 0;
padding: 0;
font-family: "Inconsolata", monospace;
font-size: 14px;
color: white;
user-select: none;
-webkit-user-select: none;
display: flex;
justify-content: center;
align-items: center;
height: 100%;
overflow: hidden;
}
button {
cursor: pointer;
border: none;
color: white;
background: transparent;
font-family: "Inconsolata", monospace;
padding: 10px;
font-size: 1em;
}
button:hover {
background-color: rgba(255, 255, 255, 0.1);
}
#info-left,
#info-right {
position: absolute;
top: 20px;
}
#info-left {
left: 25px;
}
#info-right {
right: 25px;
text-align: right;
}
#bomb-grab-area {
position: absolute;
width: 30px;
height: 30px;
border-radius: 50%;
background-color: transparent;
cursor: grab;
}
#instructions,
#congratulations {
position: absolute;
transition: visibility 0s, opacity 0.5s linear;
}
@media (min-height: 535px) {
#instructions {
min-height: 200px;
}
}
#congratulations {
background-color: rgba(255, 255, 255, 0.9);
color: black;
padding: 50px 80px;
opacity: 0;
visibility: hidden;
max-width: 300px;
backdrop-filter: blur(5px);
}
#congratulations p a {
color: inherit;
}
#congratulations button {
border: 1px solid rgba(0, 0, 0, 0.9);
color: inherit;
}
#settings {
position: absolute;
top: calc(20px + 16.385px - 10px);
display: flex;
align-items: center;
gap: 10px;
right: 11em;
}
#settings,
#info-left,
#info-right {
opacity: 0;
transition: opacity 3s;
}
@media (max-width: 450px) {
#settings,
#info-left,
#info-right {
opacity: 0;
}
#instructions {
visibility: hidden;
}
}
.dropdown {
position: relative;
display: inline-block;
}
.dropbtn:after {
content: "▼";
margin-left: 7px;
font-size: 0.8em;
vertical-align: text-top;
}
.dropdown-content {
display: none;
position: absolute;
background-color: #f9f9f9;
min-width: 120px;
box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2);
}
.dropdown-content a {
color: black;
padding: 12px 16px;
text-decoration: none;
display: block;
white-space: nowrap;
font-size: 0.9em;
}
.dropdown-content a:hover {
background-color: #f1f1f1;
}
.dropdown:hover .dropdown-content {
display: block;
}
#windmill {
position: absolute;
right: 0;
fill: rgba(255, 255, 255, 0.5);
transform-origin: bottom;
}
#windmill-head {
animation-name: rotate;
animation-duration: 4s;
animation-iteration-count: infinite;
animation-timing-function: linear;
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
#wind-info {
position: absolute;
width: 100px;
text-align: center;
margin-bottom: 30px;
}
#youtube {
z-index: 2;
display: block;
width: 30px;
height: 21px;
background: red;
position: relative;
border-radius: 50% / 11%;
transition: transform 0.5s;
color: black;
}
#youtube:hover,
#youtube:focus {
transform: scale(1.2);
}
#youtube::before {
content: "";
display: block;
position: absolute;
top: 7.5%;
left: -6%;
width: 112%;
height: 85%;
background: red;
border-radius: 9% / 50%;
}
#youtube::after {
content: "";
display: block;
position: absolute;
top: 6px;
left: 11px;
width: 15px;
height: 10px;
border: 5px solid transparent;
box-sizing: border-box;
border-left: 10px solid white;
}
#youtube span {
font-size: 0;
position: absolute;
width: 0;
height: 0;
overflow: hidden;
}
#youtube-card {
display: none;
}
#youtube:hover + #youtube-card {
color: black;
display: block;
position: absolute;
top: -20px;
right: -20px;
padding: 25px 60px 25px 25px;
width: 200px;
background-color: white;
}
#fullscreen {
all: unset;
cursor: pointer;
position: absolute;
right: 10px;
bottom: 10px;
}
</style>
</head>
<body>
<canvas id="game"></canvas>
<svg width="200" height="250" id="windmill">
<defs>
<path id="arm" d="M -7 -20 C -7 -10 7 -10 7 -20 L 2 -80 L -2 -80" />
</defs>
<g transform="translate(100, 100)">
<g id="windmill-head">
<circle r="8"></circle>
<use href="#arm" />
<use href="#arm" transform="rotate(+120)" />
<use href="#arm" transform="rotate(-120)" />
</g>
</g>
<path
transform="translate(100, 0)"
d="M -7 250 L 7 250 L 3 115 L -3 115"
></path>
</svg>
<div id="wind-info">风速: <span id="wind-speed">0</span></div>
<div id="info-left">
<h3><span class="name">玩家</span></h3>
<p>角度: <span class="angle">0</span>°</p>
<p>速度: <span class="velocity">0</span></p>
</div>
<div id="info-right">
<h3><span class="name">电脑</span></h3>
<p>角度: <span class="angle">0</span>°</p>
<p>速度: <span class="velocity">0</span></p>
</div>
<div id="instructions">
<h3 id="game-mode">玩家 vs. 电脑</h3>
<h1>拖动炸弹瞄准!</h1>
</div>
<div id="bomb-grab-area"></div>
<div id="congratulations">
<h1><span id="winner">?</span> 赢了!</h1>
<div class="dropdown">
<button class="dropbtn">新游戏</button>
<div class="dropdown-content">
<a href="#" class="single-player">单人模式</a>
<a href="#" class="two-players">双人模式</a>
<a href="#" class="auto-play">自动播放</a>
</div>
</div>
</div>
<div id="settings">
<div class="dropdown">
<button class="dropbtn">新游戏</button>
<div class="dropdown-content">
<a href="#" class="single-player">单人模式</a>
<a href="#" class="two-players">双人模式</a>
<a href="#" class="auto-play">自动播放</a>
</div>
</div>
<button id="color-mode">暗色模式</button>
</div>
<button id="fullscreen" onclick="toggleFullscreen()">
<svg width="30" height="30">
<path
id="enter-fullscreen"
stroke="white"
stroke-width="3"
fill="none"
d="
M 10, 2 L 2,2 L 2, 10
M 20, 2 L 28,2 L 28, 10
M 28, 20 L 28,28 L 20, 28
M 10, 28 L 2,28 L 2, 20"
/>
<path
id="exit-fullscreen"
stroke="transparent"
stroke-width="3"
fill="none"
d="
M 10, 2 L 10,10 L 2, 10
M 20, 2 L 20,10 L 28, 10
M 28, 20 L 20,20 L 20, 28
M 10, 28 L 10,20 L 2, 20"
/>
</svg>
</button>
<script>
let state = {};
let isDragging = false;
let dragStartX = undefined;
let dragStartY = undefined;
let previousAnimationTimestamp = undefined;
let animationFrameRequestID = undefined;
let delayTimeoutID = undefined;
let simulationMode = false;
let simulationImpact = {};
const darkModeMediaQuery = window.matchMedia(
"(prefers-color-scheme: dark)"
);
const settings = {
numberOfPlayers: 1,
mode: darkModeMediaQuery.matches ? "dark" : "light",
};
const blastHoleRadius = 18;
const canvas = document.getElementById("game");
canvas.width = window.innerWidth * window.devicePixelRatio;
canvas.height = window.innerHeight * window.devicePixelRatio;
canvas.style.width = window.innerWidth + "px";
canvas.style.height = window.innerHeight + "px";
const ctx = canvas.getContext("2d");
const windmillDOM = document.getElementById("windmill");
const windmillHeadDOM = document.getElementById("windmill-head");
const windInfoDOM = document.getElementById("wind-info");
const windSpeedDOM = document.getElementById("wind-speed");
const info1DOM = document.getElementById("info-left");
const name1DOM = document.querySelector("#info-left .name");
const angle1DOM = document.querySelector("#info-left .angle");
const velocity1DOM = document.querySelector("#info-left .velocity");
const info2DOM = document.getElementById("info-right");
const name2DOM = document.querySelector("#info-right .name");
const angle2DOM = document.querySelector("#info-right .angle");
const velocity2DOM = document.querySelector("#info-right .velocity");
const instructionsDOM = document.getElementById("instructions");
const gameModeDOM = document.getElementById("game-mode");
const bombGrabAreaDOM = document.getElementById("bomb-grab-area");
const congratulationsDOM = document.getElementById("congratulations");
const winnerDOM = document.getElementById("winner");
const settingsDOM = document.getElementById("settings");
const singlePlayerButtonDOM = document.querySelectorAll(".single-player");
const twoPlayersButtonDOM = document.querySelectorAll(".two-players");
const autoPlayButtonDOM = document.querySelectorAll(".auto-play");
const colorModeButtonDOM = document.getElementById("color-mode");
colorModeButtonDOM.addEventListener("click", () => {
if (settings.mode === "dark") {
settings.mode = "light";
colorModeButtonDOM.innerText = "暗色模式";
} else {
settings.mode = "dark";
colorModeButtonDOM.innerText = "亮色模式";
}
draw();
});
darkModeMediaQuery.addEventListener("change", (e) => {
settings.mode = e.matches ? "dark" : "light";
if (settings.mode === "dark") {
colorModeButtonDOM.innerText = "亮色模式";
} else {
colorModeButtonDOM.innerText = "暗色模式";
}
draw();
});
newGame();
function newGame() {
state = {
phase: "aiming",
currentPlayer: 1,
round: 1,
windSpeed: generateWindSpeed(),
bomb: {
x: undefined,
y: undefined,
rotation: 0,
velocity: { x: 0, y: 0 },
highlight: true,
},
backgroundBuildings: [],
buildings: [],
blastHoles: [],
stars: [],
scale: 1,
shift: 0,
};
for (
let i = 0;
i < (window.innerWidth * window.innerHeight) / 12000;
i++
) {
const x = Math.floor(Math.random() * window.innerWidth);
const y = Math.floor(Math.random() * window.innerHeight);
state.stars.push({ x, y });
}
for (let i = 0; i < 17; i++) {
generateBackgroundBuilding(i);
}
for (let i = 0; i < 8; i++) {
generateBuilding(i);
}
calculateScaleAndShift();
initializeBombPosition();
initializeWindmillPosition();
setWindMillRotation();
cancelAnimationFrame(animationFrameRequestID);
clearTimeout(delayTimeoutID);
if (settings.numberOfPlayers > 0) {
showInstructions();
} else {
hideInstructions();
}
hideCongratulations();
angle1DOM.innerText = 0;
velocity1DOM.innerText = 0;
angle2DOM.innerText = 0;
velocity2DOM.innerText = 0;
simulationMode = false;
simulationImpact = {};
draw();
if (settings.numberOfPlayers === 0) {
computerThrow();
}
}
function showInstructions() {
singlePlayerButtonDOM.checked = true;
instructionsDOM.style.opacity = 1;
instructionsDOM.style.visibility = "visible";
}
function hideInstructions() {
state.bomb.highlight = false;
instructionsDOM.style.opacity = 0;
instructionsDOM.style.visibility = "hidden";
}
function showCongratulations() {
congratulationsDOM.style.opacity = 1;
congratulationsDOM.style.visibility = "visible";
}
function hideCongratulations() {
congratulationsDOM.style.opacity = 0;
congratulationsDOM.style.visibility = "hidden";
}
function generateBackgroundBuilding(index) {
const previousBuilding = state.backgroundBuildings[index - 1];
const x = previousBuilding
? previousBuilding.x + previousBuilding.width + 4
: -300;
const minWidth = 60;
const maxWidth = 110;
const width = minWidth + Math.random() * (maxWidth - minWidth);
const smallerBuilding = index < 4 || index >= 13;
const minHeight = 80;
const maxHeight = 350;
const smallMinHeight = 20;
const smallMaxHeight = 150;
const height = smallerBuilding
? smallMinHeight + Math.random() * (smallMaxHeight - smallMinHeight)
: minHeight + Math.random() * (maxHeight - minHeight);
state.backgroundBuildings.push({ x, width, height });
}
function generateBuilding(index) {
const previousBuilding = state.buildings[index - 1];
const x = previousBuilding
? previousBuilding.x + previousBuilding.width + 4
: 0;
const minWidth = 80;
const maxWidth = 130;
const width = minWidth + Math.random() * (maxWidth - minWidth);
const smallerBuilding = index <= 1 || index >= 6;
const minHeight = 40;
const maxHeight = 300;
const minHeightGorilla = 30;
const maxHeightGorilla = 150;
const height = smallerBuilding
? minHeightGorilla +
Math.random() * (maxHeightGorilla - minHeightGorilla)
: minHeight + Math.random() * (maxHeight - minHeight);
const lightsOn = [];
for (let i = 0; i < 50; i++) {
const light = Math.random() <= 0.33 ? true : false;
lightsOn.push(light);
}
state.buildings.push({ x, width, height, lightsOn });
}
function calculateScaleAndShift() {
const lastBuilding = state.buildings.at(-1);
const totalWidthOfTheCity = lastBuilding.x + lastBuilding.width;
const horizontalScale = window.innerWidth / totalWidthOfTheCity ?? 1;
const verticalScale = window.innerHeight / 500;
state.scale = Math.min(horizontalScale, verticalScale);
const sceneNeedsToBeShifted = horizontalScale > verticalScale;
state.shift = sceneNeedsToBeShifted
? (window.innerWidth - totalWidthOfTheCity * state.scale) / 2
: 0;
}
window.addEventListener("resize", () => {
canvas.width = window.innerWidth * window.devicePixelRatio;
canvas.height = window.innerHeight * window.devicePixelRatio;
canvas.style.width = window.innerWidth + "px";
canvas.style.height = window.innerHeight + "px";
calculateScaleAndShift();
initializeBombPosition();
initializeWindmillPosition();
draw();
});
function initializeBombPosition() {
const building =
state.currentPlayer === 1
? state.buildings.at(1)
: state.buildings.at(-2);
const gorillaX = building.x + building.width / 2;
const gorillaY = building.height;
const gorillaHandOffsetX = state.currentPlayer === 1 ? -28 : 28;
const gorillaHandOffsetY = 107;
state.bomb.x = gorillaX + gorillaHandOffsetX;
state.bomb.y = gorillaY + gorillaHandOffsetY;
state.bomb.velocity.x = 0;
state.bomb.velocity.y = 0;
state.bomb.rotation = 0;
const grabAreaRadius = 15;
const left = state.bomb.x * state.scale + state.shift - grabAreaRadius;
const bottom = state.bomb.y * state.scale - grabAreaRadius;
bombGrabAreaDOM.style.left = `${left}px`;
bombGrabAreaDOM.style.bottom = `${bottom}px`;
}
function initializeWindmillPosition() {
const lastBuilding = state.buildings.at(-1);
let rooftopY = lastBuilding.height * state.scale;
let rooftopX =
(lastBuilding.x + lastBuilding.width / 2) * state.scale + state.shift;
windmillDOM.style.bottom = `${rooftopY}px`;
windmillDOM.style.left = `${rooftopX - 100}px`;
windmillDOM.style.scale = state.scale;
windInfoDOM.style.bottom = `${rooftopY}px`;
windInfoDOM.style.left = `${rooftopX - 50}px`;
}
function draw() {
ctx.save();
ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
drawBackgroundSky();
ctx.translate(0, window.innerHeight);
ctx.scale(1, -1);
ctx.translate(state.shift, 0);
ctx.scale(state.scale, state.scale);
drawBackgroundMoon();
drawBackgroundBuildings();
drawBuildingsWithBlastHoles();
drawGorilla(1);
drawGorilla(2);
drawBomb();
ctx.restore();
}
function drawBackgroundSky() {
const gradient = ctx.createLinearGradient(0, 0, 0, window.innerHeight);
if (settings.mode === "dark") {
gradient.addColorStop(1, "#27507F");
gradient.addColorStop(0, "#58A8D8");
} else {
gradient.addColorStop(1, "#F8BA85");
gradient.addColorStop(0, "#FFC28E");
}
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, window.innerWidth, window.innerHeight);
if (settings.mode === "dark") {
ctx.fillStyle = "white";
state.stars.forEach((star) => {
ctx.fillRect(star.x, star.y, 1, 1);
});
}
}
function drawBackgroundMoon() {
if (settings.mode === "dark") {
ctx.fillStyle = "rgba(255, 255, 255, 0.6)";
ctx.beginPath();
ctx.arc(
window.innerWidth / state.scale - state.shift - 200,
window.innerHeight / state.scale - 100,
30,
0,
2 * Math.PI
);
ctx.fill();
} else {
ctx.fillStyle = "rgba(255, 255, 255, 0.6)";
ctx.beginPath();
ctx.arc(300, 350, 60, 0, 2 * Math.PI);
ctx.fill();
}
}
function drawBackgroundBuildings() {
state.backgroundBuildings.forEach((building) => {
ctx.fillStyle = settings.mode === "dark" ? "#254D7E" : "#947285";
ctx.fillRect(building.x, 0, building.width, building.height);
});
}
function drawBuildingsWithBlastHoles() {
ctx.save();
state.blastHoles.forEach((blastHole) => {
ctx.beginPath();
ctx.rect(
0,
0,
window.innerWidth / state.scale,
window.innerHeight / state.scale
);
ctx.arc(
blastHole.x,
blastHole.y,
blastHoleRadius,
0,
2 * Math.PI,
true
);
ctx.clip();
});
drawBuildings();
ctx.restore();
}
function drawBuildings() {
state.buildings.forEach((building) => {
ctx.fillStyle = settings.mode === "dark" ? "#152A47" : "#4A3C68";
ctx.fillRect(building.x, 0, building.width, building.height);
const windowWidth = 10;
const windowHeight = 12;
const gap = 15;
const numberOfFloors = Math.ceil(
(building.height - gap) / (windowHeight + gap)
);
const numberOfRoomsPerFloor = Math.floor(
(building.width - gap) / (windowWidth + gap)
);
for (let floor = 0; floor < numberOfFloors; floor++) {
for (let room = 0; room < numberOfRoomsPerFloor; room++) {
if (building.lightsOn[floor * numberOfRoomsPerFloor + room]) {
ctx.save();
ctx.translate(building.x + gap, building.height - gap);
ctx.scale(1, -1);
const x = room * (windowWidth + gap);
const y = floor * (windowHeight + gap);
ctx.fillStyle =
settings.mode === "dark" ? "#5F76AB" : "#EBB6A2";
ctx.fillRect(x, y, windowWidth, windowHeight);
ctx.restore();
}
}
}
});
}
function drawGorilla(player) {
ctx.save();
const building =
player === 1
? state.buildings.at(1)
: state.buildings.at(-2);
ctx.translate(building.x + building.width / 2, building.height);
drawGorillaBody();
drawGorillaLeftArm(player);
drawGorillaRightArm(player);
drawGorillaFace(player);
drawGorillaThoughtBubbles(player);
ctx.restore();
}
function drawGorillaBody() {
ctx.fillStyle = "black";
ctx.beginPath();
ctx.moveTo(0, 15);
ctx.lineTo(-7, 0);
ctx.lineTo(-20, 0);
ctx.lineTo(-17, 18);
ctx.lineTo(-20, 44);
ctx.lineTo(-11, 77);
ctx.lineTo(0, 84);
ctx.lineTo(11, 77);
ctx.lineTo(20, 44);
ctx.lineTo(17, 18);
ctx.lineTo(20, 0);
ctx.lineTo(7, 0);
ctx.fill();
}
function drawGorillaLeftArm(player) {
ctx.strokeStyle = "black";
ctx.lineWidth = 18;
ctx.beginPath();
ctx.moveTo(-14, 50);
if (
state.phase === "aiming" &&
state.currentPlayer === 1 &&
player === 1
) {
ctx.quadraticCurveTo(
-44,
63,
-28 - state.bomb.velocity.x / 6.25,
107 - state.bomb.velocity.y / 6.25
);
} else if (
state.phase === "celebrating" &&
state.currentPlayer === player
) {
ctx.quadraticCurveTo(-44, 63, -28, 107);
} else {
ctx.quadraticCurveTo(-44, 45, -28, 12);
}
ctx.stroke();
}
function drawGorillaRightArm(player) {
ctx.strokeStyle = "black";
ctx.lineWidth = 18;
ctx.beginPath();
ctx.moveTo(+14, 50);
if (
state.phase === "aiming" &&
state.currentPlayer === 2 &&
player === 2
) {
ctx.quadraticCurveTo(
+44,
63,
+28 - state.bomb.velocity.x / 6.25,
107 - state.bomb.velocity.y / 6.25
);
} else if (
state.phase === "celebrating" &&
state.currentPlayer === player
) {
ctx.quadraticCurveTo(+44, 63, +28, 107);
} else {
ctx.quadraticCurveTo(+44, 45, +28, 12);
}
ctx.stroke();
}
function drawGorillaFace(player) {
ctx.fillStyle = settings.mode === "dark" ? "gray" : "lightgray";
ctx.beginPath();
ctx.arc(0, 63, 9, 0, 2 * Math.PI);
ctx.moveTo(-3.5, 70);
ctx.arc(-3.5, 70, 4, 0, 2 * Math.PI);
ctx.moveTo(+3.5, 70);
ctx.arc(+3.5, 70, 4, 0, 2 * Math.PI);
ctx.fill();
ctx.fillStyle = "black";
ctx.beginPath();
ctx.arc(-3.5, 70, 1.4, 0, 2 * Math.PI);
ctx.moveTo(+3.5, 70);
ctx.arc(+3.5, 70, 1.4, 0, 2 * Math.PI);
ctx.fill();
ctx.strokeStyle = "black";
ctx.lineWidth = 1.4;
ctx.beginPath();
ctx.moveTo(-3.5, 66.5);
ctx.lineTo(-1.5, 65);
ctx.moveTo(3.5, 66.5);
ctx.lineTo(1.5, 65);
ctx.stroke();
ctx.beginPath();
if (state.phase === "celebrating" && state.currentPlayer === player) {
ctx.moveTo(-5, 60);
ctx.quadraticCurveTo(0, 56, 5, 60);
} else {
ctx.moveTo(-5, 56);
ctx.quadraticCurveTo(0, 60, 5, 56);
}
ctx.stroke();
}
function drawGorillaThoughtBubbles(player) {
if (state.phase === "aiming") {
const currentPlayerIsComputer =
(settings.numberOfPlayers === 0 &&
state.currentPlayer === 1 &&
player === 1) ||
(settings.numberOfPlayers !== 2 &&
state.currentPlayer === 2 &&
player === 2);
if (currentPlayerIsComputer) {
ctx.save();
ctx.scale(1, -1);
ctx.font = "20px sans-serif";
ctx.textAlign = "center";
ctx.fillText("?", 0, -90);
ctx.font = "10px sans-serif";
ctx.rotate((5 / 180) * Math.PI);
ctx.fillText("?", 0, -90);
ctx.rotate((-10 / 180) * Math.PI);
ctx.fillText("?", 0, -90);
ctx.restore();
}
}
}
function drawBomb() {
ctx.save();
ctx.translate(state.bomb.x, state.bomb.y);
if (state.phase === "aiming") {
ctx.translate(
-state.bomb.velocity.x / 6.25,
-state.bomb.velocity.y / 6.25
);
ctx.strokeStyle = "rgba(255, 255, 255, 0.7)";
ctx.setLineDash([3, 8]);
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(state.bomb.velocity.x, state.bomb.velocity.y);
ctx.stroke();
ctx.fillStyle = "white";
ctx.beginPath();
ctx.arc(0, 0, 6, 0, 2 * Math.PI);
ctx.fill();
} else if (state.phase === "in flight") {
ctx.fillStyle = "white";
ctx.rotate(state.bomb.rotation);
ctx.beginPath();
ctx.moveTo(-8, -2);
ctx.quadraticCurveTo(0, 12, 8, -2);
ctx.quadraticCurveTo(0, 2, -8, -2);
ctx.fill();
} else {
ctx.fillStyle = "white";
ctx.beginPath();
ctx.arc(0, 0, 6, 0, 2 * Math.PI);
ctx.fill();
}
ctx.restore();
if (state.bomb.y > window.innerHeight / state.scale) {
ctx.beginPath();
ctx.strokeStyle = "white";
const distance = state.bomb.y - window.innerHeight / state.scale;
ctx.moveTo(state.bomb.x, window.innerHeight / state.scale - 10);
ctx.lineTo(state.bomb.x, window.innerHeight / state.scale - distance);
ctx.moveTo(state.bomb.x, window.innerHeight / state.scale - 10);
ctx.lineTo(state.bomb.x - 5, window.innerHeight / state.scale - 15);
ctx.moveTo(state.bomb.x, window.innerHeight / state.scale - 10);
ctx.lineTo(state.bomb.x + 5, window.innerHeight / state.scale - 15);
ctx.stroke();
}
if (state.bomb.highlight) {
ctx.beginPath();
ctx.strokeStyle = "white";
ctx.lineWidth = 2;
ctx.moveTo(state.bomb.x, state.bomb.y + 20);
ctx.lineTo(state.bomb.x, state.bomb.y + 120);
ctx.moveTo(state.bomb.x, state.bomb.y + 20);
ctx.lineTo(state.bomb.x - 5, state.bomb.y + 25);
ctx.moveTo(state.bomb.x, state.bomb.y + 20);
ctx.lineTo(state.bomb.x + 5, state.bomb.y + 25);
ctx.stroke();
}
}
bombGrabAreaDOM.addEventListener("mousedown", function (e) {
hideInstructions();
if (state.phase === "aiming") {
isDragging = true;
dragStartX = e.clientX;
dragStartY = e.clientY;
document.body.style.cursor = "grabbing";
}
});
window.addEventListener("mousemove", function (e) {
if (isDragging) {
let deltaX = e.clientX - dragStartX;
let deltaY = e.clientY - dragStartY;
state.bomb.velocity.x = -deltaX;
state.bomb.velocity.y = deltaY;
setInfo(deltaX, deltaY);
draw();
}
});
function setInfo(deltaX, deltaY) {
const hypotenuse = Math.sqrt(deltaX ** 2 + deltaY ** 2);
const angleInRadians = Math.asin(deltaY / hypotenuse);
const angleInDegrees = (angleInRadians / Math.PI) * 180;
if (state.currentPlayer === 1) {
angle1DOM.innerText = Math.round(angleInDegrees);
velocity1DOM.innerText = Math.round(hypotenuse);
} else {
angle2DOM.innerText = Math.round(angleInDegrees);
velocity2DOM.innerText = Math.round(hypotenuse);
}
}
window.addEventListener("mouseup", function () {
if (isDragging) {
isDragging = false;
document.body.style.cursor = "default";
throwBomb();
}
});
function computerThrow() {
const numberOfSimulations = 2 + state.round * 3;
const bestThrow = runSimulations(numberOfSimulations);
initializeBombPosition();
state.bomb.velocity.x = bestThrow.velocityX;
state.bomb.velocity.y = bestThrow.velocityY;
setInfo(bestThrow.velocityX, bestThrow.velocityY);
draw();
delayTimeoutID = setTimeout(throwBomb, 1000);
}
function runSimulations(numberOfSimulations) {
let bestThrow = {
velocityX: undefined,
velocityY: undefined,
distance: Infinity,
};
simulationMode = true;
const enemyBuilding =
state.currentPlayer === 1
? state.buildings.at(-2)
: state.buildings.at(1);
const enemyX = enemyBuilding.x + enemyBuilding.width / 2;
const enemyY = enemyBuilding.height + 30;
for (let i = 0; i < numberOfSimulations; i++) {
const angleInDegrees = -10 + Math.random() * 100;
const angleInRadians = (angleInDegrees / 180) * Math.PI;
const velocity = 40 + Math.random() * 130;
const direction = state.currentPlayer === 1 ? 1 : -1;
const velocityX = Math.cos(angleInRadians) * velocity * direction;
const velocityY = Math.sin(angleInRadians) * velocity;
initializeBombPosition();
state.bomb.velocity.x = velocityX;
state.bomb.velocity.y = velocityY;
throwBomb();
const distance = Math.sqrt(
(enemyX - simulationImpact.x) ** 2 +
(enemyY - simulationImpact.y) ** 2
);
if (distance < bestThrow.distance) {
bestThrow = { velocityX, velocityY, distance };
}
}
simulationMode = false;
return bestThrow;
}
function throwBomb() {
if (simulationMode) {
previousAnimationTimestamp = 0;
animate(16);
} else {
state.phase = "in flight";
previousAnimationTimestamp = undefined;
animationFrameRequestID = requestAnimationFrame(animate);
}
}
function animate(timestamp) {
if (previousAnimationTimestamp === undefined) {
previousAnimationTimestamp = timestamp;
animationFrameRequestID = requestAnimationFrame(animate);
return;
}
const elapsedTime = timestamp - previousAnimationTimestamp;
const hitDetectionPrecision = 10;
for (let i = 0; i < hitDetectionPrecision; i++) {
moveBomb(elapsedTime / hitDetectionPrecision);
const miss = checkFrameHit() || checkBuildingHit();
const hit = checkGorillaHit();
if (simulationMode && (hit || miss)) {
simulationImpact = { x: state.bomb.x, y: state.bomb.y };
return;
}
if (miss) {
state.currentPlayer = state.currentPlayer === 1 ? 2 : 1;
if (state.currentPlayer === 1) state.round++;
state.phase = "aiming";
initializeBombPosition();
draw();
const computerThrowsNext =
settings.numberOfPlayers === 0 ||
(settings.numberOfPlayers === 1 && state.currentPlayer === 2);
if (computerThrowsNext) setTimeout(computerThrow, 50);
return;
}
if (hit) {
state.phase = "celebrating";
announceWinner();
draw();
return;
}
}
if (!simulationMode) draw();
previousAnimationTimestamp = timestamp;
if (simulationMode) {
animate(timestamp + 16);
} else {
animationFrameRequestID = requestAnimationFrame(animate);
}
}
function moveBomb(elapsedTime) {
const multiplier = elapsedTime / 200;
state.bomb.velocity.x += state.windSpeed * multiplier;
state.bomb.velocity.y -= 20 * multiplier;
state.bomb.x += state.bomb.velocity.x * multiplier;
state.bomb.y += state.bomb.velocity.y * multiplier;
const direction = state.currentPlayer === 1 ? -1 : +1;
state.bomb.rotation += direction * 5 * multiplier;
}
function checkFrameHit() {
if (
state.bomb.y < 0 ||
state.bomb.x < -state.shift / state.scale ||
state.bomb.x > (window.innerWidth - state.shift) / state.scale
) {
return true;
}
}
function checkBuildingHit() {
for (let i = 0; i < state.buildings.length; i++) {
const building = state.buildings[i];
if (
state.bomb.x + 4 > building.x &&
state.bomb.x - 4 < building.x + building.width &&
state.bomb.y - 4 < 0 + building.height
) {
for (let j = 0; j < state.blastHoles.length; j++) {
const blastHole = state.blastHoles[j];
const horizontalDistance = state.bomb.x - blastHole.x;
const verticalDistance = state.bomb.y - blastHole.y;
const distance = Math.sqrt(
horizontalDistance ** 2 + verticalDistance ** 2
);
if (distance < blastHoleRadius) {
return false;
}
}
if (!simulationMode) {
state.blastHoles.push({ x: state.bomb.x, y: state.bomb.y });
}
return true;
}
}
}
function checkGorillaHit() {
const enemyPlayer = state.currentPlayer === 1 ? 2 : 1;
const enemyBuilding =
enemyPlayer === 1
? state.buildings.at(1)
: state.buildings.at(-2);
ctx.save();
ctx.translate(
enemyBuilding.x + enemyBuilding.width / 2,
enemyBuilding.height
);
drawGorillaBody();
let hit = ctx.isPointInPath(state.bomb.x, state.bomb.y);
drawGorillaLeftArm(enemyPlayer);
hit ||= ctx.isPointInStroke(state.bomb.x, state.bomb.y);
drawGorillaRightArm(enemyPlayer);
hit ||= ctx.isPointInStroke(state.bomb.x, state.bomb.y);
ctx.restore();
return hit;
}
function announceWinner() {
if (settings.numberOfPlayers === 0) {
winnerDOM.innerText = `Computer ${state.currentPlayer}`;
} else if (
settings.numberOfPlayers === 1 &&
state.currentPlayer === 1
) {
winnerDOM.innerText = `You`;
} else if (
settings.numberOfPlayers === 1 &&
state.currentPlayer === 2
) {
winnerDOM.innerText = `Computer`;
} else {
winnerDOM.innerText = `Player ${state.currentPlayer}`;
}
showCongratulations();
}
singlePlayerButtonDOM.forEach((button) =>
button.addEventListener("click", () => {
settings.numberOfPlayers = 1;
gameModeDOM.innerHTML = "Player vs. Computer";
name1DOM.innerText = "Player";
name2DOM.innerText = "Computer";
newGame();
})
);
twoPlayersButtonDOM.forEach((button) =>
button.addEventListener("click", () => {
settings.numberOfPlayers = 2;
gameModeDOM.innerHTML = "Player vs. Player";
name1DOM.innerText = "Player 1";
name2DOM.innerText = "Player 2";
newGame();
})
);
autoPlayButtonDOM.forEach((button) =>
button.addEventListener("click", () => {
settings.numberOfPlayers = 0;
name1DOM.innerText = "Computer 1";
name2DOM.innerText = "Computer 2";
newGame();
})
);
function generateWindSpeed() {
return -10 + Math.random() * 20;
}
function setWindMillRotation() {
const rotationSpeed = Math.abs(50 / state.windSpeed);
windmillHeadDOM.style.animationDirection =
state.windSpeed > 0 ? "normal" : "reverse";
windmillHeadDOM.style.animationDuration = `${rotationSpeed}s`;
windSpeedDOM.innerText = Math.round(state.windSpeed);
}
window.addEventListener("mousemove", function (e) {
settingsDOM.style.opacity = 1;
info1DOM.style.opacity = 1;
info2DOM.style.opacity = 1;
});
const enterFullscreen = document.getElementById("enter-fullscreen");
const exitFullscreen = document.getElementById("exit-fullscreen");
function toggleFullscreen() {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen();
enterFullscreen.setAttribute("stroke", "transparent");
exitFullscreen.setAttribute("stroke", "white");
} else {
document.exitFullscreen();
enterFullscreen.setAttribute("stroke", "white");
exitFullscreen.setAttribute("stroke", "transparent");
}
}
</script>
</body>
</html>