You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

game.js 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576
  1. let Vec2 = require("./vec2");
  2. function randint(min, max) {
  3. return Math.floor(Math.random() * (max - min + 1)) + min;
  4. }
  5. function random(min, max) {
  6. return Math.random() * (max - min + 1) + min;
  7. }
  8. function diff(n1, n2) {
  9. return Math.abs(n1 - n2);
  10. }
  11. function createImage(url) {
  12. var img = document.createElement("img");
  13. img.src = url;
  14. return img;
  15. }
  16. function background(ctx, camera, offset) {
  17. if (!background.cache) {
  18. let cache = [];
  19. let n = window.innerWidth / 2;
  20. for (let i = 0; i < n; ++i) {
  21. let parallax = random(5.6, 9);
  22. cache.push({
  23. x: randint(0, window.innerWidth * parallax),
  24. y: randint(0, window.innerHeight * parallax),
  25. p: parallax
  26. });
  27. }
  28. background.cache = cache;
  29. }
  30. let cx = camera.x;
  31. let cy = camera.y;
  32. let cache = background.cache;
  33. let fs = ctx.fillStyle;
  34. ctx.fillStyle = "#FFFFFF";
  35. for (let i = 0, len = cache.length; i < len; ++i) {
  36. let p = cache[i];
  37. let x = (((p.x - cx) / p.p) + offset.x) % window.innerWidth;
  38. let y = (((p.y - cy) / p.p) + offset.y) % window.innerHeight;
  39. if (x < 0)
  40. x += window.innerWidth;
  41. if (y < 0)
  42. y += window.innerHeight;
  43. ctx.beginPath();
  44. ctx.arc(x, y, 1, 0, 2*Math.PI);
  45. ctx.closePath();
  46. ctx.fill();
  47. }
  48. ctx.fillStyle = fs;
  49. }
  50. window.addEventListener("resize", () => background.cache = null);
  51. class Animation {
  52. constructor({img, x, y, width, height, dwidth, dheight, wsteps, hsteps, nsteps, loop, fps, rot, offsetX, offsetY}) {
  53. loop = loop || false;
  54. fps = fps || 30;
  55. nsteps = nsteps || wsteps * hsteps;
  56. dwidth = dwidth || width;
  57. dheight = dheight || height;
  58. rot = rot || 0;
  59. offsetX = offsetX || 0;
  60. offsetY = offsetY || 0;
  61. this.img = img;
  62. this.x = x;
  63. this.y = y;
  64. this.offsetX = offsetX;
  65. this.offsetY = offsetY;
  66. this.rot = rot;
  67. this.width = width;
  68. this.height = height;
  69. this.dwidth = dwidth;
  70. this.dheight = dheight;
  71. this.wsteps = wsteps;
  72. this.hsteps = hsteps;
  73. this.wstep = 0;
  74. this.hstep = 0;
  75. this.step = 0;
  76. this.nsteps = nsteps;
  77. this.loop = loop;
  78. this.visible = true;
  79. this.onend = function(){};
  80. let interval = setInterval(() => {
  81. this.step += 1;
  82. if (this.step >= this.nsteps) {
  83. this.step = 0;
  84. this.wstep = 0;
  85. this.hstep = 0;
  86. if (!this.loop) {
  87. clearInterval(interval);
  88. this.onend();
  89. }
  90. return;
  91. }
  92. this.wstep += 1;
  93. if (this.wstep >= this.wsteps) {
  94. this.wstep = 0;
  95. this.hstep += 1;
  96. }
  97. }, 1000/fps);
  98. }
  99. animate(ctx) {
  100. if (!this.visible)
  101. return;
  102. ctx.translate(this.x, this.y);
  103. if (this.rot)
  104. ctx.rotate(this.rot);
  105. ctx.drawImage(
  106. this.img,
  107. this.wstep * this.width,
  108. this.hstep * this.width,
  109. this.width,
  110. this.height,
  111. this.offsetX,
  112. this.offsetY,
  113. this.dwidth,
  114. this.dheight
  115. );
  116. if (this.rot)
  117. ctx.rotate(-this.rot);
  118. }
  119. }
  120. class Entity {
  121. constructor(x, y, width, height, id, game) {
  122. this.pos = new Vec2(x, y);
  123. this.vel = new Vec2(0, 0);
  124. this.width = width;
  125. this.height = height;
  126. this.game = game;
  127. this.id = id;
  128. }
  129. draw(ctx) {}
  130. set(obj) {
  131. this.pos.set(obj.pos.x, obj.pos.y);
  132. this.vel.set(obj.vel.x, obj.vel.y);
  133. }
  134. update(dt) {
  135. this.pos.x += this.vel.x * dt;
  136. this.pos.y += this.vel.y * dt;
  137. }
  138. despawn() {}
  139. }
  140. let BulletImgs = {
  141. despawn: createImage("imgs/bullet_despawn.png")
  142. };
  143. let BulletSounds = {
  144. spawn: {url: "sounds/bullet_spawn.wav", vol: 0.5},
  145. despawn: {url: "sounds/bullet_despawn.wav", vol: 1}
  146. };
  147. class Bullet extends Entity {
  148. constructor(x, y, vel, id, ownerId, game) {
  149. super(x, y, 5, 5, id, game);
  150. this.vel.set(vel.x, vel.y);
  151. this.ownerId = ownerId;
  152. game.playSound(BulletSounds.spawn, this.pos);
  153. }
  154. draw(ctx, selfId) {
  155. if (selfId == this.ownerId) {
  156. ctx.fillStyle = "#FFFFFF";
  157. } else {
  158. ctx.fillStyle = "#FF0000";
  159. }
  160. ctx.beginPath();
  161. ctx.arc(-(this.width/2), -(this.height/2), this.width/2, 0, 2*Math.PI);
  162. ctx.closePath();
  163. ctx.fill();
  164. }
  165. set(obj) {
  166. super.set(obj);
  167. }
  168. despawn() {
  169. this.game.animate(new Animation({
  170. img: BulletImgs.despawn,
  171. x: this.pos.x,
  172. y: this.pos.y,
  173. width: 64,
  174. height: 64,
  175. dwidth: 16,
  176. dheight: 16,
  177. wsteps: 5,
  178. hsteps: 5
  179. }));
  180. this.game.playSound(BulletSounds.despawn, this.pos);
  181. }
  182. }
  183. let PlayerImgs = {
  184. thrust_back: createImage("imgs/player_thrust_back.png"),
  185. despawn: createImage("imgs/player_despawn.png")
  186. };
  187. let PlayerSounds = {
  188. despawn: {url: "sounds/player_despawn.wav", vol: 1},
  189. thrust: {url: "sounds/player_thrust.wav", vol: 1}
  190. };
  191. class Player extends Entity {
  192. constructor(x, y, id, rot, name, game) {
  193. super(x, y, 25, 60, id, game);
  194. this.rot = rot;
  195. this.rotVel = 0;
  196. this.keys = {};
  197. this.health = 0;
  198. this.name = name;
  199. this.thrustAnim = new Animation({
  200. img: PlayerImgs.thrust_back,
  201. x: this.pos.x,
  202. y: this.pos.y,
  203. width: 128,
  204. height: 128,
  205. dwidth: 64,
  206. dheight: 64,
  207. wsteps: 4,
  208. hsteps: 4,
  209. fps: 60,
  210. loop: true
  211. });
  212. this.thrustAnim.visible = false;
  213. game.animate(this.thrustAnim);
  214. if (this.id === game.id) {
  215. this.thrustSound = document.createElement("audio");
  216. this.thrustSound.src = PlayerSounds.thrust.url;
  217. this.thrustSound.loop = true;
  218. this.thrustSound.play();
  219. this.thrustSound.volume = 0;
  220. }
  221. }
  222. draw(ctx, selfId) {
  223. let h = Math.round((this.health * 2.4) + 15);
  224. if (selfId == this.id) {
  225. ctx.fillStyle = "rgb("+h+", "+h+", "+h+")";
  226. } else {
  227. ctx.fillStyle = "rgb("+h+", 0, 0)";
  228. }
  229. ctx.rotate(this.rot);
  230. if (this.keys.up) {
  231. this.thrustAnim.rot = this.rot;
  232. this.thrustAnim.x = this.pos.x;
  233. this.thrustAnim.y = this.pos.y;
  234. this.thrustAnim.offsetX = -this.thrustAnim.dwidth/2;
  235. this.thrustAnim.offsetY = this.thrustAnim.dwidth/2;
  236. if (this.keys.sprint) {
  237. this.thrustAnim.dheight = 100;
  238. if (this.id == selfId)
  239. this.game.screenShake(10);
  240. } else {
  241. this.thrustAnim.dheight = 64;
  242. }
  243. }
  244. ctx.beginPath();
  245. ctx.moveTo(0, -(this.height/2));
  246. ctx.lineTo(-this.width, this.height/2);
  247. ctx.lineTo(this.width, this.height/2);
  248. ctx.closePath();
  249. ctx.fill();
  250. {
  251. let h = (this.height/2) + 10;
  252. if (this.thrustAnim.visible)
  253. h += this.thrustAnim.dheight;
  254. }
  255. ctx.rotate(-this.rot);
  256. ctx.textAlign = "center";
  257. ctx.textBaseline = "middle";
  258. ctx.font = "bold 30px Arial";
  259. ctx.strokeStyle = "#000000";
  260. ctx.fillStyle = "#ffffff";
  261. ctx.lineWidth = 2;
  262. ctx.fillText(this.name, 0, 0);
  263. ctx.strokeText(this.name, 0, 0);
  264. //Draw pointers to far away players
  265. if (selfId == this.id) {
  266. this.game.entities.forEach((e) => {
  267. //Only draw players
  268. if (!(e instanceof Player))
  269. return;
  270. //Only draw far away players
  271. if (
  272. diff(e.pos.x, this.pos.x) < window.innerWidth / 2 &&
  273. diff(e.pos.y, this.pos.y) < window.innerHeight / 2
  274. ) {
  275. return;
  276. }
  277. let pos = e.pos.clone().sub(this.pos).normalize().scale(130);
  278. ctx.fillStyle = "rgb(255, 0, 0)";
  279. ctx.beginPath();
  280. ctx.arc(pos.x, pos.y, 5, 0, 2*Math.PI);
  281. ctx.closePath();
  282. ctx.fill();
  283. });
  284. }
  285. }
  286. set(obj) {
  287. super.set(obj);
  288. let lastHealth = this.health;
  289. this.rot = obj.rot;
  290. this.rotVel = obj.rotVel;
  291. this.keys = obj.keys;
  292. this.health = obj.health;
  293. if (obj.name)
  294. this.name = obj.name;
  295. if (this.id == this.game.id && lastHealth > obj.health) {
  296. this.game.screenShake(50);
  297. }
  298. }
  299. update(dt) {
  300. super.update(dt);
  301. this.rot += this.rotVel * dt;
  302. if (this.keys.up) {
  303. this.thrustAnim.visible = true;
  304. if (this.keys.sprint && this.thrustSound)
  305. this.thrustSound.volume = 0.6;
  306. else if (this.id === this.game.id)
  307. this.thrustSound.volume = 0.3;
  308. } else {
  309. this.thrustAnim.visible = false;
  310. if (this.thrustSound)
  311. this.thrustSound.volume = 0;
  312. }
  313. }
  314. despawn() {
  315. this.game.animate(new Animation({
  316. img: PlayerImgs.despawn,
  317. x: this.pos.x,
  318. y: this.pos.y,
  319. width: 64,
  320. height: 64,
  321. dwidth: 256,
  322. dheight: 256,
  323. offsetX: -128,
  324. offsetY: -128,
  325. wsteps: 5,
  326. hsteps: 5
  327. }));
  328. this.thrustAnim.visible = false;
  329. this.thrustAnim.loop = false;
  330. this.game.playSound(PlayerSounds.despawn, this.pos);
  331. if (this.thrustSound)
  332. this.thrustSound.pause();
  333. }
  334. }
  335. function createEntity(obj, game) {
  336. if (obj.type == "player") {
  337. return new Player(obj.pos.x, obj.pos.y, obj.id, obj.rot, obj.name, game);
  338. } else if (obj.type == "bullet") {
  339. return new Bullet(obj.pos.x, obj.pos.y, obj.vel, obj.id, obj.ownerId, game);
  340. } else {
  341. throw new Error("Unknown entity type: "+obj.type);
  342. }
  343. }
  344. export default class Game {
  345. constructor(sock, canvas, name) {
  346. this.sock = sock;
  347. this.canvas = canvas;
  348. this.ctx = canvas.getContext("2d");
  349. this.id = null;
  350. this.camera = new Vec2(0, 0);
  351. this.raf = null;
  352. this.prevTime = new Date().getTime();
  353. this.player = null;
  354. this.onloss = function(){};
  355. this.shake = 0;
  356. this.shakedec = 0.5;
  357. this.keymap = [];
  358. this.keymap[87] = "up";
  359. this.keymap[38] = "up";
  360. this.keymap[83] = "down";
  361. this.keymap[40] = "down";
  362. this.keymap[65] = "left";
  363. this.keymap[37] = "left";
  364. this.keymap[68] = "right";
  365. this.keymap[39] = "right";
  366. this.keymap[32] = "shoot";
  367. this.keymap[16] = "sprint";
  368. this.entities = [];
  369. this.animations = [];
  370. sock.on("ready", () => {
  371. sock.send("get_id", {
  372. name: name
  373. }, (err, res) => {
  374. this.id = res.id;
  375. });
  376. });
  377. sock.on("set", (msg) => {
  378. msg.forEach((m) => {
  379. if (this.entities[m.id]) {
  380. this.entities[m.id].set(m);
  381. } else {
  382. if (!m.type)
  383. return;
  384. let ent = createEntity(m, this);
  385. this.entities[m.id] = ent;
  386. }
  387. });
  388. });
  389. sock.on("despawn", (msg) => {
  390. if (!this.entities[msg.id])
  391. return;
  392. this.entities[msg.id].despawn();
  393. delete this.entities[msg.id];
  394. if (msg.id == this.id) {
  395. this.screenShake(400);
  396. setTimeout(() => {
  397. this.stop();
  398. this.onloss();
  399. }, 1200);
  400. }
  401. });
  402. window.addEventListener("keydown", (evt) => {
  403. if (this.keymap[evt.keyCode]) {
  404. evt.preventDefault();
  405. evt.stopPropagation();
  406. this.sock.send("keydown", {
  407. key: this.keymap[evt.keyCode]
  408. });
  409. }
  410. });
  411. window.addEventListener("keyup", (evt) => {
  412. if (this.keymap[evt.keyCode]) {
  413. this.sock.send("keyup", {
  414. key: this.keymap[evt.keyCode]
  415. });
  416. }
  417. });
  418. this.update();
  419. }
  420. update() {
  421. let dt = new Date().getTime() - this.prevTime;
  422. this.prevTime = new Date().getTime();
  423. this.canvas.width = window.innerWidth;
  424. this.canvas.height = window.innerHeight;
  425. let player = this.entities[this.id];
  426. if (player) {
  427. this.camera.set(
  428. player.pos.x - (window.innerWidth / 2),
  429. player.pos.y - (window.innerHeight / 2)
  430. );
  431. }
  432. let shakeOffset = new Vec2(0, 0);
  433. if (this.shake > 0) {
  434. shakeOffset.set(
  435. (Math.random() - 0.5) * this.shake,
  436. (Math.random() - 0.5) * this.shake
  437. );
  438. this.shake -= dt * this.shakedec;
  439. } else {
  440. shakeOffset.set(0, 0);
  441. }
  442. let cam = this.camera.clone().add(shakeOffset);
  443. background(this.ctx, this.camera, shakeOffset);
  444. this.ctx.translate(-cam.x, -cam.y);
  445. this.entities.forEach((ent) => {
  446. this.ctx.save();
  447. this.ctx.translate(ent.pos.x, ent.pos.y);
  448. ent.draw(this.ctx, this.id);
  449. this.ctx.restore();
  450. ent.update(dt);
  451. });
  452. this.animations.forEach((a) => {
  453. this.ctx.save();
  454. a.animate(this.ctx);
  455. this.ctx.restore();
  456. });
  457. this.ctx.translate(cam.x, cam.y);
  458. this.raf = window.requestAnimationFrame(this.update.bind(this));
  459. }
  460. stop() {
  461. window.cancelAnimationFrame(this.raf);
  462. }
  463. screenShake(n) {
  464. if (this.shake < n)
  465. this.shake = n;
  466. }
  467. animate(animation) {
  468. let i = this.animations.length;
  469. this.animations.push(animation);
  470. animation.onend = () => {
  471. delete this.animations[i];
  472. };
  473. }
  474. playSound(s, pos) {
  475. let player = this.entities[this.id];
  476. if (!player)
  477. return;
  478. let dist = player.pos.clone().sub(pos);
  479. let sound = document.createElement("audio");
  480. sound.src = s.url;
  481. sound.volume = Math.max(1 - (dist.length() / 1000), 0) * s.vol;
  482. sound.play();
  483. }
  484. }