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 9.8KB

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