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 }