]> git.plutz.net Git - flarejs/blob - engine.js
d4b920990200254df3fc0bf56dc461215f79d835
[flarejs] / engine.js
1 var gamedata = {
2   // associative array for gamedata files
3   load: function(def) { return this[def] ? this[def] : this[def] = new Textfile(def); }
4 }
5
6 var gfx = {
7   // associative array for game graphics
8   load: function(def) {
9     if (!this[def]) {
10       this[def] = document.createElement("img");
11       this[def].setAttribute("src", def);
12     }
13     return this[def];
14   }
15 }
16
17 // map object is global
18 var map;
19
20 // quest status
21 var qstatus = {};
22
23 function Textfile(txtfile) {
24   // fetch and parse gamedata file into structured object
25   var lines, fetch = new XMLHttpRequest();
26   fetch.open("GET", txtfile, false);
27   fetch.send();
28
29   var line, key, value, ref = this, section;
30   for (line of fetch.responseText.split('\n')) switch(true) {
31     // section headers that always become array elements
32     case /^\[(npc|event|layer|dialog)\]$/.test(line):
33       section = line.split(/[\]\[]/)[1]; ref = {};
34       if                     (!this[section]) this[section] = [ref];
35       else if ( Array.isArray(this[section])) this[section].push(ref);
36       break;
37     // general section headers (may or may not be unique)
38     case /^\[.*\]$/.test(line):
39       section = line.split(/[\]\[]/)[1]; ref = {};
40       if                     (!this[section]) this[section] = ref;
41       else if (!Array.isArray(this[section])) this[section] = [this[section],ref];
42       else if ( Array.isArray(this[section])) this[section].push(ref);
43       break;
44     // frame definition from animation files
45     // frame=#frame,direction,srcX,srcY,width,height,dstX,dstY
46     case /^frame=[0-9]+,[0-7](,[0-9-]+){6}$/.test(line):
47       key = line.split(/[=,]/).slice(1,3);  value = line.split(/,/).slice(-6);
48       if (!ref.frame) ref.frame = [];
49       if (!ref.frame[key[0]]) ref.frame[key[0]] = [];
50       ref.frame[key[0]][key[1]] = value.map(x => parseInt(x));
51       break;
52     // order of gfx draw from engine/hero_layers.txt
53     // layer=direction,list,of,limbs,...
54     case /^layer=[0-9]+,/.test(line):
55       key = line.split(/[=,]/)[1];  value = line.split(/,/).slice(1);
56       if (!ref.layer) ref.layer = []; ref.layer[key] = value;
57       break;
58     // empty data= line starting map data in map files
59     case /^data=$/.test(line):
60       ref.data = [];
61       break;
62     // map data array
63     case /^[0-9,]+,$/.test(line):
64       value = line.split(/,/).slice(0,-1);
65       ref.data = ref.data.concat(value);
66       break;
67     // map data array (last line)
68     case /^[0-9,]+$/.test(line):
69       value = line.split(/,/);
70       ref.data = ref.data.concat(value);
71       ref.data = ref.data.map(x => parseInt(x));
72       break;
73     // tile description from tiledef file
74     // tile=#tile,srcX,srcY,width,height,dstX,dstY
75     case /^tile=[0-9]+,/.test(line):
76       key = line.split(/[=,]/)[1];  value = line.split(/,/).slice(1);
77       if (!ref.tile) ref.tile = []; ref.tile[key] = value.map(x => parseInt(x));
78       break;
79     // animated tile from tiledef file
80     // animation=#tile;(srcX,srcY,duration;)...
81     case /^animation=[0-9]+;.*;$/.test(line):
82       key = line.split(/[=;]/)[1];  value = line.split(/;/).slice(1,-1);
83       value = value.map(x => x.split(/,/));
84       value.forEach(x => x[2] = parseInt(x[2]));
85       if (!ref.animation) ref.animation = []; ref.animation[key] = value;
86       break;
87     // frame duration from animation file (im millisec)
88     case /^duration=[0-9]+ms$/.test(line):
89       ref.duration = line.split(/=|ms$/)[1];
90       break;
91     // frame duration from animation file (in whole seconds)
92     case /^duration=[0-9]+s$/.test(line):
93       ref.duration = line.split(/=|s$/)[1] * 1000;
94       break;
95     // mapmod events
96     case /^mapmod=.*$/.test(line):
97       ref.mapmod = line.split(/[=]/)[1].split(/[;]/).map( m => m.split(/[,]/).map( n => n * 1 ? n * 1 : n) );
98       break;
99     // status array used in events
100     case /^(requires_status|requires_not_status|set_status|unset_status)=.*$/.test(line):
101       key = line.split(/[=]/)[0]; value = line.split(/[=]/)[1];
102       ref[key] = value.split(/[,]/);
103       break;
104     // general array type, will not be aggregated
105     case /^[^#].*=.*,.*$/.test(line):
106       key = line.split(/[=]/)[0]; value = line.split(/[=]/)[1].split(/,/);
107       ref[key] = value.map(m => m * 1 ? m * 1 : m);  // parse numerics
108       break;
109     // general key=value
110     case /^[^#].*=.+$/.test(line):
111       key = line.split(/[=]/)[0];
112       value = line.split(/=/).slice(1).join("=");
113       value = value * 1 ? value * 1 : value;  // try to parse as numeric
114       if                     (!ref[key]) ref[key] = value;
115       else if (!Array.isArray(ref[key])) ref[key] = [ref[key],value];
116       else if ( Array.isArray(ref[key])) ref[key].push(value);
117       break;
118   }
119 }
120
121 function Map(textdef) {
122   // object for map operations (drawing coorinate transformation, etc.)
123   this.info = gamedata.load(textdef);
124   const tileset = gamedata.load(this.info.header.tileset);
125   const frametime = performance.now();
126   gfx.load(tileset.img);
127
128   const  h = this.info.header.height,      w = this.info.header.width;
129   const th = this.info.header.tileheight, tw = this.info.header.tilewidth;
130   var posx = canvas.canvas.width / 2, posy = canvas.canvas.height / 2 - h * th/2;
131
132   canvas.fillStyle = "rgba("+this.info.header.background_color+")";
133
134   // precalculated tile positions
135   // assign x/y coordinates to each tile index
136   // looks dumb but is faster than on the fly isometric calculations
137   const dx = [], dy = [];
138   for ( let y = 0; y < h; y++ ) for ( let x = 0; x < w; x++ ) {
139     dx[y * w + x] = (w + x - y) * tw / 2;
140     dy[y * w + x] = (x + y) * th / 2;
141   }
142
143   const npcs = [];
144   if (this.info.npc) for (let def of this.info.npc) {
145     let loc = def.location[1] * w + def.location[0];
146     npcs[loc] = new Npc(def.filename)
147     npcs[loc].place(dx[loc], dy[loc]);
148     this.info.layer.find(l => l.type == "collision").data[loc] = 1;
149   }
150
151   this.triggers = [];
152   for ( let ev of this.info.event.filter(e => e.activate == "on_trigger") ) {
153     for (let x = ev.location[0]; x < ev.location[0] + ev.location[2]; x++)
154       for (let y = ev.location[1]; y < ev.location[1] + ev.location[3]; y++) {
155         let loc = y * w + x;
156         if (!this.triggers[loc]) this.triggers[loc] = [];
157         this.triggers[loc].push(ev);
158       }
159   }
160
161   // tile index for pixel position on map
162   this.tileAt = function(x, y) {
163     var r = (y + th / 2) / th; var c = (x - w * tw /2) / tw;
164     var nx = r + c |0; var ny = r - c |0;
165     return ny * w + nx;
166   }
167   this.tileX = (x,y) => this.tileAt(x,y) % w;
168   this.tileY = (x,y) => this.tileAt(x,y) / w |0;
169
170   // vice versa the pixel coordinates of a given map tile
171   this.positionOf = (tl, ty = -1) => (~ty) ? [dx[ty * w + tl], dy[ty * w + tl]] : [ dx[tl], dy[tl] ];
172   this.xOf = (tl, ty = -1) => (~ty) ? dx[ty * w + tl] : dx[tl];
173   this.yOf = (tl, ty = -1) => (~ty) ? dy[ty * w + tl] : dy[tl]; 
174
175   // map pixel for given screen pixel
176   this.mapX = x => x - posx;
177   this.mapY = y => y - posy;
178
179   // center map to pixel position (by setting drawing offsets)
180   this.center = function(x, y) {
181     posx = canvas.canvas.width  / 2 - x;
182     posy = canvas.canvas.height / 2 - y;
183     return this;
184   }
185
186   // draw the entire map, including all mobs
187   // mobs is an array of all heros, enemies, loot, npcs, etc.
188   this.draw = function(mobs) {
189     const bg = this.info.layer.find(l => l.type == "background").data;
190     const ob = this.info.layer.find(l => l.type == "object").data;
191     var i, mm = [];
192
193     // mobs only have x/y pixel positions,
194     // to draw them in order with map tiles we set up
195     // an array with current tile positions of mobs
196     mobs.forEach(m => {
197       let i = this.tileAt(m.position[0], m.position[1]);
198       mm[i] = mobs.filter(m => i == this.tileAt(m.position[0], m.position[1]));
199     });
200     canvas.fillRect(0,0, canvas.canvas.width, canvas.canvas.height);
201
202     // draw background layer first
203     for ( i = 0; i < h * w; i++ ) draw_tile(bg[i], posx + dx[i], posy + dy[i]);
204
205     // draw object layer and mobs
206     for ( i = 0; i < h * w; i++ ) {
207       draw_tile(ob[i], posx + dx[i], posy + dy[i]);
208       if (mm[i]) mm[i].forEach(m => m.draw(posx + m.position[0], posy + m.position[1]));
209       if (npcs[i]) npcs[i].draw(posx + dx[i], posy + dy[i]);
210     }
211   }
212
213   // draw a single map tile at screen position x/y
214   // tile may be animated, if so defined in tiledef
215   function draw_tile(tile, x, y) {
216     const t = tileset.tile[tile];
217     const f = tileset.animation[tile];
218     x = x |0; y = y |0;
219
220     if (t && f) {
221       frame = ((performance.now() - frametime) / f[0][2] |0) % f.length;
222       if ( x + t[2] - t[4] > 0 && y + t[3] - t[5] > 0 && x - t[4] < canvas.canvas.width && y - t[5] < canvas.canvas.height)
223       canvas.drawImage(gfx[tileset.img], f[frame][0], f[frame][1], t[2], t[3],
224                                                x - t[4], y - t[5], t[2], t[3]);
225     } else if (t) {
226       if ( x + t[2] - t[4] > 0 && y + t[3] - t[5] > 0 && x - t[4] < canvas.canvas.width && y - t[5] < canvas.canvas.height)
227       canvas.drawImage(gfx[tileset.img], t[0], t[1], t[2], t[3],
228                                  x - t[4], y - t[5], t[2], t[3]);
229     }
230   }
231 }
232
233 function Sprite(textdef) {
234   // sprite object, mostly for gfx representation
235   // may be hero, enemy, loot, npc, etc.
236   this.position = [0, 0];
237   const info = gamedata.load(textdef);
238   var direction = 0;
239   var animation = "stance";
240   var previous_animation = "";
241   var frametime = performance.now();
242   gfx.load(info.image)
243
244   // some simplified npc files do not define animation frames
245   // render size and offset are given instead, and we
246   // assume, that we can just loop horizontally over the image
247   if (!info[animation].frame){
248     info[animation].frame = [];
249     let rs = info.render_size, ro = info.render_offset;
250
251     for (let i = 0; i < info[animation].frames; i++ )
252       info[animation].frame[i] = [[ i * rs[0], 0, rs[0], rs[1], ro[0], ro[1]]];
253   }
254
255   // place sprite on x/y pixel coordinates
256   // e.g. when spawning, walking, etc.
257   this.place = function(x, y) {
258     this.position[0] = x, this.position[1] = y;
259     return this;
260   }
261
262   // change facing direction of sprite
263   this.direct = function(d) { direction = d % 8; return this; }
264
265   // set animation cycle for drawing
266   // play_once animations will automatically fall back
267   // to previous loop after completion
268   this.animate = function(a) {
269     if ( a != animation ) {
270       previous_animation = animation;
271       animation = a;
272       frametime = performance.now();
273     }
274     return this;
275   }
276
277   // draw this sprite to screen position x/y
278   this.draw = function(x, y){
279     var f, a = info[animation];
280     var f, frame = ( performance.now() - frametime ) * a.frames / a.duration | 0;
281
282     // determine current animation frame
283     switch(a.type){
284       case "looped":
285         frame = frame % a.frames;
286         break;
287       case "play_once":
288         if ( frame >= a.frames ){
289           animation = previous_animation;
290           previous_animation = "";
291           frametime = performance.now();
292           a = info[animation];
293           frame = 0;
294         }
295         break;
296       case "back_forth":
297         frame = frame % (a.frames * 2 - 2);
298         if ( frame >= a.frames ){
299          frame = a.frames - frame % a.frames - 1;
300         }
301         break;
302       default: break;
303     }
304     f = a.frame[frame][direction];
305
306     canvas.drawImage(gfx[info.image], f[0], f[1], f[2], f[3],
307                      x - f[4], y - f[5],
308                      f[2], f[3]);
309   }
310
311   // shortcut functions for specific animations
312   this.block  = () => this.animate("block" );
313   this.cast   = () => this.animate("cast"  );
314   this.die    = () => this.animate("die"   );
315   this.hit    = () => this.animate("hit"   );
316   this.run    = () => this.animate("run"   );
317   this.shoot  = () => this.animate("shoot" );
318   this.stance = () => this.animate("stance");
319   this.swing  = () => this.animate("swing" );
320 }
321
322 function Npc(textdef){
323   this.info = gamedata.load(textdef);
324   const avatar = new Sprite(this.info.gfx);
325
326   this.place = function(x, y) {
327     avatar.place(x,y);
328     return this;
329   }
330
331   this.draw = function(x, y) {
332     avatar.draw(x, y);
333     return this;
334   }
335 }
336
337 function Hero(gender = "female", hair = "short"){
338   // Object for player character
339   // unique in single player
340   this.position = [0,0];
341   this.stats = gamedata.load("engine/stats.txt");
342   const layers = gamedata.load("engine/hero_layers.txt");
343   var direction = 0, animation = "stance";
344   hair = (gender == "female")?"long":hair;
345
346   // hero consists of multiple sprites, one for each clothing/item overlay
347   var limbs = {
348     head : new Sprite("animations/avatar/"+gender+"/head_"+hair+".txt"),
349     chest: new Sprite("animations/avatar/"+gender+"/cloth_shirt.txt"),
350     hands: new Sprite("animations/avatar/"+gender+"/default_hands.txt"),
351     legs : new Sprite("animations/avatar/"+gender+"/cloth_pants.txt"),
352     feet : new Sprite("animations/avatar/"+gender+"/default_feet.txt"),
353     main : null, // main hand, e.g. melee weapon, magic weapon
354     off  : null  // off hand, e.g. shield, ranged weapon
355   }
356
357   // "dress" the player, i.e. assign clothing/item to limb
358   this.dress = function(limb, item) {
359     limbs[limb] = new Sprite("animations/avatar/"+gender+"/"+item+".txt")
360     limbs[limb].place(this.position[0], this.position[1]).direct(direction);
361     this.animate(animation);
362     return this;
363   }
364
365   // // Wrapper functions for Sprite class // //
366
367   // place all sprites beloning to hero (when spawning, walking, teleporting, etc.)
368   this.place   = function(x,y) {
369     this.position[0] = x, this.position[1] = y;
370     for (var limb in limbs) limbs[limb] && limbs[limb].place(x,y);
371     return this;
372   }
373   // change facing direction of hero (i.e. of all sprites)
374   this.direct  = function(d)   {
375     direction = d;
376     for (var limb in limbs) limbs[limb] && limbs[limb].direct(d);
377     return this;
378   }
379   // start animation cycle
380   this.animate = function(anim){
381     animation = anim;
382     for (var limb in limbs) limbs[limb] && limbs[limb].animate(anim);
383     return this;
384   }
385   this.draw    = function(x, y){
386     layers.layer[direction].forEach(limb => limbs[limb] && limbs[limb].draw(x, y));
387     return this;
388   }
389
390   // shortcuts for animation
391   this.block  = () => this.animate("block" );
392   this.cast   = () => this.animate("cast"  );
393   this.die    = () => this.animate("die"   );
394   this.hit    = () => this.animate("hit"   );
395   this.run    = () => this.animate("run"   );
396   this.shoot  = () => this.animate("shoot" );
397   this.stance = () => this.animate("stance");
398   this.swing  = () => this.animate("swing" );
399 }
400
401 function Controls(hero){
402   // processes keyboard / touch / mouse
403   // causes according player actions
404   // single player only, conrol will be assigned to server in multi player
405   var kbdmap = {
406        up: 87,    altup: 38,
407      down: 83,  altdown: 40,
408      left: 65,  altleft: 37,
409     right: 68, altright: 39,
410   }
411   var keys = [], click = [];
412
413   loadevents();
414
415   window.addEventListener("keydown", e => keys[e.keyCode] = true );
416   window.addEventListener("keyup"  , e => keys[e.keyCode] = false);
417   window.addEventListener("click", m => click = [ map.mapX(m.clientX), map.mapY(m.clientY) ] );
418   setInterval(() => input(), 33.33)
419
420   // cause player to walk, processes blocked terrain and player speed
421   // x/y are factors of speed and direction
422   //     i.e. +/-1 for diagonal movement
423   //     and  +/-1.4 for horizontal/vertical movement
424   function translate(x, y){
425     var sx = map.info.header.tilewidth  * hero.stats.speed / 33.33;
426     var sy = map.info.header.tileheight * hero.stats.speed / 33.33;
427     var dx = x * sx, hx = hero.position[0];
428     var dy = y * sy, hy = hero.position[1];
429     var f = 2.1;
430     const col = map.info.layer.find(l => l.type == "collision").data;
431
432     if (col[map.tileAt(hx + dx, hy + dy)] == 0 )
433             hero.place(hx + dx, hy + dy);
434     else if ( dy == 0 && col[map.tileAt(hx + dx / f, hy + sy / 1.5)] == 0 )
435                              hero.place(hx + dx / f, hy + sy / 1.5);
436     else if ( dy == 0 && col[map.tileAt(hx + dx / f, hy - sy / 1.5)] == 0 )
437                              hero.place(hx + dx / f, hy - sy / 1.5);
438     else if ( dx == 0 && col[map.tileAt(hx + sx / 1.5, hy + dy / f)] == 0 )
439                              hero.place(hx + sx / 1.5, hy + dy / f);
440     else if ( dx == 0 && col[map.tileAt(hx - sx / 1.5, hy + dy / f)] == 0 )
441                              hero.place(hx - sx / 1.5, hy + dy / f);
442     else player.stance();
443
444     map.center(hero.position[0], hero.position[1]);
445     triggers(hero.position[0], hero.position[1]);
446   }
447
448   // Process on_trigger events at pixel position x/y
449   function triggers(x, y) {
450     const i = map.tileAt(x,y);
451
452     if ( map.triggers[i] ) events (
453       map.triggers[i].filter( t => (!t.requires_status)     ? true : t.requires_status.find(s => qstatus[s])
454                     ).filter( t => (!t.requires_not_status) ? true : !t.requires_not_status.find(s => qstatus[s]) )
455     );
456   }
457
458   function loadevents() {
459     if ( map.info.event ) events(
460       map.info.event.filter( ev => ev.activate == "on_load"
461         ).filter( t => (!t.requires_status)     ? true : t.requires_status.find(s => qstatus[s])
462         ).filter( t => (!t.requires_not_status) ? true : !t.requires_not_status.find(s => qstatus[s]) )
463     )
464   }
465
466   function events( events ) {
467     var ev, item;
468
469     for ( ev of events ) {
470       // game status modification
471       if ( ev.set_status ) for (item of ev.set_status) {
472         qstatus[item] = true;
473       }
474       if ( ev.unset_status ) for (item of ev.unset_status) {
475         qstatus[item] = false;
476       }
477       if ( item = ev.msg ) {
478         console.log(item);
479       }
480       // intramap (i.e. teleporters)
481       if ( item = ev.intramap ){
482         hero.place( map.xOf(item[0], item[1]), map.yOf(item[0], item[1]) );
483         map.center( map.xOf(item[0], item[1]), map.yOf(item[0], item[1]) );
484       }
485       // mapmod (e.g. opening doors, activating platforms, changing terrain, ...)
486       if ( ev.mapmod ) for (item of ev.mapmod) {
487           map.info.layer.find(l => l.type == item[0]).data[item[2] * map.info.header.width + item[1]] = item[3];
488       }
489       // intermap (i.e. enter new area)
490       if ( item = ev.intermap ){
491         map = new Map(item[0]);
492         hero.place( map.xOf(item[1], item[2]), map.yOf(item[1], item[2]) );
493         map.center( map.xOf(item[1], item[2]), map.yOf(item[1], item[2]) );
494         loadevents();
495         break;  // further events would not be valid on new map
496       }
497     }
498   }
499
500   // process input and decide on according action
501   function input(){
502     // facing directions, indexed by OR of key press combinations
503     const dir   = [ -1, 0, 4, -1, 2, 1, 3, 2, 6, 7, 5, 6, -1, 0, 4, -1 ];
504     // translation speed and direction, indexed by facing direction of movement
505     const trans = [ [-1.4,0], [-1,-1], [0,-1.4], [1,-1], [1.4,0], [1,1], [0,1.4], [-1,1] ];
506     var k = 0;
507
508     // OR of direction key presses
509     k += (keys[kbdmap.left]  || keys[kbdmap.altleft])  ? 1 : 0;
510     k += (keys[kbdmap.right] || keys[kbdmap.altright]) ? 2 : 0;
511     k += (keys[kbdmap.up]    || keys[kbdmap.altup])    ? 4 : 0;
512     k += (keys[kbdmap.down]  || keys[kbdmap.altdown])  ? 8 : 0;
513
514     if (~dir[k]) {
515       hero.direct(dir[k]).run();
516       translate(trans[dir[k]][0], trans[dir[k]][1]);
517     } else hero.stance();
518
519     if (click[0]) {
520         let hx = map.tileX(hero.position[0], hero.position[1]);
521         let hy = map.tileY(hero.position[0], hero.position[1]);
522         let cx = map.tileX(click[0], click[1]);
523         let cy = map.tileY(click[0], click[1]);
524         if (   (cx == hx - 1 || cx == hx + 1 || cx == hx)
525             && (cy == hy - 1 || cy == hy + 1 || cy == hy))
526         triggers(click[0], click[1]);
527       click = [];
528     }
529   }
530 }
531
532 document.querySelector("body").setAttribute("style", "margin: 0; padding: 0;");
533 canvas = document.createElement("canvas").getContext("2d");
534 canvas.canvas.setAttribute("style", "display: block; border: 0; margin: 0; padding: 0;");
535 document.querySelector("body").appendChild(canvas.canvas);
536
537 canvas.canvas.width  = window.innerWidth;
538 canvas.canvas.height = window.innerHeight;
539 window.addEventListener("resize", function(){
540   canvas.canvas.width  = window.innerWidth;
541   canvas.canvas.height = window.innerHeight;
542 });
543
544 // player = new Sprite("animations/enemies/zombie.txt"); player.stats = { speed: 3 };
545 player = new Hero();
546 map = new Map("maps/perdition_harbor.txt");
547 player.place(map.xOf(map.info.header.hero_pos[0], map.info.header.hero_pos[1]),
548              map.yOf(map.info.header.hero_pos[0], map.info.header.hero_pos[1]));
549 player.direct(6).stance();
550
551 map.center(player.position[0], player.position[1]);
552 c = new Controls(player);
553
554 setInterval (() => map.draw([player]), 33.33 );