Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b12022b07f | ||
|
|
0d3ae58b40 | ||
|
|
6b68ce3fa1 |
@@ -1,14 +0,0 @@
|
||||
## Game
|
||||
|
||||
This is a very simple game.
|
||||
|
||||
## Objective
|
||||
|
||||
- Explore Nystrom's Game Programming patterns when appropriate.
|
||||
- Learn Odin
|
||||
- Enjoy the love of hand writing code (cuz I love it)
|
||||
|
||||
## Goals
|
||||
- Build a playable single player game
|
||||
- Build a multiplayer aspect
|
||||
- Keep things as simple as possible
|
||||
+12
-16
@@ -3,31 +3,27 @@ package game
|
||||
import rl "vendor:raylib"
|
||||
|
||||
run :: proc() {
|
||||
init_display()
|
||||
defer {
|
||||
shutdown_display()
|
||||
rl.CloseWindow()
|
||||
}
|
||||
rl.InitWindow(SCREEN_W, SCREEN_H, "Tower Defense")
|
||||
defer rl.CloseWindow()
|
||||
rl.SetTargetFPS(60)
|
||||
|
||||
world := init_world()
|
||||
defer {
|
||||
delete(world.path)
|
||||
delete(world.towers)
|
||||
delete(world.events)
|
||||
delete(world.spawner.recipe)
|
||||
}
|
||||
|
||||
for !rl.WindowShouldClose() {
|
||||
dt := rl.GetFrameTime()
|
||||
|
||||
update_display()
|
||||
update_world(&world, dt)
|
||||
|
||||
begin_game_draw()
|
||||
rl.BeginDrawing()
|
||||
rl.ClearBackground(rl.Color{34, 139, 34, 255})
|
||||
|
||||
render_world(&world)
|
||||
render_overlay(&world)
|
||||
render_controls(&world)
|
||||
end_game_draw()
|
||||
rl.EndDrawing()
|
||||
}
|
||||
|
||||
delete(world.path)
|
||||
delete(world.towers)
|
||||
delete(world.events)
|
||||
|
||||
}
|
||||
|
||||
|
||||
+6
-15
@@ -6,7 +6,6 @@ Command_Kind :: enum {
|
||||
None,
|
||||
Place_Tower,
|
||||
Start_Wave,
|
||||
Upgrade_Tower,
|
||||
}
|
||||
|
||||
Command :: struct {
|
||||
@@ -16,24 +15,18 @@ Command :: struct {
|
||||
}
|
||||
|
||||
gather_commands :: proc(world: ^World) -> Command {
|
||||
ui_cmd := poll_controls_command(world)
|
||||
if ui_cmd.kind != .None do return ui_cmd
|
||||
|
||||
if rl.IsKeyPressed(.ONE) do world.selected_tower = .Archer
|
||||
if rl.IsKeyPressed(.TWO) do world.selected_tower = .Cannon
|
||||
if rl.IsKeyPressed(.THREE) do world.selected_tower = .Ice
|
||||
if world.phase == .Build && rl.IsKeyPressed(.N) {
|
||||
return Command{kind = .Start_Wave}
|
||||
}
|
||||
|
||||
if rl.IsMouseButtonPressed(.RIGHT) {
|
||||
mouse := game_mouse()
|
||||
if world.phase == .Build && rl.IsMouseButtonPressed(.LEFT) {
|
||||
mouse := rl.GetMousePosition()
|
||||
gx, gy := screen_to_grid(mouse.x, mouse.y)
|
||||
return Command{kind = .Upgrade_Tower, gx = gx, gy = gy}
|
||||
return Command{kind = .Place_Tower, gx = gx, gy = gy}
|
||||
}
|
||||
|
||||
if world.phase == .Build && rl.IsMouseButtonPressed(.LEFT) {
|
||||
mouse := game_mouse()
|
||||
if rl.IsMouseButtonPressed(.LEFT) {
|
||||
mouse := rl.GetMousePosition()
|
||||
gx, gy := screen_to_grid(mouse.x, mouse.y)
|
||||
return Command{kind = .Place_Tower, gx = gx, gy = gy}
|
||||
}
|
||||
@@ -45,11 +38,9 @@ execute_command :: proc(world: ^World, cmd: Command) {
|
||||
switch cmd.kind {
|
||||
case .None:
|
||||
case .Place_Tower:
|
||||
try_place_tower(world, cmd.gx, cmd.gy, world.selected_tower)
|
||||
try_place_tower(world, cmd.gx, cmd.gy, .Archer)
|
||||
case .Start_Wave:
|
||||
start_wave(world)
|
||||
case .Upgrade_Tower:
|
||||
try_upgrade_tower(world, cmd.gx, cmd.gy)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-54
@@ -1,7 +1,5 @@
|
||||
package game
|
||||
|
||||
import rl "vendor:raylib"
|
||||
|
||||
MAP_W :: 30 // map width in tiles
|
||||
MAP_H :: 20 // map height in tiles
|
||||
TILE_SIZE :: 32 // size of tiles in pixels
|
||||
@@ -9,63 +7,13 @@ TILE_SIZE :: 32 // size of tiles in pixels
|
||||
SCREEN_W :: MAP_W * TILE_SIZE
|
||||
SCREEN_H :: MAP_H * TILE_SIZE
|
||||
|
||||
DEFAULT_WINDOW_W :: 1280
|
||||
DEFAULT_WINDOW_H :: 720
|
||||
|
||||
MAX_ENEMIES :: 64
|
||||
MAX_WAVES :: 5
|
||||
MAX_TOWERS :: 32
|
||||
|
||||
TOWER_COST :: 50
|
||||
|
||||
MAX_PROJECTILES :: 64
|
||||
PROJECTILE_SPEED :: 280 // pixels per second
|
||||
PROJECTILE_HIT_RADIUS :: 10
|
||||
CANNON_PROJECTILES_SPEED :: 320.0
|
||||
|
||||
OVERLAY_BAR_H :: 28
|
||||
OVERLAY_PADDING :: 6
|
||||
OVERLAY_BAR_COLOR :: rl.Color{20, 24, 20, 200}
|
||||
OVERLAY_GOLD_COLOR :: rl.Color{255, 215, 80, 255}
|
||||
OVERLAY_TEXT_COLOR :: rl.Color{240, 240, 230, 255}
|
||||
OVERLAY_FONT_SIZE :: 18
|
||||
|
||||
CONTROL_BAR_H :: 40
|
||||
CONTROL_BAR_BG :: rl.Color{30, 55, 30, 220}
|
||||
CONTROL_BUTTON_COLOR :: rl.Color{60, 120, 60, 255}
|
||||
CONTROL_BUTTON_HOVER :: rl.Color{60, 150, 60, 255}
|
||||
|
||||
CONTROLS_SHOP_IDLE :: rl.Color{45, 75, 45, 255}
|
||||
CONTROL_SHOP_SELECTED :: rl.Color{70, 110, 70, 255}
|
||||
CONTROLS_SHOP_HOVER :: rl.Color{90, 130, 90, 255}
|
||||
|
||||
ICE_SLOW_DURATION :: 2.5
|
||||
ICE_SLOW_FACTOR :: 0.5
|
||||
|
||||
ENEMY_LABEL_COLOR :: rl.Color{255, 200, 200, 255}
|
||||
|
||||
// starting values for the game
|
||||
STARTING_BASE_HEALTH :: 20
|
||||
STARTING_GOLD :: 100
|
||||
|
||||
//controls constants
|
||||
BOTTOM_BAR_Y :: SCREEN_H - CONTROL_BAR_H
|
||||
ARCHER_BUTTON_W :: 120
|
||||
START_WAVE_BUTTON_W :: 110
|
||||
CONTROLS_BUTTON_GAP :: 12
|
||||
CONTROLS_BUTTON_H :: 24
|
||||
CONTROLS_BUTTON_Y :: BOTTOM_BAR_Y + 8
|
||||
CONTROLS_BUTTON_X :: (SCREEN_W - (ARCHER_BUTTON_W + CONTROLS_BUTTON_GAP + START_WAVE_BUTTON_W)) / 2
|
||||
|
||||
ARCHER_BUTTON_RECT :: rl.Rectangle {
|
||||
f32(CONTROLS_BUTTON_X),
|
||||
f32(CONTROLS_BUTTON_Y),
|
||||
ARCHER_BUTTON_W,
|
||||
CONTROLS_BUTTON_H,
|
||||
}
|
||||
|
||||
START_WAVE_BUTTON_RECT :: rl.Rectangle {
|
||||
f32(CONTROLS_BUTTON_X + ARCHER_BUTTON_W + CONTROLS_BUTTON_GAP),
|
||||
f32(CONTROLS_BUTTON_Y),
|
||||
START_WAVE_BUTTON_W,
|
||||
CONTROLS_BUTTON_H,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
package game
|
||||
|
||||
import "core:fmt"
|
||||
import rl "vendor:raylib"
|
||||
|
||||
|
||||
poll_controls_command :: proc(world: ^World) -> Command {
|
||||
if world.phase != .Build do return Command{kind = .None}
|
||||
|
||||
mouse := game_mouse()
|
||||
if rl.IsMouseButtonPressed(.LEFT) {
|
||||
if rl.CheckCollisionPointRec(mouse, ARCHER_BUTTON_RECT) {
|
||||
world.selected_tower = .Archer
|
||||
}
|
||||
if rl.CheckCollisionPointRec(mouse, START_WAVE_BUTTON_RECT) {
|
||||
return Command{kind = .Start_Wave}
|
||||
}
|
||||
}
|
||||
|
||||
return Command{kind = .None}
|
||||
}
|
||||
|
||||
draw_archer_button :: proc(world: ^World, archer: rl.Rectangle) {
|
||||
arch := TOWER_ARCHETYPES[.Archer]
|
||||
mouse := rl.GetMousePosition()
|
||||
|
||||
arch_bg := CONTROLS_SHOP_IDLE
|
||||
if world.selected_tower == .Archer do arch_bg = CONTROL_SHOP_SELECTED
|
||||
if rl.CheckCollisionPointRec(mouse, archer) do arch_bg = CONTROLS_SHOP_HOVER
|
||||
|
||||
label := fmt.ctprintf("Archer - %dg", arch.cost)
|
||||
draw_button(archer, label, arch_bg, 14, OVERLAY_TEXT_COLOR)
|
||||
}
|
||||
|
||||
draw_start_wave_button :: proc(start_wave_rect: rl.Rectangle, mouse: rl.Vector2) {
|
||||
bg := CONTROL_BUTTON_COLOR
|
||||
if rl.CheckCollisionPointRec(mouse, start_wave_rect) do bg = CONTROL_BUTTON_HOVER
|
||||
|
||||
draw_button(start_wave_rect, "Start Wave", bg, 16, OVERLAY_TEXT_COLOR)
|
||||
}
|
||||
|
||||
draw_build_controls :: proc(world: ^World) {
|
||||
if world.phase != .Build do return
|
||||
mouse := game_mouse()
|
||||
|
||||
rl.DrawRectangle(0, BOTTOM_BAR_Y, SCREEN_W, CONTROL_BAR_H, CONTROL_BAR_BG)
|
||||
|
||||
draw_archer_button(world, ARCHER_BUTTON_RECT)
|
||||
draw_start_wave_button(START_WAVE_BUTTON_RECT, mouse)
|
||||
}
|
||||
|
||||
render_controls :: proc(world: ^World) {
|
||||
switch world.phase {
|
||||
case .Build:
|
||||
draw_build_controls(world)
|
||||
case .Combat:
|
||||
draw_combat_banner()
|
||||
case .Game_Over, .Victory:
|
||||
}
|
||||
}
|
||||
|
||||
draw_combat_banner :: proc() {
|
||||
rl.DrawRectangle(0, BOTTOM_BAR_Y, SCREEN_W, CONTROL_BAR_H, rl.Color{120, 40, 40, 200})
|
||||
|
||||
text: cstring = "Combat!"
|
||||
font: i32 = 16
|
||||
tw := rl.MeasureText(text, font)
|
||||
rl.DrawText(
|
||||
text,
|
||||
(SCREEN_W - tw) / 2,
|
||||
BOTTOM_BAR_Y + (CONTROL_BAR_H - font) / 2,
|
||||
font,
|
||||
rl.Color{255, 220, 220, 255},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
package game
|
||||
|
||||
import rl "vendor:raylib"
|
||||
|
||||
game_target: rl.RenderTexture2D
|
||||
|
||||
init_display :: proc() {
|
||||
rl.InitWindow(DEFAULT_WINDOW_W, DEFAULT_WINDOW_H, "Tower Defense")
|
||||
rl.SetWindowState({.WINDOW_RESIZABLE})
|
||||
rl.SetTargetFPS(60)
|
||||
game_target = rl.LoadRenderTexture(i32(SCREEN_W), i32(SCREEN_H))
|
||||
}
|
||||
|
||||
shutdown_display :: proc() {
|
||||
rl.UnloadRenderTexture(game_target)
|
||||
}
|
||||
|
||||
update_display :: proc() {
|
||||
if rl.IsKeyPressed(.F11) {
|
||||
rl.ToggleFullscreen()
|
||||
}
|
||||
}
|
||||
|
||||
game_viewport_dest :: proc() -> rl.Rectangle {
|
||||
sw := f32(rl.GetRenderWidth())
|
||||
sh := f32(rl.GetRenderHeight())
|
||||
scale := min(sw / f32(SCREEN_W), sh / f32(SCREEN_H))
|
||||
dw := f32(SCREEN_W) * scale
|
||||
dh := f32(SCREEN_H) * scale
|
||||
return {x = (sw - dw) * 0.5, y = (sh - dh) * 0.5, width = dw, height = dh}
|
||||
}
|
||||
|
||||
game_mouse :: proc() -> rl.Vector2 {
|
||||
m := rl.GetMousePosition()
|
||||
dest := game_viewport_dest()
|
||||
if dest.width <= 0 || dest.height <= 0 {
|
||||
return m
|
||||
}
|
||||
return {
|
||||
(m.x - dest.x) / dest.width * f32(SCREEN_W),
|
||||
(m.y - dest.y) / dest.height * f32(SCREEN_H),
|
||||
}
|
||||
}
|
||||
|
||||
begin_game_draw :: proc() {
|
||||
rl.BeginTextureMode(game_target)
|
||||
rl.ClearBackground(rl.Color{34, 139, 34, 255})
|
||||
}
|
||||
|
||||
end_game_draw :: proc() {
|
||||
rl.EndTextureMode()
|
||||
|
||||
rl.BeginDrawing()
|
||||
rl.ClearBackground(rl.BLACK)
|
||||
|
||||
dest := game_viewport_dest()
|
||||
source := rl.Rectangle{0, 0, f32(SCREEN_W), -f32(SCREEN_H)}
|
||||
rl.DrawTexturePro(game_target.texture, source, dest, {}, 0, rl.WHITE)
|
||||
|
||||
rl.EndDrawing()
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
package game
|
||||
|
||||
import "core:fmt"
|
||||
import rl "vendor:raylib"
|
||||
|
||||
draw_end_screen :: proc(world: ^World) {
|
||||
title: cstring = "GAME OVER"
|
||||
title_color := rl.RED
|
||||
sub: cstring = "Press close to quit"
|
||||
sub_color := OVERLAY_TEXT_COLOR
|
||||
|
||||
if world.phase == .Victory {
|
||||
title = "VICTORY!"
|
||||
title_color = rl.GREEN
|
||||
sub = fmt.ctprintf("You survived %d waves!", MAX_WAVES)
|
||||
}
|
||||
|
||||
|
||||
rl.DrawRectangle(0, 0, SCREEN_W, SCREEN_H, rl.Color{0, 0, 0, 140})
|
||||
|
||||
title_font: i32 = 40
|
||||
tw := rl.MeasureText(title, title_font)
|
||||
rl.DrawText(title, (SCREEN_W - tw) / 2, SCREEN_H / 2 - 50, title_font, title_color)
|
||||
|
||||
sub_font: i32 = 18
|
||||
sw := rl.MeasureText(sub, sub_font)
|
||||
rl.DrawText(sub, (SCREEN_W - sw) / 2, SCREEN_H / 2 + 10, sub_font, sub_color)
|
||||
}
|
||||
|
||||
+13
-71
@@ -1,7 +1,6 @@
|
||||
package game
|
||||
|
||||
import "core:fmt"
|
||||
import "core:sys/posix"
|
||||
import rl "vendor:raylib"
|
||||
|
||||
Enemy :: struct {
|
||||
@@ -10,55 +9,7 @@ Enemy :: struct {
|
||||
max_health: int,
|
||||
active: bool,
|
||||
path_index: int,
|
||||
base_speed: f32,
|
||||
slow_timer: f32,
|
||||
kind: Enemy_Kind,
|
||||
}
|
||||
|
||||
Enemy_Kind :: enum {
|
||||
Grunt,
|
||||
Runner,
|
||||
Tank,
|
||||
}
|
||||
|
||||
Enemy_Def :: struct {
|
||||
base_health: int,
|
||||
health_per_wave: int,
|
||||
base_speed: f32,
|
||||
speed_per_wave: f32,
|
||||
color: rl.Color,
|
||||
draw_radius: f32,
|
||||
gold_reward: int,
|
||||
}
|
||||
|
||||
ENEMY_DEF: [Enemy_Kind]Enemy_Def = {
|
||||
.Grunt = {
|
||||
base_health = 15,
|
||||
health_per_wave = 5,
|
||||
base_speed = 55,
|
||||
speed_per_wave = 5,
|
||||
color = rl.MAROON,
|
||||
draw_radius = 11,
|
||||
gold_reward = 5,
|
||||
},
|
||||
.Runner = {
|
||||
base_health = 10,
|
||||
health_per_wave = 2,
|
||||
base_speed = 95,
|
||||
speed_per_wave = 3,
|
||||
color = rl.ORANGE,
|
||||
draw_radius = 11,
|
||||
gold_reward = 5,
|
||||
},
|
||||
.Tank = {
|
||||
base_health = 50,
|
||||
health_per_wave = 12,
|
||||
base_speed = 35,
|
||||
speed_per_wave = 2,
|
||||
color = rl.DARKGREEN,
|
||||
draw_radius = 14,
|
||||
gold_reward = 10,
|
||||
},
|
||||
speed: f32,
|
||||
}
|
||||
|
||||
acquire_enemy :: proc(world: ^World) -> ^Enemy {
|
||||
@@ -72,34 +23,35 @@ acquire_enemy :: proc(world: ^World) -> ^Enemy {
|
||||
return nil
|
||||
}
|
||||
|
||||
spawn_enemy :: proc(world: ^World, kind: Enemy_Kind) {
|
||||
spawn_enemy :: proc(world: ^World, health: int, speed: f32) {
|
||||
if len(world.path) == 0 do return // don't spawn enemies if no paths exist
|
||||
|
||||
e := acquire_enemy(world) // grab a free slot
|
||||
if e == nil do return // if there are no slots, skil spawning
|
||||
|
||||
def := ENEMY_DEF[kind]
|
||||
health := def.base_health + world.wave * def.health_per_wave
|
||||
speed := def.base_speed + f32(world.wave) * def.speed_per_wave
|
||||
e^ = Enemy {
|
||||
position = world.path[0],
|
||||
health = health,
|
||||
max_health = health,
|
||||
base_speed = speed,
|
||||
slow_timer = 0,
|
||||
speed = speed,
|
||||
path_index = 1,
|
||||
kind = kind,
|
||||
active = true,
|
||||
}
|
||||
}
|
||||
|
||||
update_enemies :: proc(world: ^World, dt: f32) {
|
||||
if len(world.path) < 2 do return
|
||||
spawn_group :: proc(world: ^World, count: int) {
|
||||
for _ in 0 ..< count {
|
||||
spawn_enemy(world, 20, 60)
|
||||
}
|
||||
}
|
||||
|
||||
update_enemies :: proc(world: ^World, dt: f32) {
|
||||
for i in 0 ..< MAX_ENEMIES {
|
||||
e := &world.enemies[i]
|
||||
if !e.active do continue
|
||||
|
||||
if len(world.path) == 2 do return
|
||||
|
||||
if e.path_index >= len(world.path) {
|
||||
world.base_health -= 1
|
||||
e.active = false
|
||||
@@ -107,12 +59,7 @@ update_enemies :: proc(world: ^World, dt: f32) {
|
||||
}
|
||||
|
||||
target := world.path[e.path_index]
|
||||
move_speed := e.base_speed
|
||||
if e.slow_timer > 0 {
|
||||
move_speed *= ICE_SLOW_FACTOR
|
||||
e.slow_timer -= dt
|
||||
}
|
||||
move_toward(&e.position, target, move_speed, dt)
|
||||
move_toward(&e.position, target, e.speed, dt)
|
||||
|
||||
dx := target.x - e.position.x
|
||||
dy := target.y - e.position.y
|
||||
@@ -135,12 +82,7 @@ render_enemies :: proc(world: ^World) {
|
||||
for i in 0 ..< MAX_ENEMIES {
|
||||
e := world.enemies[i]
|
||||
if !e.active do continue
|
||||
def := ENEMY_DEF[e.kind]
|
||||
|
||||
rl.DrawCircle(i32(e.position.x), i32(e.position.y), def.draw_radius, def.color)
|
||||
if e.slow_timer > 0 {
|
||||
rl.DrawCircleLines(i32(e.position.x), i32(e.position.y), 14, rl.SKYBLUE)
|
||||
}
|
||||
rl.DrawCircle(i32(e.position.x), i32(e.position.y), 11, rl.MAROON)
|
||||
rl.DrawText(
|
||||
fmt.ctprintf("%d", e.health),
|
||||
i32(e.position.x) - 4,
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package game
|
||||
|
||||
import "core:fmt"
|
||||
import rl "vendor:raylib"
|
||||
|
||||
render_hud :: proc(world: ^World) {
|
||||
rl.DrawText(fmt.ctprintf("Gold: %d", world.gold), 10, 10, 22, rl.BLACK) // Gold HUD
|
||||
rl.DrawText(fmt.ctprintf("Base HP: %d", world.base_health), 10, 32, 22, rl.BLACK) // HP HUD
|
||||
rl.DrawText(fmt.ctprintf("Enemies: %d", active_enemy_count(world)), 10, 54, 22, rl.BLACK)
|
||||
rl.DrawText(fmt.ctprintf("Wave: %d / %d", world.wave, MAX_WAVES), 10, 76, 22, rl.BLACK)
|
||||
|
||||
switch world.phase {
|
||||
case .Build:
|
||||
rl.DrawText("Build - click towers, N = start wave", 10, 98, 22, rl.BLACK)
|
||||
case .Combat:
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
+12
-5
@@ -11,10 +11,17 @@ Map :: struct {
|
||||
tiles: [MAP_H][MAP_W]Tile_Kind,
|
||||
}
|
||||
|
||||
TILE_COLORS: [Tile_Kind]rl.Color = {
|
||||
.Blocked = {35, 95, 35, 255},
|
||||
.Path = {130, 85, 45, 255},
|
||||
.Build = {50, 130, 50, 255},
|
||||
tile_color :: proc(kind: Tile_Kind) -> rl.Color {
|
||||
switch kind {
|
||||
case .Blocked:
|
||||
return {35, 95, 35, 255}
|
||||
case .Path:
|
||||
return {130, 85, 45, 255}
|
||||
case .Build:
|
||||
return {50, 130, 50, 255}
|
||||
}
|
||||
|
||||
return rl.MAGENTA
|
||||
}
|
||||
|
||||
init_map :: proc(world_map: ^Map) {
|
||||
@@ -50,7 +57,7 @@ can_build_at :: proc(world_map: ^Map, gx, gy: int) -> bool {
|
||||
render_map :: proc(world: ^World) {
|
||||
for y in 0 ..< MAP_H {
|
||||
for x in 0 ..< MAP_W {
|
||||
c := TILE_COLORS[world.world_map.tiles[y][x]]
|
||||
c := tile_color(world.world_map.tiles[y][x])
|
||||
rl.DrawRectangle(i32(x * TILE_SIZE), i32(y * TILE_SIZE), TILE_SIZE, TILE_SIZE, c)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ screen_to_grid :: proc(px, py: f32) -> (int, int) {
|
||||
distance :: proc(a, b: Vec2) -> f32 {
|
||||
dx := b.x - a.x
|
||||
dy := b.y - a.y
|
||||
return math.sqrt(dx * dx + dy * dy)
|
||||
return math.sqrt(dx * dx + dy + dy)
|
||||
}
|
||||
|
||||
move_toward :: proc(pos: ^Vec2, target: Vec2, speed, dt: f32) {
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
package game
|
||||
|
||||
import "core:math"
|
||||
import "core:testing"
|
||||
|
||||
@(test)
|
||||
test_distance :: proc(t: ^testing.T) {
|
||||
a := Vec2 {
|
||||
x = 3,
|
||||
y = 2,
|
||||
}
|
||||
b := Vec2 {
|
||||
x = 1,
|
||||
y = 2,
|
||||
}
|
||||
|
||||
d := distance(a, b)
|
||||
|
||||
testing.expect(t, math.abs(d - 2.0) < 0.0001)
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_move_toward_already_close :: proc(t: ^testing.T) {
|
||||
pos := Vec2{0.5, 0}
|
||||
target := Vec2{0, 0}
|
||||
|
||||
move_toward(&pos, target, 10, 1.0)
|
||||
|
||||
testing.expect(t, math.abs(pos.x - 0.5) < 0.0001)
|
||||
testing.expect(t, math.abs(pos.y - 0.0) < 0.0001)
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_move_toward_reaches_target :: proc(t: ^testing.T) {
|
||||
pos := Vec2{0, 0}
|
||||
target := Vec2{3, 4}
|
||||
|
||||
move_toward(&pos, target, 100, 1.0)
|
||||
|
||||
testing.expect(t, math.abs(pos.x - 3) < 0.0001)
|
||||
testing.expect(t, math.abs(pos.y - 4) < 0.0001)
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_move_toward_partial :: proc(t: ^testing.T) {
|
||||
pos := Vec2{0, 0}
|
||||
target := Vec2{10, 0}
|
||||
|
||||
move_toward(&pos, target, 2, 1.0)
|
||||
|
||||
testing.expect(t, math.abs(pos.x - 2) < 0.0001)
|
||||
testing.expect(t, math.abs(pos.y) < 0.0001)
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_move_toward_diagonal :: proc(t: ^testing.T) {
|
||||
pos := Vec2{0, 0}
|
||||
target := Vec2{3, 4}
|
||||
|
||||
move_toward(&pos, target, 5, 0.5)
|
||||
}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
package game
|
||||
|
||||
import "core:fmt"
|
||||
import rl "vendor:raylib"
|
||||
|
||||
render_overlay :: proc(world: ^World) {
|
||||
rl.DrawRectangle(0, 0, SCREEN_W, OVERLAY_BAR_H, OVERLAY_BAR_COLOR)
|
||||
|
||||
y: i32 = (OVERLAY_BAR_H - OVERLAY_FONT_SIZE) / 2
|
||||
x: i32 = OVERLAY_PADDING
|
||||
gap: i32 : 16
|
||||
|
||||
|
||||
gold := fmt.ctprintf("Gold: %d", world.gold)
|
||||
rl.DrawText(gold, OVERLAY_PADDING, OVERLAY_PADDING, 18, OVERLAY_GOLD_COLOR)
|
||||
x += rl.MeasureText(gold, OVERLAY_FONT_SIZE) + gap
|
||||
|
||||
hp := fmt.ctprintf("Health: %d", world.base_health)
|
||||
rl.DrawText(hp, x, y, OVERLAY_FONT_SIZE, OVERLAY_TEXT_COLOR)
|
||||
x += rl.MeasureText(hp, OVERLAY_FONT_SIZE) + gap
|
||||
|
||||
enemies := fmt.ctprintf("Enemies: %d", active_enemy_count(world))
|
||||
rl.DrawText(enemies, x, y, OVERLAY_FONT_SIZE, ENEMY_LABEL_COLOR)
|
||||
x += rl.MeasureText(enemies, OVERLAY_FONT_SIZE) + gap
|
||||
|
||||
waves := fmt.ctprintf("Waves: %d / %d", world.wave, MAX_WAVES)
|
||||
rl.DrawText(waves, x, y, OVERLAY_FONT_SIZE, OVERLAY_TEXT_COLOR)
|
||||
|
||||
if world.phase == .Game_Over || world.phase == .Victory {
|
||||
draw_end_screen(world)
|
||||
}
|
||||
}
|
||||
|
||||
+24
-104
@@ -3,19 +3,11 @@ package game
|
||||
import rl "vendor:raylib"
|
||||
|
||||
Projectile :: struct {
|
||||
active: bool,
|
||||
position: Vec2,
|
||||
target_slot: int,
|
||||
damage: int,
|
||||
speed: f32,
|
||||
mode: Projectile_Mode,
|
||||
direction: Vec2,
|
||||
splash_radius: f32,
|
||||
}
|
||||
|
||||
Projectile_Mode :: enum {
|
||||
Homing, // chases target
|
||||
Ballistic, // flies in a fixed direction
|
||||
active: bool,
|
||||
position: Vec2,
|
||||
target_slot: int,
|
||||
damage: int,
|
||||
speed: f32,
|
||||
}
|
||||
|
||||
acquire_projectile :: proc(world: ^World) -> ^Projectile {
|
||||
@@ -41,87 +33,37 @@ spawn_projectile :: proc(world: ^World, from: Vec2, target_slot: int, damage: in
|
||||
p := acquire_projectile(world)
|
||||
if p == nil do return
|
||||
|
||||
p.mode = .Homing
|
||||
p^ = Projectile {
|
||||
active = true,
|
||||
position = from,
|
||||
target_slot = target_slot,
|
||||
damage = damage,
|
||||
speed = PROJECTILE_SPEED,
|
||||
splash_radius = 0,
|
||||
active = true,
|
||||
position = from,
|
||||
target_slot = target_slot,
|
||||
damage = damage,
|
||||
speed = PROJECTILE_SPEED,
|
||||
}
|
||||
}
|
||||
|
||||
spawn_ballistic :: proc(world: ^World, from, dir: Vec2, damage: int, splash: f32) {
|
||||
p := acquire_projectile(world)
|
||||
if p == nil do return
|
||||
p^ = Projectile {
|
||||
active = true,
|
||||
position = from,
|
||||
direction = dir,
|
||||
mode = .Ballistic,
|
||||
damage = damage,
|
||||
speed = CANNON_PROJECTILES_SPEED,
|
||||
splash_radius = splash,
|
||||
}
|
||||
}
|
||||
|
||||
hit_enemy_at :: proc(world: ^World, pos: Vec2, damage: int) -> bool {
|
||||
for i in 0 ..< MAX_ENEMIES {
|
||||
e := &world.enemies[i]
|
||||
if !e.active do continue
|
||||
if distance(pos, e.position) <= PROJECTILE_HIT_RADIUS {
|
||||
apply_tower_hit(world, e, damage)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
update_projectiles :: proc(world: ^World, dt: f32) {
|
||||
for i in 0 ..< MAX_PROJECTILES {
|
||||
p := &world.projectiles[i]
|
||||
if !p.active do continue
|
||||
|
||||
switch p.mode {
|
||||
case .Homing:
|
||||
update_homing_projectile(world, p, dt)
|
||||
case .Ballistic:
|
||||
update_ballistic_projectile(world, p, dt)
|
||||
target, ok := enemy_still_on_map(world, p.target_slot)
|
||||
if !ok {
|
||||
p.active = false
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
update_homing_projectile :: proc(world: ^World, p: ^Projectile, dt: f32) {
|
||||
target, ok := enemy_still_on_map(world, p.target_slot)
|
||||
if !ok {
|
||||
p.active = false
|
||||
return
|
||||
}
|
||||
move_toward(&p.position, target.position, p.speed, dt)
|
||||
|
||||
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 distance(p.position, target.position) <= PROJECTILE_HIT_RADIUS {
|
||||
p.active = false
|
||||
apply_tower_hit(world, target, p.damage)
|
||||
}
|
||||
}
|
||||
|
||||
update_ballistic_projectile :: proc(world: ^World, p: ^Projectile, dt: f32) {
|
||||
step := p.speed * dt
|
||||
p.position.x += p.direction.x * step
|
||||
p.position.y += p.direction.y * step
|
||||
|
||||
if apply_splash_damage(world, p.position, p.damage, p.splash_radius) {
|
||||
p.active = false
|
||||
return
|
||||
}
|
||||
|
||||
if p.position.x < 0 ||
|
||||
p.position.y < 0 ||
|
||||
p.position.x > MAP_W * TILE_SIZE ||
|
||||
p.position.y > MAP_H * TILE_SIZE {
|
||||
p.active = false
|
||||
if target.health <= 0 {
|
||||
target.active = false
|
||||
push_event(world, .Enemy_Killed, gold_reward = 5)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,29 +71,7 @@ render_projectiles :: proc(world: ^World) {
|
||||
for i in 0 ..< MAX_PROJECTILES {
|
||||
p := world.projectiles[i]
|
||||
if !p.active do continue
|
||||
color := rl.GOLD
|
||||
radius: f32 = 4
|
||||
if p.mode == .Ballistic {
|
||||
color = rl.GRAY
|
||||
radius = 6
|
||||
}
|
||||
rl.DrawCircle(i32(p.position.x), i32(p.position.y), radius, color)
|
||||
rl.DrawCircle(i32(p.position.x), i32(p.position.y), 4, rl.GOLD)
|
||||
}
|
||||
}
|
||||
|
||||
apply_splash_damage :: proc(world: ^World, center: Vec2, damage: int, radius: f32) -> bool {
|
||||
if radius <= 0 do return hit_enemy_at(world, center, damage)
|
||||
|
||||
hit_any := false
|
||||
for i in 0 ..< MAX_ENEMIES {
|
||||
e := &world.enemies[i]
|
||||
if !e.active do continue
|
||||
if distance(center, e.position) <= radius {
|
||||
apply_tower_hit(world, e, damage)
|
||||
hit_any = true
|
||||
}
|
||||
}
|
||||
|
||||
return hit_any
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ render_world :: proc(world: ^World) {
|
||||
render_map(world)
|
||||
render_enemies(world)
|
||||
render_towers(world)
|
||||
render_placement_preview(world)
|
||||
render_projectiles(world)
|
||||
render_hud(world)
|
||||
}
|
||||
|
||||
|
||||
+102
-111
@@ -1,91 +1,125 @@
|
||||
package game
|
||||
|
||||
import "core:fmt"
|
||||
import rl "vendor:raylib"
|
||||
|
||||
Tower :: struct {
|
||||
upgrade_level: int,
|
||||
position: Vec2,
|
||||
kind: Tower_Kind,
|
||||
cooldown: f32,
|
||||
anchor_gx: int,
|
||||
anchor_gy: int,
|
||||
position: Vec2,
|
||||
kind: Tower_Kind,
|
||||
cooldown: f32,
|
||||
anchor_gx: int,
|
||||
anchor_gy: int,
|
||||
}
|
||||
|
||||
Tower_Kind :: enum {
|
||||
Archer,
|
||||
Cannon,
|
||||
Ice,
|
||||
}
|
||||
|
||||
Tower_Archetype :: struct {
|
||||
range: f32,
|
||||
damage: int,
|
||||
fire_rate: f32,
|
||||
cost: int,
|
||||
footprint_w: int, // 1 column wide
|
||||
footprint_h: int, // two rows tall
|
||||
max_upgrade: int,
|
||||
upgrade_cost: int,
|
||||
damage_per_level: int,
|
||||
range_per_level: f32,
|
||||
splash_radius: f32,
|
||||
color: rl.Color,
|
||||
kind: Tower_Kind,
|
||||
range: f32,
|
||||
damage: int,
|
||||
fire_rate: f32,
|
||||
cost: int,
|
||||
footprint_w: int, // 1 column wide
|
||||
footprint_h: int, // two rows tall
|
||||
}
|
||||
|
||||
TOWER_ARCHETYPES: [Tower_Kind]Tower_Archetype = {
|
||||
.Archer = {
|
||||
range = 100,
|
||||
kind = .Archer,
|
||||
range = 50,
|
||||
damage = 12,
|
||||
fire_rate = 0.45,
|
||||
cost = 50,
|
||||
cost = TOWER_COST,
|
||||
footprint_w = 1,
|
||||
footprint_h = 2,
|
||||
max_upgrade = 1,
|
||||
upgrade_cost = 40,
|
||||
damage_per_level = 3,
|
||||
range_per_level = 8,
|
||||
splash_radius = 0,
|
||||
color = rl.YELLOW,
|
||||
},
|
||||
.Cannon = {
|
||||
range = 110,
|
||||
damage = 35,
|
||||
fire_rate = 1.2,
|
||||
cost = 80,
|
||||
footprint_w = 2,
|
||||
footprint_h = 2,
|
||||
max_upgrade = 1,
|
||||
upgrade_cost = 40,
|
||||
damage_per_level = 3,
|
||||
range_per_level = 8,
|
||||
splash_radius = 60,
|
||||
color = rl.DARKGRAY,
|
||||
},
|
||||
.Ice = {
|
||||
range = 85,
|
||||
damage = 6,
|
||||
fire_rate = 0.55,
|
||||
cost = 65,
|
||||
footprint_w = 1,
|
||||
footprint_h = 1,
|
||||
max_upgrade = 1,
|
||||
upgrade_cost = 40,
|
||||
damage_per_level = 3,
|
||||
range_per_level = 8,
|
||||
splash_radius = 0,
|
||||
color = rl.DARKBLUE,
|
||||
},
|
||||
}
|
||||
|
||||
tower_stats_at_level :: proc(tower: Tower) -> (damage: int, range: f32) {
|
||||
arch := TOWER_ARCHETYPES[tower.kind]
|
||||
damage = arch.damage + tower.upgrade_level * arch.damage_per_level
|
||||
range = arch.range + f32(tower.upgrade_level) * arch.range_per_level
|
||||
return
|
||||
rects_overlap :: proc(
|
||||
left_a, top_a, width_a, height_a: int,
|
||||
left_b, top_b, width_b, height_b: int,
|
||||
) -> bool {
|
||||
return(
|
||||
left_a < left_b + width_b &&
|
||||
left_a + width_a > left_b &&
|
||||
top_a < top_b + height_b &&
|
||||
top_a + height_a > top_b \
|
||||
)
|
||||
}
|
||||
|
||||
footprint_center :: proc(gx, gy, footprint_w, footprint_h: int) -> Vec2 {
|
||||
return Vec2 {
|
||||
f32(gx * TILE_SIZE + (footprint_w * TILE_SIZE) / 2),
|
||||
f32(gy * TILE_SIZE + (footprint_h * TILE_SIZE) / 2),
|
||||
}
|
||||
}
|
||||
|
||||
footprint_fits_map :: proc(world_map: ^Map, gx, gy, footprint_w, footprint_h: int) -> bool {
|
||||
if gx < 0 || gy < 0 do return false
|
||||
if gx + footprint_w > MAP_W do return false
|
||||
if gy + footprint_h > MAP_H do return false
|
||||
|
||||
for row in gy ..< gy + footprint_h {
|
||||
for column in gx ..< gx + footprint_w {
|
||||
if !can_build_at(world_map, column, row) do return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
footprint_blocked :: proc(world: ^World, gx, gy, footprint_w, footprint_h: int) -> bool {
|
||||
for placed_tower in world.towers {
|
||||
archetype := get_archetype(placed_tower.kind)
|
||||
if rects_overlap(
|
||||
gx,
|
||||
gy,
|
||||
footprint_w,
|
||||
footprint_h,
|
||||
placed_tower.anchor_gx,
|
||||
placed_tower.anchor_gy,
|
||||
archetype.footprint_w,
|
||||
archetype.footprint_h,
|
||||
) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
get_archetype :: proc(kind: Tower_Kind) -> Tower_Archetype {
|
||||
return TOWER_ARCHETYPES[kind]
|
||||
}
|
||||
|
||||
try_place_tower :: proc(world: ^World, gx, gy: int, kind: Tower_Kind) -> bool {
|
||||
if world.phase != .Build do return false
|
||||
|
||||
arch := get_archetype(kind)
|
||||
footprint_w := arch.footprint_w
|
||||
footprint_h := arch.footprint_h
|
||||
|
||||
if !footprint_fits_map(&world.world_map, gx, gy, footprint_w, footprint_h) do return false
|
||||
if footprint_blocked(world, gx, gy, footprint_w, footprint_h) do return false
|
||||
if world.gold < arch.cost do return false
|
||||
|
||||
world.gold -= arch.cost
|
||||
append(
|
||||
&world.towers,
|
||||
Tower {
|
||||
position = footprint_center(gx, gy, footprint_w, footprint_h),
|
||||
anchor_gx = gx,
|
||||
anchor_gy = gy,
|
||||
kind = kind,
|
||||
cooldown = 0,
|
||||
},
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
find_target_slot :: proc(tower: Tower, world: ^World) -> int {
|
||||
arch := get_archetype(tower.kind)
|
||||
best_slot: int = -1
|
||||
best_dist: f32 = 999999
|
||||
|
||||
@@ -95,8 +129,7 @@ find_target_slot :: proc(tower: Tower, world: ^World) -> int {
|
||||
if e.path_index >= len(world.path) do continue
|
||||
|
||||
d := distance(tower.position, e.position)
|
||||
_, range := tower_stats_at_level(tower)
|
||||
if d <= range && d < best_dist {
|
||||
if d <= arch.range && d < best_dist {
|
||||
best_slot = i
|
||||
best_dist = d
|
||||
}
|
||||
@@ -114,70 +147,28 @@ update_towers :: proc(world: ^World, dt: f32) {
|
||||
slot := find_target_slot(t^, world)
|
||||
if slot < 0 do continue
|
||||
|
||||
arch := TOWER_ARCHETYPES[t.kind]
|
||||
arch := get_archetype(t.kind)
|
||||
|
||||
damage, _ := tower_stats_at_level(t^)
|
||||
switch t.kind {
|
||||
case .Archer:
|
||||
spawn_projectile(world, t.position, slot, damage)
|
||||
case .Cannon:
|
||||
if target := enemy_at_slot(world, slot); target != nil {
|
||||
len := distance(t.position, target.position)
|
||||
if (len > 0.001) {
|
||||
dir := Vec2 {
|
||||
(target.position.x - t.position.x) / len,
|
||||
(target.position.y - t.position.y) / len,
|
||||
}
|
||||
spawn_ballistic(world, t.position, dir, damage, arch.splash_radius)
|
||||
}
|
||||
}
|
||||
case .Ice:
|
||||
if target := enemy_at_slot(world, slot); target != nil {
|
||||
apply_ice_hit(world, target, damage)
|
||||
}
|
||||
spawn_projectile(world, t.position, slot, arch.damage)
|
||||
t.cooldown = arch.fire_rate
|
||||
}
|
||||
t.cooldown = arch.fire_rate
|
||||
}
|
||||
}
|
||||
|
||||
apply_tower_hit :: proc(world: ^World, target: ^Enemy, damage: int) {
|
||||
target.health -= damage
|
||||
if target.health <= 0 {
|
||||
target.active = false
|
||||
push_event(world, .Enemy_Killed, gold_reward = ENEMY_DEF[target.kind].gold_reward)
|
||||
}
|
||||
}
|
||||
|
||||
apply_ice_hit :: proc(world: ^World, target: ^Enemy, damage: int) {
|
||||
apply_tower_hit(world, target, damage)
|
||||
if target.active do target.slow_timer = ICE_SLOW_DURATION
|
||||
}
|
||||
|
||||
enemy_at_slot :: proc(world: ^World, slot: int) -> ^Enemy {
|
||||
if slot < 0 || slot >= MAX_ENEMIES do return nil
|
||||
|
||||
e := &world.enemies[slot]
|
||||
if !e.active do return nil
|
||||
return e
|
||||
}
|
||||
|
||||
render_towers :: proc(world: ^World) {
|
||||
for t in world.towers {
|
||||
archetype := TOWER_ARCHETYPES[t.kind]
|
||||
archetype := get_archetype(t.kind)
|
||||
pixel_width := f32(archetype.footprint_w * TILE_SIZE)
|
||||
pixel_height := f32(archetype.footprint_h * TILE_SIZE)
|
||||
color := archetype.color
|
||||
rl.DrawRectangle(
|
||||
i32(t.position.x - pixel_width * 0.5),
|
||||
i32(t.position.y - pixel_height * 0.5),
|
||||
i32(pixel_width),
|
||||
i32(pixel_height),
|
||||
color,
|
||||
rl.Color{255, 203, 0, 255},
|
||||
)
|
||||
if t.upgrade_level > 0 {
|
||||
label := fmt.ctprintf("Lv%d", t.upgrade_level + 1) // or just t.upgrade_level
|
||||
rl.DrawText(label, i32(t.position.x), i32(t.position.y), 14, rl.WHITE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
package game
|
||||
|
||||
import rl "vendor:raylib"
|
||||
|
||||
can_place_tower :: proc(world: ^World, gx, gy: int, kind: Tower_Kind) -> bool {
|
||||
if world.phase != .Build do return false
|
||||
arch := TOWER_ARCHETYPES[kind]
|
||||
if world.gold < arch.cost do return false
|
||||
if !footprint_fits_map(&world.world_map, gx, gy, arch.footprint_w, arch.footprint_h) do return false
|
||||
if footprint_blocked(world, gx, gy, arch.footprint_w, arch.footprint_h) do return false
|
||||
return true
|
||||
}
|
||||
|
||||
rects_overlap :: proc(
|
||||
left_a, top_a, width_a, height_a: int,
|
||||
left_b, top_b, width_b, height_b: int,
|
||||
) -> bool {
|
||||
return(
|
||||
left_a < left_b + width_b &&
|
||||
left_a + width_a > left_b &&
|
||||
top_a < top_b + height_b &&
|
||||
top_a + height_a > top_b \
|
||||
)
|
||||
}
|
||||
|
||||
footprint_center :: proc(gx, gy, footprint_w, footprint_h: int) -> Vec2 {
|
||||
return Vec2 {
|
||||
f32(gx * TILE_SIZE + (footprint_w * TILE_SIZE) / 2),
|
||||
f32(gy * TILE_SIZE + (footprint_h * TILE_SIZE) / 2),
|
||||
}
|
||||
}
|
||||
|
||||
footprint_fits_map :: proc(world_map: ^Map, gx, gy, footprint_w, footprint_h: int) -> bool {
|
||||
if gx < 0 || gy < 0 do return false
|
||||
if gx + footprint_w > MAP_W do return false
|
||||
if gy + footprint_h > MAP_H do return false
|
||||
|
||||
for column in 0 ..< footprint_h {
|
||||
for row in 0 ..< footprint_w {
|
||||
cell_column := gx + column
|
||||
cell_row := gy + row
|
||||
if !can_build_at(world_map, cell_column, cell_row) do return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
footprint_blocked :: proc(world: ^World, gx, gy, footprint_w, footprint_h: int) -> bool {
|
||||
for placed_tower in world.towers {
|
||||
archetype := TOWER_ARCHETYPES[placed_tower.kind]
|
||||
if rects_overlap(
|
||||
gx,
|
||||
gy,
|
||||
footprint_w,
|
||||
footprint_h,
|
||||
placed_tower.anchor_gx,
|
||||
placed_tower.anchor_gy,
|
||||
archetype.footprint_w,
|
||||
archetype.footprint_h,
|
||||
) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
try_place_tower :: proc(world: ^World, gx, gy: int, kind: Tower_Kind) -> bool {
|
||||
if !can_place_tower(world, gx, gy, kind) do return false
|
||||
|
||||
arch := TOWER_ARCHETYPES[kind]
|
||||
world.gold -= arch.cost
|
||||
append(
|
||||
&world.towers,
|
||||
Tower {
|
||||
position = footprint_center(gx, gy, arch.footprint_w, arch.footprint_h),
|
||||
anchor_gx = gx,
|
||||
anchor_gy = gy,
|
||||
kind = kind,
|
||||
cooldown = 0,
|
||||
},
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
tower_at_grid :: proc(world: ^World, gx, gy: int) -> ^Tower {
|
||||
for i in 0 ..< len(world.towers) {
|
||||
t := &world.towers[i]
|
||||
arch := TOWER_ARCHETYPES[t.kind]
|
||||
if gx >= t.anchor_gx &&
|
||||
gx < t.anchor_gx + arch.footprint_w &&
|
||||
gy >= t.anchor_gy &&
|
||||
gy < t.anchor_gy + arch.footprint_h {
|
||||
return t
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
try_upgrade_tower :: proc(world: ^World, gx, gy: int) -> bool {
|
||||
if world.phase != .Build do return false
|
||||
t := tower_at_grid(world, gx, gy)
|
||||
if t == nil do return false
|
||||
|
||||
arch := TOWER_ARCHETYPES[t.kind]
|
||||
if t.upgrade_level >= arch.max_upgrade do return false
|
||||
|
||||
cost := arch.upgrade_cost + t.upgrade_level * 25
|
||||
if world.gold < cost do return false
|
||||
|
||||
world.gold -= cost
|
||||
t.upgrade_level += 1
|
||||
return true
|
||||
}
|
||||
|
||||
render_placement_preview :: proc(world: ^World) {
|
||||
if world.phase != .Build do return
|
||||
|
||||
mouse := game_mouse()
|
||||
gx, gy := screen_to_grid(mouse.x, mouse.y)
|
||||
arch := TOWER_ARCHETYPES[world.selected_tower]
|
||||
ok := can_place_tower(world, gx, gy, world.selected_tower)
|
||||
|
||||
center := footprint_center(gx, gy, arch.footprint_w, arch.footprint_h)
|
||||
rl.DrawCircleLines(i32(center.x), i32(center.y), arch.range, rl.Fade(rl.SKYBLUE, 0.4))
|
||||
|
||||
tint := rl.Fade(rl.GREEN, 0.3)
|
||||
if !ok do tint = rl.Fade(rl.RED, 0.35)
|
||||
|
||||
for row in 0 ..< arch.footprint_h {
|
||||
for col in 0 ..< arch.footprint_w {
|
||||
px := (gx + col) * TILE_SIZE
|
||||
py := (gy + row) * TILE_SIZE
|
||||
rl.DrawRectangle(i32(px), i32(py), TILE_SIZE, TILE_SIZE, tint)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
package game
|
||||
|
||||
import rl "vendor:raylib"
|
||||
|
||||
draw_text_centered_in_rect :: proc(
|
||||
text: cstring,
|
||||
rect: rl.Rectangle,
|
||||
font_size: i32,
|
||||
color: rl.Color,
|
||||
) {
|
||||
text_w := rl.MeasureText(text, font_size)
|
||||
x := i32(rect.x) + (i32(rect.width) - text_w) / 2
|
||||
y := i32(rect.y) + (i32(rect.height) - font_size) / 2
|
||||
rl.DrawText(text, x, y, font_size, color)
|
||||
}
|
||||
|
||||
draw_button :: proc(
|
||||
rect: rl.Rectangle,
|
||||
label: cstring,
|
||||
bg: rl.Color,
|
||||
font_size: i32,
|
||||
text_color: rl.Color,
|
||||
) {
|
||||
rl.DrawRectangleRec(rect, bg)
|
||||
draw_text_centered_in_rect(label, rect, font_size, text_color)
|
||||
}
|
||||
|
||||
+21
-70
@@ -8,93 +8,44 @@ Game_Phase :: enum {
|
||||
}
|
||||
|
||||
Wave_Spawner :: struct {
|
||||
spawn_timer: f32,
|
||||
spawn_interval: f32,
|
||||
recipe: [dynamic]Wave_Entry,
|
||||
recipe_index: int,
|
||||
entry_remaining: int,
|
||||
enemies_to_spawn: int,
|
||||
spawn_timer: f32,
|
||||
spawn_interval: f32,
|
||||
}
|
||||
|
||||
start_wave :: proc(world: ^World) {
|
||||
if world.phase != .Build do return
|
||||
world.wave += 1
|
||||
world.phase = .Combat
|
||||
delete(world.spawner.recipe)
|
||||
world.spawner = Wave_Spawner {
|
||||
spawn_timer = 0,
|
||||
spawn_interval = 0.55,
|
||||
recipe = build_wave_recipe(world.wave),
|
||||
recipe_index = 0,
|
||||
entry_remaining = 0,
|
||||
enemies_to_spawn = 5 + world.wave * 2,
|
||||
spawn_timer = 0,
|
||||
spawn_interval = 0.55,
|
||||
}
|
||||
load_next_recipe_entry(world)
|
||||
}
|
||||
|
||||
Wave_Entry :: struct {
|
||||
kind: Enemy_Kind,
|
||||
count: int,
|
||||
}
|
||||
|
||||
build_wave_recipe :: proc(wave: int) -> [dynamic]Wave_Entry {
|
||||
recipe: [dynamic]Wave_Entry
|
||||
switch wave {
|
||||
case 1:
|
||||
append(&recipe, Wave_Entry{.Grunt, 6})
|
||||
case 2:
|
||||
append(&recipe, Wave_Entry{.Grunt, 4})
|
||||
append(&recipe, Wave_Entry{.Runner, 2})
|
||||
case:
|
||||
grunt := 3 + wave
|
||||
runners := wave / 2
|
||||
tanks := wave / 3
|
||||
append(&recipe, Wave_Entry{.Grunt, grunt})
|
||||
if runners > 0 do append(&recipe, Wave_Entry{.Runner, runners})
|
||||
if tanks > 0 do append(&recipe, Wave_Entry{.Tank, tanks})
|
||||
}
|
||||
|
||||
return recipe
|
||||
}
|
||||
|
||||
load_next_recipe_entry :: proc(world: ^World) {
|
||||
s := &world.spawner
|
||||
if s.recipe_index >= len(s.recipe) {
|
||||
s.entry_remaining = 0
|
||||
return
|
||||
}
|
||||
entry := s.recipe[s.recipe_index]
|
||||
s.entry_remaining = entry.count
|
||||
}
|
||||
|
||||
clear_wave :: proc(world: ^World) -> bool {
|
||||
s := world.spawner
|
||||
if s.recipe_index < len(s.recipe) do return false
|
||||
if s.entry_remaining > 0 do return false
|
||||
return active_enemy_count(world) == 0
|
||||
}
|
||||
|
||||
update_wave :: proc(world: ^World, dt: f32) {
|
||||
if world.phase != .Combat do return
|
||||
s := &world.spawner
|
||||
if s.entry_remaining <= 0 && s.recipe_index >= len(s.recipe) do return
|
||||
if world.spawner.enemies_to_spawn <= 0 do return
|
||||
if len(world.path) < 2 do return
|
||||
|
||||
s.spawn_timer -= dt
|
||||
if s.spawn_timer > 0 do return
|
||||
|
||||
if s.entry_remaining <= 0 {
|
||||
s.recipe_index += 1
|
||||
load_next_recipe_entry(world)
|
||||
if s.entry_remaining <= 0 do return
|
||||
world.spawner.spawn_timer -= dt
|
||||
if world.spawner.spawn_timer <= 0 {
|
||||
health := 15 + world.wave * 5
|
||||
speed := 55 + f32(world.wave) * 5
|
||||
spawn_enemy(world, health, speed)
|
||||
world.spawner.enemies_to_spawn -= 1
|
||||
world.spawner.spawn_timer = world.spawner.spawn_interval
|
||||
}
|
||||
}
|
||||
|
||||
kind := s.recipe[s.recipe_index].kind
|
||||
spawn_enemy(world, kind)
|
||||
s.entry_remaining -= 1
|
||||
s.spawn_timer = s.spawn_interval
|
||||
|
||||
wave_clear :: proc(world: ^World) -> bool {
|
||||
if world.spawner.enemies_to_spawn > 0 do return false
|
||||
return active_enemy_count(world) == 0
|
||||
}
|
||||
|
||||
update_phase :: proc(world: ^World) {
|
||||
if world.phase == .Combat && clear_wave(world) {
|
||||
if world.phase == .Combat && wave_clear(world) {
|
||||
world.phase = .Build
|
||||
push_event(world, .Wave_Survived, gold_reward = 25)
|
||||
}
|
||||
@@ -105,7 +56,7 @@ check_end :: proc(world: ^World) {
|
||||
world.phase = .Game_Over
|
||||
}
|
||||
|
||||
if world.wave >= MAX_WAVES && world.phase == .Build && clear_wave(world) {
|
||||
if world.wave >= MAX_WAVES && world.phase == .Build && wave_clear(world) {
|
||||
world.phase = .Victory
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
package game
|
||||
|
||||
import "core:testing"
|
||||
|
||||
@(test)
|
||||
test_start_wave_enters_combat :: proc(t: ^testing.T) {
|
||||
world := World {
|
||||
phase = .Combat,
|
||||
}
|
||||
world.wave = 0
|
||||
|
||||
start_wave(&world)
|
||||
|
||||
testing.expect(t, world.phase == .Combat)
|
||||
testing.expect(t, world.wave == 0)
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_start_wave_from_build :: proc(t: ^testing.T) {
|
||||
world := World {
|
||||
phase = .Build,
|
||||
}
|
||||
world.wave = 0
|
||||
|
||||
start_wave(&world)
|
||||
defer delete(world.spawner.recipe)
|
||||
|
||||
testing.expect(t, world.phase == .Combat)
|
||||
testing.expect(t, world.wave == 1)
|
||||
testing.expect(t, world.spawner.spawn_timer == 0)
|
||||
testing.expect(t, world.spawner.spawn_interval == 0.55)
|
||||
testing.expect(t, world.spawner.recipe_index == 0)
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_clear_wave_not_cleared :: proc(t: ^testing.T) {
|
||||
world := World{}
|
||||
world.spawner.recipe = make([dynamic]Wave_Entry, context.allocator)
|
||||
append(&world.spawner.recipe, Wave_Entry{})
|
||||
defer delete(world.spawner.recipe)
|
||||
world.spawner.recipe_index = 0
|
||||
world.spawner.entry_remaining = 0
|
||||
|
||||
result := clear_wave(&world)
|
||||
|
||||
testing.expect(t, result == false)
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_clear_wave_cleared :: proc(t: ^testing.T) {
|
||||
world := World{}
|
||||
world.spawner.recipe = make([dynamic]Wave_Entry, context.allocator)
|
||||
append(&world.spawner.recipe, Wave_Entry{})
|
||||
defer delete(world.spawner.recipe)
|
||||
world.spawner.recipe_index = 1
|
||||
world.spawner.entry_remaining = 0
|
||||
|
||||
result := clear_wave(&world)
|
||||
|
||||
testing.expect(t, result == true)
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_update_wave_ignores_build_phase :: proc(t: ^testing.T) {
|
||||
world := init_world()
|
||||
defer delete(world.path)
|
||||
world.phase = .Build
|
||||
world.spawner.spawn_timer = 0
|
||||
world.spawner.entry_remaining = 5
|
||||
|
||||
update_wave(&world, 1.0)
|
||||
|
||||
testing.expect(t, active_enemy_count(&world) == 0)
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_update_wave_waits_for_spawn_timer :: proc(t: ^testing.T) {
|
||||
world := init_world()
|
||||
defer delete(world.path)
|
||||
world.phase = .Combat
|
||||
world.spawner.recipe = make([dynamic]Wave_Entry, context.allocator)
|
||||
append(&world.spawner.recipe, Wave_Entry{.Grunt, 1})
|
||||
defer delete(world.spawner.recipe)
|
||||
world.spawner.recipe_index = 0
|
||||
world.spawner.entry_remaining = 1
|
||||
world.spawner.spawn_timer = 0.5
|
||||
|
||||
update_wave(&world, 0.1)
|
||||
|
||||
testing.expect(t, active_enemy_count(&world) == 0)
|
||||
testing.expect(t, world.spawner.spawn_timer == 0.4)
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_update_wave_spawns_and_resets_timer :: proc(t: ^testing.T) {
|
||||
world := init_world()
|
||||
defer delete(world.path)
|
||||
world.phase = .Combat
|
||||
world.wave = 1
|
||||
world.spawner.recipe = make([dynamic]Wave_Entry, context.allocator)
|
||||
append(&world.spawner.recipe, Wave_Entry{.Grunt, 2})
|
||||
defer delete(world.spawner.recipe)
|
||||
world.spawner.recipe_index = 0
|
||||
world.spawner.entry_remaining = 2
|
||||
world.spawner.spawn_timer = 0
|
||||
world.spawner.spawn_interval = 0.55
|
||||
|
||||
update_wave(&world, 0.55)
|
||||
|
||||
testing.expect(t, active_enemy_count(&world) == 1)
|
||||
testing.expect(t, world.spawner.entry_remaining == 1)
|
||||
testing.expect(t, world.spawner.spawn_timer == 0.55)
|
||||
|
||||
found_grunt := false
|
||||
for i in 0 ..< MAX_ENEMIES {
|
||||
if world.enemies[i].active {
|
||||
testing.expect(t, world.enemies[i].kind == .Grunt)
|
||||
found_grunt = true
|
||||
}
|
||||
}
|
||||
testing.expect(t, found_grunt)
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_update_phase_from_build_phase :: proc(t: ^testing.T) {
|
||||
world := World{}
|
||||
defer delete(world.events)
|
||||
|
||||
world.phase = .Build
|
||||
update_phase(&world)
|
||||
|
||||
testing.expect(t, world.phase == .Build)
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_update_phase_from_combat_phase :: proc(t: ^testing.T) {
|
||||
world := World{}
|
||||
defer delete(world.events)
|
||||
world.phase = .Combat
|
||||
|
||||
update_phase(&world)
|
||||
|
||||
testing.expect(t, world.phase == .Build)
|
||||
}
|
||||
|
||||
+14
-16
@@ -1,26 +1,24 @@
|
||||
package game
|
||||
|
||||
World :: struct {
|
||||
gold: int,
|
||||
base_health: int,
|
||||
world_map: Map,
|
||||
path: [dynamic]Vec2,
|
||||
enemies: [MAX_ENEMIES]Enemy,
|
||||
towers: [dynamic]Tower,
|
||||
events: [dynamic]Event,
|
||||
phase: Game_Phase,
|
||||
wave: int,
|
||||
spawner: Wave_Spawner,
|
||||
projectiles: [MAX_PROJECTILES]Projectile,
|
||||
selected_tower: Tower_Kind,
|
||||
gold: int,
|
||||
base_health: int,
|
||||
world_map: Map,
|
||||
path: [dynamic]Vec2,
|
||||
enemies: [MAX_ENEMIES]Enemy,
|
||||
towers: [dynamic]Tower,
|
||||
events: [dynamic]Event,
|
||||
phase: Game_Phase,
|
||||
wave: int,
|
||||
spawner: Wave_Spawner,
|
||||
projectiles: [MAX_PROJECTILES]Projectile,
|
||||
}
|
||||
|
||||
init_world :: proc() -> World {
|
||||
world := World {
|
||||
gold = STARTING_GOLD,
|
||||
base_health = STARTING_BASE_HEALTH,
|
||||
phase = .Build,
|
||||
selected_tower = .Archer,
|
||||
gold = 100,
|
||||
base_health = 20,
|
||||
phase = .Build,
|
||||
}
|
||||
init_map(&world.world_map)
|
||||
build_path(&world.world_map, &world.path)
|
||||
|
||||
Reference in New Issue
Block a user