1 module game.entity;
2 
3 import game.level;
4 import game.renderer;
5 import game.math;
6 import game.terminal;
7 import libwasm.rt.memory;
8 import game.audio;
9 import std.range : only;
10 
11 nothrow:
12 @safe:
13 
14 struct Entity {
15   nothrow:
16   float x=0,y=0,z=0;
17   float vx=0,vy=0,vz=0;
18   float ax=0,ay=0,az=0;
19   float friction;
20   int sprite, health=5;
21   bool dead = false;
22   this(float x, float y, float z, float friction, int sprite) {
23     this.x=x;this.y=y;this.z=z;this.friction=friction;this.sprite=sprite;
24     health=5;
25   }
26   void kill() {
27     dead = true;
28   }
29 }
30 
31 int roundf(float v) {
32   if (v > 0f)
33     return cast(int)(v + 0.5f);
34   return cast(int)(v - 0.5f);
35 }
36 int truncf(float v) {
37   return cast(int)v;
38 }
39 Level.Block update(ref Level level, ref Entity entity, float elapsed) {
40     float last_x = entity.x, last_z = entity.z;
41     auto r = Level.Block.empty;
42     import std.algorithm : min;
43     auto f = min(entity.friction*elapsed, 1);
44     entity.vx += entity.ax * elapsed - entity.vx * f;
45     entity.vy += entity.ay * elapsed - entity.vy * f;
46     entity.vz += entity.az * elapsed - entity.vz * f;
47     entity.x += entity.vx * elapsed;
48     entity.y += entity.vy * elapsed;
49     entity.z += entity.vz * elapsed;
50     auto xi = entity.x.truncf;
51     if (level.collides(xi, last_z.truncf)) {
52       entity.x = last_x;
53       entity.vx = 0;
54       r = Level.Block.wall;
55     }
56     if (level.collides(xi, entity.z.truncf)) {
57       entity.z = last_z;
58       entity.vz = 0;
59       r = Level.Block.wall;
60     }
61     return r;
62 }
63 
64 void render(ref Renderer renderer, ref Entity entity) {
65     renderer.push_sprite(entity.x-1,entity.y,entity.z,entity.sprite);
66 }
67 
68 struct Spider {
69   nothrow:
70   Entity entity;
71   float animation_time = 0;
72   float select_target_counter = 0;
73   float target_x, target_z;
74   this(Entity entity) {
75     this.entity = entity;
76     target_x = entity.x;
77     target_z = entity.z;
78   }
79 	void render(ref Renderer renderer) {
80 		renderer.render(entity);
81 	}
82   void update(ref Level level, float elapsed) {
83 		auto txd = entity.x - target_x;
84 		auto tzd = entity.z - target_z;
85     auto xd = entity.x - level.player.entity.x;
86     auto zd = entity.z - level.player.entity.z;
87     auto dist = sqrt(xd * xd + zd * zd);
88 
89     select_target_counter -= elapsed;
90 
91 		// select new target after a while
92 		if (select_target_counter < 0 && dist < 64) {
93 			select_target_counter = random() * 0.5 + 0.3;
94 			target_x = level.player.entity.x;
95 			target_z = level.player.entity.z;
96 		}
97 
98 		// set velocity towards target
99 		entity.ax = abs(txd) > 2 ? (txd > 0 ? -160 : 160) : 0;
100 		entity.az = abs(tzd) > 2 ? (tzd > 0 ? -160 : 160) : 0;
101 
102 		level.update(entity, elapsed);
103 		animation_time += elapsed;
104 		entity.sprite = 27 + (cast(int)(animation_time*15f)|0)%3;
105 	}
106   void receive_damage(ref Level level, ref Entity from, int amount) {
107     entity.health -= amount;
108     if (entity.health <= 0)
109       kill(level);
110     entity.vx = from.vx;
111     entity.vz = from.vz;
112     import std.range : take;
113     foreach(p;ParticleSpawner(entity.x,entity.z).take(5))
114       level.put(p);
115   }
116   void check(Spider other) {
117     if (abs(other.entity.x - entity.x) > abs(other.entity.z - entity.z)) {
118       auto amount = entity.x > other.entity.x ? 0.6 : -0.6;
119       entity.vx += amount;
120       other.entity.vx -= amount;
121     } else {
122       auto amount = entity.z > other.entity.z ? 0.6 : -0.6;
123       entity.vz += amount;
124       other.entity.vz -= amount;
125     }
126   }
127   void check(ref Level level, ref Player other) {
128 			entity.vx *= -1.5;
129 			entity.vz *= -1.5;
130 			other.receive_damage(level, 1);
131   }
132   void kill(ref Level level) @trusted {
133 		entity.kill();
134     level.remove(&this);
135     level.put(allocator.make!Explosion(Entity(entity.x, 0, entity.z, 0, 26)));
136 		camera_shake = 1f;
137     gSoundPlayer.playExplode();
138 	}
139 }
140 
141 struct ParticleSpawner {
142   nothrow:
143   enum empty = false;
144   Particle* p;
145   float x, z;
146   void gen() @trusted {
147     p = allocator.make!Particle(Entity(this.x, 0, this.z, 1, 30));
148     p.entity.vx = (random() - 0.5) * 128;
149     p.entity.vy = random() * 96;
150     p.entity.vz = (random() - 0.5) * 128;
151   }
152   this(float x, float z) {
153     this.x = x; this.z = z;
154     gen();
155   }
156   auto front() {
157     return p;
158   }
159   auto popFront() {
160     gen();
161   }
162 }
163 
164 struct Sentry {
165   nothrow:
166   Entity entity;
167   float select_target_counter = 0;
168   float target_x, target_z;
169   this(Entity entity) {
170     this.entity = entity;
171 		target_x = entity.x;
172 		target_z = entity.z;
173 		entity.health = 20;
174 	}
175 
176   void render(ref Renderer renderer) {
177     renderer.render(entity);
178   }
179 
180 	void update(ref Level level, float elapsed) @trusted {
181 		auto txd = entity.x - target_x;
182 		auto tzd = entity.z - target_z;
183     auto xd = entity.x - level.player.entity.x;
184     auto zd = entity.z - level.player.entity.z;
185     auto dist = sqrt(xd * xd + zd * zd);
186 
187 		select_target_counter -= elapsed;
188 
189 		// select new target after a while
190 		if (select_target_counter < 0) {
191 			if (dist < 64) {
192 				select_target_counter = random() * 0.5 + 0.3;
193 				target_x = level.player.entity.x;
194 				target_z = level.player.entity.z;
195 			}
196 			if (dist < 48) {
197 				auto angle = atan2(
198 					level.player.entity.z - entity.z,
199 					level.player.entity.x - entity.x
200 				);
201         level.put(allocator.make!SentryPlasma(Entity(entity.x, 0, entity.z, 0, 26), angle + random() * 0.2 - 0.11));
202 			}
203 		}
204 		// set velocity towards target
205 		if (dist > 24) {
206 			entity.ax = abs(txd) > 2 ? (txd > 0 ? -48 : 48) : 0;
207 			entity.az = abs(tzd) > 2 ? (tzd > 0 ? -48 : 48) : 0;
208 		} else {
209 			entity.ax = entity.az = 0;
210 		}
211 
212     level.update(entity, elapsed);
213 	}
214 
215 	void receive_damage(ref Level level, Entity from, int amount) {
216     entity.health -= amount;
217     if (entity.health <= 0)
218       kill(level);
219 		entity.vx = from.vx * 0.1;
220 		entity.vz = from.vz * 0.1;
221     import std.range : take;
222     foreach(p;ParticleSpawner(entity.x,entity.z).take(3))
223       level.put(p);
224 	}
225 
226 	void kill(ref Level level) @trusted {
227     entity.kill();
228     level.remove(&this);
229     level.put(allocator.make!Explosion(Entity(entity.x, 0, entity.z, 0, 26)));
230 		camera_shake = 3f;
231     gSoundPlayer.playExplode();
232 	}
233 }
234 
235 struct SentryPlasma {
236   nothrow:
237   Entity entity;
238   this(Entity entity, float angle) {
239     this.entity = entity;
240 		enum speed = 64;
241 		this.entity.vx = cos(angle) * speed;
242 		this.entity.vz = sin(angle) * speed;
243 	}
244 
245   void update(ref Level level, float elapsed) {
246     if (level.update(entity, elapsed) == Level.Block.wall)
247       kill(level);
248   }
249 
250 	void render(ref Renderer renderer) {
251 		renderer.render(entity);
252 		renderer.push_light(entity.x, 4, entity.z + 6, 1.5, 0.2, 0.1, 0.04);
253 	}
254 
255   void kill(ref Level level) {
256     entity.kill;
257     level.remove(&this);
258   }
259 
260 	void check(ref Level level, Player other) {
261     other.receive_damage(level, 1);
262     kill(level);
263 	}
264 }
265 
266 enum PI = 3.141592654f;
267 
268 struct Player {
269   nothrow:
270   Entity entity;
271   float last_shot = 0, last_damage = 0, bob = 0;
272   int frame = 0;
273   this(Entity entity) {
274     this.entity = entity;
275 	}
276 
277 	void update(ref Level level, float elapsed, Input input, int mouseX, int mouseY) @trusted {
278 		enum speed = 128;
279 
280 		// movement
281 		entity.ax = (input & Input.Left) == Input.Left ? -speed : (input & Input.Right) ? speed : 0;
282 		entity.az = (input & Input.Up) ? -speed : (input & Input.Down) ? speed : 0;
283 
284 		// rotation - select appropriate sprite
285 		auto angle = atan2(
286                        mouseY - (-34 + 180 /*c.height*/ * 0.8),
287                        mouseX - (6 + /*camera_x +*/ 320/*c.width*/ * 0.5)
288 		);
289 		entity.sprite = 18 + cast(int)((angle / PI * 4f + 10.5f) % 8)|0;
290 
291 		// bobbing
292 		bob += elapsed * 1.75 * (abs(entity.vx) + abs(entity.vz));
293 		entity.y = sin(bob) * 0.25;
294 
295 		last_damage -= elapsed;
296 		last_shot -= elapsed;
297 
298 		if (input & Input.Shoot && last_shot < 0) {
299       // TODO
300 			gSoundPlayer.playShoot();
301       level.put(
302                 allocator.make!PlayerPlasma(Entity(entity.x, 0, entity.z, 0, 26), angle + random() * 0.2 - 0.11)
303                 );
304 			last_shot = 0.1;
305 		}
306 
307 		level.update(entity, elapsed);
308 	}
309 
310 	void render(ref Renderer renderer) {
311 		frame++;
312 		if (last_damage < 0 || frame % 6 < 4) {
313 			renderer.render(entity);
314 		}
315 		renderer.push_light(entity.x, 4, entity.z + 6, 1,0.5,0, 0.04);
316 	}
317 
318 	void kill(ref Level level) @trusted {
319     if (entity.dead)
320       return;
321 		entity.kill();
322 		entity.y = 10;
323 		entity.z += 5;
324     (*gTerminal).terminal_show_notice(only(
325                                        l(0,"DEPLOYMENT FAILED"),
326                                        l(1,"RESTORING BACKUP...")
327                                        )
328                                       );
329     setTimeout(&gGame.loadLevel, 3000);
330   }
331 
332   void receive_damage(ref Level level, int amount) {
333 		if (last_damage < 0) {
334       // TODO
335 			// audio_play(audio_sfx_hurt);
336       entity.health -= amount;
337       if (entity.health <= 0)
338         kill(level);
339 			last_damage = 2;
340 		}
341 	}
342 }
343 
344 struct PlayerPlasma {
345   nothrow:
346   Entity entity;
347   this(Entity entity, float angle) {
348     this.entity = entity;
349 		enum speed = 96;
350 		this.entity.vx = cos(angle) * speed;
351 		this.entity.vz = sin(angle) * speed;
352 	}
353   void update(ref Level level, float elapsed) {
354     if (level.update(entity, elapsed) == Level.Block.wall)
355       kill(level);
356   }
357 	void render(ref Renderer renderer) {
358 		renderer.render(entity);
359 		renderer.push_light(entity.x, 4, entity.z + 6, 0.9, 0.2, 0.1, 0.04);
360 	}
361   void kill(ref Level level) {
362     entity.kill();
363     level.remove(&this);
364   }
365   void check(ref Level level, Sentry* other) @trusted {
366     gSoundPlayer.playHit();
367     other.receive_damage(level, entity, 1);
368     kill(level);
369   }
370 	void check(ref Level level, Spider* other) @trusted {
371     gSoundPlayer.playHit();
372     other.receive_damage(level, entity, 1);
373     kill(level);
374 	}
375 }
376 
377 struct Particle {
378   nothrow:
379   Entity entity;
380   float lifetime = 3;
381   this(Entity entity) {
382     this.entity = entity;
383 	}
384 
385   void render(ref Renderer renderer) {
386     renderer.render(entity);
387   }
388 
389 	void update(ref Level level, float elapsed) {
390 		entity.ay = -320;
391 
392 		if (entity.y < 0) {
393 			entity.y = 0;
394 			entity.vy = -entity.vy * 0.96;
395 		}
396 		level.update(entity, elapsed);
397 		lifetime -= elapsed;
398 		if (lifetime < 0) {
399       entity.kill();
400       level.remove(&this);
401 		}
402 	}
403 }
404 
405 struct Health {
406   nothrow:
407   Entity entity;
408   this(Entity entity) {
409     this.entity = entity;
410 	}
411   void render(ref Renderer renderer) {
412     renderer.render(entity);
413   }
414   void kill(ref Level level) {
415     entity.kill();
416     level.remove(&this);
417   }
418 	void check(ref Level level, ref Player other) {
419     kill(level);
420     other.entity.health += other.entity.health < 5 ? 1 : 0;
421     // TODO:
422     // audio_play(audio_sfx_pickup);
423   }
424 }
425 
426 struct Explosion {
427   nothrow:
428   Entity entity;
429   float lifetime;
430   this(Entity entity) {
431     this.entity = entity;
432     lifetime = 1;
433 	}
434 
435 	void update(ref Level level, float elapsed) {
436 		lifetime -= elapsed;
437 		if (lifetime < 0) {
438 			entity.kill();
439       level.remove(&this);
440 		}
441 	}
442 
443 	void render(ref Renderer renderer) {
444     renderer.render(entity);
445 		renderer.push_light(entity.x, 4, entity.z + 6, 1,0.7,0.3, 0.08*(1-lifetime));
446 	}
447 }
448 
449 struct Cpu {
450   nothrow:
451   Entity entity;
452   float animation_time = 0f;
453   void update(ref Level level, float elapsed) {
454 		animation_time += elapsed;
455   }
456 	void render(ref Renderer renderer) {
457 		renderer.push_block(entity.x, entity.z, 4, 17);
458 		float intensity = entity.health == 5
459 			? 0.02f + sin(animation_time*10f+random()*2f) * 0.01f
460 			: 0.01f;
461 		renderer.push_light(entity.x + 4, 4, entity.z + 12, 0.2, 0.4, 1.0, intensity);
462 	}
463 
464 	void check(ref Level level, ref Player other) {
465 		if (entity.health == 5) {
466 			entity.health = 10;
467 			level.rebootCpu();
468 		}
469 	}
470 }