react-ts-asteroids
Version:
Asteroids written in Typescript using React
437 lines (436 loc) • 15.7 kB
JavaScript
var et = Object.defineProperty;
var ot = (n, t, i) => t in n ? et(n, t, { enumerable: !0, configurable: !0, writable: !0, value: i }) : n[t] = i;
var s = (n, t, i) => ot(n, typeof t != "symbol" ? t + "" : t, i);
import { jsxs as M, jsx as S } from "react/jsx-runtime";
import { useRef as y, useState as x, useMemo as st, useCallback as u, useEffect as v } from "react";
function X(n, t, i) {
return {
x: (n.x - t.x) * Math.cos(i) - (n.y - t.y) * Math.sin(i) + t.x,
y: (n.x - t.x) * Math.sin(i) + (n.y - t.y) * Math.cos(i) + t.y
};
}
function h(n, t) {
return Math.random() * (t - n + 1) + n;
}
function Q(n, t, i, a) {
let p = h(n, t);
for (; p > i && p < a; )
p = Math.random() * (t - n + 1) + n;
return p;
}
function nt(n, t) {
const i = [];
for (let a = 0; a < n; a++)
i[a] = {
x: (-Math.sin(360 / n * a * Math.PI / 180) + Math.random() * 0.2) * t,
y: (-Math.cos(360 / n * a * Math.PI / 180) + Math.random() * 0.2) * t
};
return i;
}
class rt {
constructor(t) {
s(this, "position");
s(this, "velocity");
s(this, "rotation");
s(this, "radius");
s(this, "delete");
const i = X({ x: 0, y: -20 }, { x: 0, y: 0 }, t.ship.rotation * Math.PI / 180);
this.position = {
x: t.ship.position.x + i.x,
y: t.ship.position.y + i.y
}, this.rotation = t.ship.rotation, this.velocity = {
x: i.x / 2,
y: i.y / 2
}, this.radius = 2;
}
destroy() {
this.delete = !0;
}
render(t) {
this.position.x += this.velocity.x, this.position.y += this.velocity.y, (this.position.x < 0 || this.position.y < 0 || this.position.x > t.screen.width || this.position.y > t.screen.height) && this.destroy();
const i = t.context;
i.save(), i.translate(this.position.x, this.position.y), i.rotate(this.rotation * Math.PI / 180), i.fillStyle = t.colors.text, i.lineWidth = 0.5, i.beginPath(), i.arc(0, 0, 2, 0, 2 * Math.PI), i.closePath(), i.fill(), i.restore();
}
}
class L {
// Required by GameObject interface
constructor(t) {
s(this, "position");
s(this, "velocity");
s(this, "radius");
s(this, "lifeSpan");
s(this, "inertia");
s(this, "delete");
s(this, "rotation");
this.position = t.position, this.velocity = t.velocity, this.radius = t.size, this.lifeSpan = t.lifeSpan, this.inertia = 0.98, this.rotation = 0;
}
destroy() {
this.delete = !0;
}
render(t) {
this.position.x += this.velocity.x, this.position.y += this.velocity.y, this.velocity.x *= this.inertia, this.velocity.y *= this.inertia, this.radius -= 0.1, this.radius < 0.1 && (this.radius = 0.1), this.lifeSpan-- < 0 && this.destroy();
const i = t.context;
i.save(), i.translate(this.position.x, this.position.y), i.fillStyle = t.colors.text, i.lineWidth = 2, i.beginPath(), i.moveTo(0, -this.radius), i.arc(0, 0, this.radius, 0, 2 * Math.PI), i.closePath(), i.fill(), i.restore();
}
}
class ht {
constructor(t) {
s(this, "position");
s(this, "velocity");
s(this, "rotation");
s(this, "rotationSpeed");
s(this, "speed");
s(this, "inertia");
s(this, "radius");
s(this, "lastShot");
s(this, "create");
s(this, "onDie");
s(this, "delete");
this.position = t.position, this.velocity = {
x: 0,
y: 0
}, this.rotation = 0, this.rotationSpeed = 6, this.speed = 0.15, this.inertia = 0.99, this.radius = 20, this.lastShot = 0, this.create = t.create, this.onDie = t.onDie;
}
destroy() {
this.delete = !0, this.onDie();
for (let t = 0; t < 60; t++) {
const i = new L({
lifeSpan: h(60, 100),
size: h(1, 4),
position: {
x: this.position.x + h(-this.radius / 4, this.radius / 4),
y: this.position.y + h(-this.radius / 4, this.radius / 4)
},
velocity: {
x: h(-1.5, 1.5),
y: h(-1.5, 1.5)
}
});
this.create(i, "particles");
}
}
rotate(t) {
t === "LEFT" && (this.rotation -= this.rotationSpeed), t === "RIGHT" && (this.rotation += this.rotationSpeed);
}
accelerate() {
this.velocity.x -= Math.sin(-this.rotation * Math.PI / 180) * this.speed, this.velocity.y -= Math.cos(-this.rotation * Math.PI / 180) * this.speed;
const t = X({ x: 0, y: -10 }, { x: 0, y: 0 }, (this.rotation - 180) * Math.PI / 180), i = new L({
lifeSpan: h(20, 40),
size: h(1, 3),
position: {
x: this.position.x + t.x + h(-2, 2),
y: this.position.y + t.y + h(-2, 2)
},
velocity: {
x: t.x / h(3, 5),
y: t.y / h(3, 5)
}
});
this.create(i, "particles");
}
render(t) {
if (t.keys.up && this.accelerate(), t.keys.left && this.rotate("LEFT"), t.keys.right && this.rotate("RIGHT"), t.keys.space && Date.now() - this.lastShot > 300) {
const a = new rt({ ship: this });
this.create(a, "bullets"), this.lastShot = Date.now();
}
this.position.x += this.velocity.x, this.position.y += this.velocity.y, this.velocity.x *= this.inertia, this.velocity.y *= this.inertia, this.rotation >= 360 && (this.rotation -= 360), this.rotation < 0 && (this.rotation += 360), this.position.x > t.screen.width ? this.position.x = 0 : this.position.x < 0 && (this.position.x = t.screen.width), this.position.y > t.screen.height ? this.position.y = 0 : this.position.y < 0 && (this.position.y = t.screen.height);
const i = t.context;
i.save(), i.translate(this.position.x, this.position.y), i.rotate(this.rotation * Math.PI / 180), i.strokeStyle = t.colors.text, i.fillStyle = t.colors.background, i.lineWidth = 2, i.beginPath(), i.moveTo(0, -15), i.lineTo(10, 10), i.lineTo(5, 7), i.lineTo(-5, 7), i.lineTo(-10, 10), i.closePath(), i.fill(), i.stroke(), i.restore();
}
}
class F {
constructor(t) {
s(this, "position");
s(this, "velocity");
s(this, "rotation");
s(this, "rotationSpeed");
s(this, "radius");
s(this, "score");
s(this, "create");
s(this, "addScore");
s(this, "vertices");
s(this, "delete");
this.position = t.position, this.velocity = {
x: h(-1.5, 1.5),
y: h(-1.5, 1.5)
}, this.rotation = 0, this.rotationSpeed = h(-1, 1), this.radius = t.size, this.score = 80 / this.radius * 5, this.create = t.create, this.addScore = t.addScore, this.vertices = nt(8, t.size);
}
destroy() {
this.addScore(this.score);
for (let t = 0; t < this.radius; t++) {
const i = new L({
lifeSpan: h(60, 100),
size: h(1, 3),
position: {
x: this.position.x + h(-this.radius / 4, this.radius / 4),
y: this.position.y + h(-this.radius / 4, this.radius / 4)
},
velocity: {
x: h(-1.5, 1.5),
y: h(-1.5, 1.5)
}
});
this.create(i, "particles");
}
if (this.radius > 10)
for (let t = 0; t < 2; t++) {
const i = new F({
size: this.radius / 2,
position: {
x: h(-10, 20) + this.position.x,
y: h(-10, 20) + this.position.y
},
create: this.create,
addScore: this.addScore
});
this.create(i, "asteroids");
}
}
render(t) {
this.position.x += this.velocity.x, this.position.y += this.velocity.y, this.rotation += this.rotationSpeed, this.rotation >= 360 && (this.rotation -= 360), this.rotation < 0 && (this.rotation += 360), this.position.x > t.screen.width + this.radius ? this.position.x = -this.radius : this.position.x < -this.radius && (this.position.x = t.screen.width + this.radius), this.position.y > t.screen.height + this.radius ? this.position.y = -this.radius : this.position.y < -this.radius && (this.position.y = t.screen.height + this.radius);
const i = t.context;
i.save(), i.translate(this.position.x, this.position.y), i.rotate(this.rotation * Math.PI / 180), i.strokeStyle = t.colors.text, i.lineWidth = 2, i.beginPath(), i.moveTo(0, -this.radius);
for (let a = 1; a < this.vertices.length; a++)
i.lineTo(this.vertices[a].x, this.vertices[a].y);
i.closePath(), i.stroke(), i.restore();
}
}
const g = {
LEFT: 37,
RIGHT: 39,
UP: 38,
A: 65,
D: 68,
W: 87,
SPACE: 32
}, ct = (n, t, i, a) => ({
screen: n,
context: t,
keys: i,
colors: a
}), pt = ({ darkMode: n = !1 }) => {
const t = y(null), i = y(null), a = y([]), p = y([]), k = y([]), z = y([]), T = y(), [c, Z] = x({
width: window.innerWidth,
height: window.innerHeight,
ratio: window.devicePixelRatio || 1
}), [d, K] = x(null), [W, _] = x({
left: 0,
right: 0,
up: 0,
down: 0,
space: 0
}), [B] = x(3), [m, O] = x(0), [tt, it] = x(() => Number(localStorage.topscore) || 0), [b, N] = x(!1), U = y(!0), C = y({ inGame: !1 }), [q, V] = x(!1), f = st(() => ({
text: n ? "#ffffff" : "#000000",
background: n ? "#000000" : "#ffffff",
border: n ? "#ffffff" : "#000000"
}), [n]), E = u(() => {
if (i.current) {
const { width: e, height: o } = i.current.getBoundingClientRect();
Z({
width: e,
height: o,
ratio: window.devicePixelRatio || 1
});
}
}, []), A = u((e, o) => {
const { keyCode: r } = o;
_((l) => ({
...l,
left: r === g.LEFT || r === g.A ? e : l.left,
right: r === g.RIGHT || r === g.D ? e : l.right,
up: r === g.UP || r === g.W ? e : l.up,
space: r === g.SPACE ? e : l.space
}));
}, []), P = u((e, o) => {
o === "ship" && a.current.push(e), o === "asteroids" && p.current.push(e), o === "bullets" && k.current.push(e), o === "particles" && z.current.push(e);
}, []), Y = u((e) => {
C.current.inGame && O((o) => o + e);
}, []);
v(() => {
C.current.inGame = b;
}, [b]), v(() => {
}, [m]);
const $ = u(() => {
C.current.inGame = !1, N(!1), it((e) => {
const o = Math.max(m, e);
return o > e && (localStorage.topscore = o), o;
});
}, [m]), j = u((e) => {
const o = (r) => {
Y(r);
};
for (let r = 0; r < e; r++) {
const l = new F({
size: 80,
position: {
x: Q(0, c.width, c.width / 2 - 60, c.width / 2 + 60),
y: Q(0, c.height, c.height / 2 - 60, c.height / 2 + 60)
},
create: P,
addScore: o
// Use the wrapped version
});
P(l, "asteroids");
}
}, [c.width, c.height, P, Y]), D = u(() => {
var l, w;
C.current.inGame = !0, N(!0), O(0), a.current = [], p.current = [], k.current = [], z.current = [];
const e = ((l = i.current) == null ? void 0 : l.getBoundingClientRect().width) ?? c.width, o = ((w = i.current) == null ? void 0 : w.getBoundingClientRect().height) ?? c.height, r = new ht({
position: {
x: e / 2,
y: o / 2
},
create: P,
onDie: $
});
P(r, "ship"), j(B);
}, [B, P, $, j, c.width, c.height]), J = u((e, o) => {
const r = e.position.x - o.position.x, l = e.position.y - o.position.y;
return Math.sqrt(r * r + l * l) < e.radius + o.radius;
}, []), I = u((e) => {
e.forEach((r) => {
!r.delete && d && r.render(ct(c, d, W, f));
});
const o = e.filter((r) => !r.delete);
o.length !== e.length && (e.length = 0, e.push(...o));
}, [c, d, W, f]), G = u((e, o) => {
let r = e.length - 1, l;
for (r; r > -1; --r)
for (l = o.length - 1, l; l > -1; --l) {
const w = e[r], R = o[l];
J(w, R) && (w.destroy(), R.destroy(), w.delete = !0, R.delete = !0);
}
}, [J]), H = u(() => {
d && (d.save(), d.scale(c.ratio, c.ratio), d.fillStyle = f.background, d.globalAlpha = 0.4, d.fillRect(0, 0, c.width, c.height), d.globalAlpha = 1, b && (I(a.current), I(p.current), I(k.current), I(z.current), k.current.length > 0 && p.current.length > 0 && G(k.current, p.current), a.current.length > 0 && p.current.length > 0 && G(a.current, p.current)), d.restore(), T.current = requestAnimationFrame(H));
}, [d, c, b, I, G]);
return v(() => {
E();
const e = new ResizeObserver(E);
return i.current && e.observe(i.current), () => {
e.disconnect();
};
}, [E]), v(() => {
const e = (r) => A(0, r), o = (r) => A(1, r);
return window.addEventListener("keyup", e), window.addEventListener("keydown", o), () => {
window.removeEventListener("keyup", e), window.removeEventListener("keydown", o);
};
}, [A]), v(() => {
var o;
const e = (o = t.current) == null ? void 0 : o.getContext("2d");
e && K(e);
}, []), v(() => {
d && U.current && (U.current = !1, D());
}, [d, D]), v(() => {
if (d)
return T.current = requestAnimationFrame(H), () => {
T.current && cancelAnimationFrame(T.current);
};
}, [d, H]), v(() => {
}, [b, m]), /* @__PURE__ */ M(
"div",
{
ref: i,
id: "react-ts-asteroids",
style: {
height: "100%",
width: "100%",
position: "relative",
overflow: "hidden",
color: f.text,
padding: 0
},
children: [
!b && /* @__PURE__ */ M("div", { className: "endgame", style: {
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
padding: "16px",
zIndex: 1,
textAlign: "center"
}, children: [
/* @__PURE__ */ S("p", { children: "Game over!" }),
/* @__PURE__ */ M("p", { children: [
"Score: ",
m
] }),
/* @__PURE__ */ S(
"button",
{
onClick: D,
onMouseEnter: () => V(!0),
onMouseLeave: () => V(!1),
style: {
border: `4px solid ${f.border}`,
backgroundColor: q ? f.text : "transparent",
color: q ? f.background : f.text,
fontSize: "20px",
padding: "10px 20px",
margin: "10px",
cursor: "pointer"
},
children: "Try again?"
}
)
] }),
/* @__PURE__ */ S("div", { style: {
position: "absolute",
top: 15,
left: 20,
zIndex: 1,
fontSize: 20
}, children: /* @__PURE__ */ M("div", { className: "score current-score", children: [
"Score: ",
m
] }) }),
/* @__PURE__ */ S("div", { style: {
position: "absolute",
top: 15,
right: 20,
zIndex: 1,
fontSize: 20
}, children: /* @__PURE__ */ M("div", { className: "score top-score", children: [
"Top Score: ",
tt
] }) }),
/* @__PURE__ */ S("div", { style: {
position: "absolute",
top: 15,
left: "50%",
transform: "translate(-50%, 0)",
zIndex: 1,
fontSize: 11,
textAlign: "center",
lineHeight: 1.6
}, children: /* @__PURE__ */ M("span", { className: "controls", children: [
"Use [A][S][W][D] or [←][↑][↓][→] to MOVE",
/* @__PURE__ */ S("br", {}),
"Use [SPACE] to SHOOT"
] }) }),
/* @__PURE__ */ S(
"canvas",
{
ref: t,
id: "react-ts-asteroids--canvas",
width: c.width * c.ratio,
height: c.height * c.ratio,
style: {
width: "100%",
height: "100%",
display: "block",
backgroundColor: f.background,
position: "absolute",
top: 0,
bottom: 0,
left: 0,
right: 0
}
}
)
]
}
);
};
export {
pt as Asteroids
};