shoot projectiles

This commit is contained in:
2026-06-13 23:33:03 -07:00
parent 5f3ef33b05
commit b9235246c7
6 changed files with 123 additions and 26 deletions
+4
View File
@@ -13,3 +13,7 @@ MAX_TOWERS :: 32
TOWER_COST :: 50
MAX_PROJECTILES :: 64
PROJECTILE_SPEED :: 280 // pixels per second
PROJECTILE_HIT_RADIUS :: 10
+67
View File
@@ -0,0 +1,67 @@
package game
Projectile :: struct {
active: bool,
position: Vec2,
target_slot: int,
damage: int,
speed: f32,
}
acquire_projectile :: proc(world: ^World) -> ^Projectile {
for i in 0 ..< MAX_PROJECTILES {
if !world.projectiles[i].active {
return &world.projectiles[i]
}
}
return nil
}
enemy_still_on_map :: proc(world: ^World, slot: int) -> (^Enemy, bool) {
if slot < 0 || slot >= MAX_ENEMIES do return nil, false
e := &world.enemies[slot]
if !e.active do return nil, false
if len(world.path) == 0 do return nil, false
if e.path_index >= len(world.path) do return nil, false
return e, true
}
spawn_projectile :: proc(world: ^World, from: Vec2, target_slot: int, damage: int) {
p := acquire_projectile(world)
if p == nil do return
p^ = Projectile {
active = true,
position = from,
target_slot = target_slot,
damage = damage,
speed = PROJECTILE_SPEED,
}
}
update_projectiles :: proc(world: ^World, dt: f32) {
for i in 0 ..< MAX_PROJECTILES {
p := &world.projectiles[i]
if !p.active do continue
target, ok := enemy_still_on_map(world, p.target_slot)
if !ok {
p.active = false
continue
}
move_toward(&p.position, target.position, p.speed, dt)
if distance(p.position, target.position) <= PROJECTILE_HIT_RADIUS {
target.health -= p.damage
p.active = false
if target.health <= 0 {
target.active = false
push_event(world, .Enemy_Killed, gold_reward = 5)
}
}
}
}
+25 -12
View File
@@ -11,6 +11,22 @@ render_world :: proc(world: ^World) {
}
}
for t in world.towers {
rl.DrawRectangle(
i32(t.position.x - 14),
i32(t.position.y - 14),
28,
28,
rl.Color{255, 203, 0, 255},
)
}
for i in 0 ..< MAX_PROJECTILES {
p := world.projectiles[i]
if !p.active do continue
rl.DrawCircle(i32(p.position.x), i32(p.position.y), 4, rl.GOLD)
}
for i in 0 ..< MAX_ENEMIES {
e := world.enemies[i]
if !e.active do continue
@@ -24,18 +40,11 @@ render_world :: proc(world: ^World) {
)
}
for t in world.towers {
rl.DrawRectangle(
i32(t.position.x - 14),
i32(t.position.y - 14),
28,
28,
rl.Color{255, 203, 0, 255},
)
}
if (world.base_health == 0) {
rl.DrawText("**GAME OVER**", 20, 20, 20, rl.RED)
for i in 0 ..< MAX_PROJECTILES {
p := world.projectiles[i]
if !p.active do continue
rl.DrawCircle(i32(p.position.x), i32(p.position.y), 4, rl.GOLD)
}
rl.DrawText(fmt.ctprintf("Gold: %d", world.gold), 10, 10, 22, rl.BLACK) // Gold HUD
@@ -47,7 +56,11 @@ render_world :: proc(world: ^World) {
case .Build:
rl.DrawText("Build - click towers, N = start wave", 10, 98, 22, rl.BLACK)
case .Combat:
rl.DrawText("Combat!", 10, 74, 22, rl.MAROON)
rl.DrawText("Combat!", 10, 98, 22, rl.MAROON)
case .Game_Over:
rl.DrawText("GAME_OVER", 260, 280, 40, rl.RED)
case .Victory:
rl.DrawText("Victory!", 260, 280, 40, rl.GREEN)
}
}
+11 -12
View File
@@ -42,23 +42,23 @@ try_place_tower :: proc(world: ^World, gx, gy: int, kind: Tower_Kind) -> bool {
return true
}
find_target :: proc(tower: Tower, world: ^World) -> ^Enemy {
find_target_slot :: proc(tower: Tower, world: ^World) -> int {
arch := get_archetype(tower.kind)
best: ^Enemy = nil
best_slot: int = -1
best_dist: f32 = 999999
for i in 0 ..< MAX_ENEMIES {
e := &world.enemies[i]
if !e.active do continue
if e.path_index >= len(world.path) do continue
d := distance(tower.position, e.position)
if d <= arch.range && d < best_dist {
best = e
best_slot = i
best_dist = d
}
}
return best
return best_slot
}
update_towers :: proc(world: ^World, dt: f32) {
@@ -68,16 +68,15 @@ update_towers :: proc(world: ^World, dt: f32) {
if t.cooldown > 0 do continue
target := find_target(t^, world)
if target == nil do continue
slot := find_target_slot(t^, world)
if slot < 0 do continue
arch := get_archetype(t.kind)
target.health -= arch.damage
t.cooldown = arch.fire_rate
if target.health <= 0 {
target.active = false
push_event(world, .Enemy_Killed, gold_reward = 5)
switch t.kind {
case .Archer:
spawn_projectile(world, t.position, slot, arch.damage)
t.cooldown = arch.fire_rate
}
}
}
+12
View File
@@ -3,6 +3,8 @@ package game
Game_Phase :: enum {
Build,
Combat,
Game_Over,
Victory,
}
Wave_Spawner :: struct {
@@ -49,3 +51,13 @@ update_phase :: proc(world: ^World) {
}
}
check_end :: proc(world: ^World) {
if world.base_health <= 0 {
world.phase = .Game_Over
}
if world.wave >= MAX_WAVES && world.phase == .Build && wave_clear(world) {
world.phase = .Victory
}
}
+4 -2
View File
@@ -1,7 +1,5 @@
package game
import "core:fmt"
import rl "vendor:raylib"
World :: struct {
gold: int,
base_health: int,
@@ -13,6 +11,7 @@ World :: struct {
phase: Game_Phase,
wave: int,
spawner: Wave_Spawner,
projectiles: [MAX_PROJECTILES]Projectile,
}
init_world :: proc() -> World {
@@ -28,13 +27,16 @@ init_world :: proc() -> World {
// update world, never draw
update_world :: proc(world: ^World, dt: f32) {
if world.phase == .Game_Over || world.phase == .Victory do return
cmd := gather_commands(world)
execute_command(world, cmd)
update_wave(world, dt)
update_enemies(world, dt)
update_projectiles(world, dt)
update_towers(world, dt)
update_phase(world)
process_events(world)
check_end(world)
}