This commit is contained in:
2026-08-03 21:11:00 +03:00
parent 8f596735e1
commit 06e41fc7da
33 changed files with 2180 additions and 356 deletions
+29 -18
View File
@@ -1,27 +1,38 @@
package main package main
import hm "core:container/handle_map"
import rl "vendor:raylib" import rl "vendor:raylib"
// ИИ врага — летит к игроку. Вызывается из диспатчера sys_update Enemy :: struct {
using actor: Actor,
touch_dmg: i32,
}
make_enemy :: proc(pos: rl.Vector2) -> Entity {
return Entity {
v = Enemy {
actor = Actor {
base = BaseEntity {
pos = pos,
col_size = 16,
gfx = {0 = GfxCircle{pos = {}, radius = 11, color = {220, 70, 70, 255}}},
},
hp = 40,
max_hp = 40,
speed = 55,
},
touch_dmg = 10,
},
}
}
enemy_update :: proc(state: ^GameState, e: ^Enemy, h: Handle, dt: f32) { enemy_update :: proc(state: ^GameState, e: ^Enemy, h: Handle, dt: f32) {
pe := get_entity(state, state.player) pe := get_entity(state, state.player)
if pe == nil do return if pe != nil {
dir := get_base(pe).pos - e.pos dir := get_base(pe).pos - e.pos
set_vel(&e.base, dir, e.speed) e.vel = rl.Vector2Normalize(dir) * e.speed
} }
// Мёртвые враги → лут if e.hp <= 0 && !e.marked {
sys_death :: proc(state: ^GameState) { e.marked = true
it := hm.iterator_make(&state.entities) spawn(state, make_ground_item(e.pos))
for e, _ in hm.iterate(&it) {
#partial switch &v in e.v {
case Enemy:
if v.hp <= 0 && !v.marked {
v.marked = true
spawn(state, make_ground_item(v.pos))
}
}
} }
} }
+69
View File
@@ -0,0 +1,69 @@
package main
import hm "core:container/handle_map"
import rl "vendor:raylib"
MAX_GFX :: 2
Handle :: hm.Handle32
MAX_ENTITIES :: 1024
BaseEntity :: struct {
pos: rl.Vector2,
vel: rl.Vector2,
col_size: f32,
marked: bool,
gfx: [MAX_GFX]Graphics,
}
Actor :: struct {
using base: BaseEntity,
hp: i32,
max_hp: i32,
speed: f32,
}
EntityVariant :: union {
Player,
Enemy,
GroundItem,
FX,
}
Entity :: struct {
handle: Handle,
v: EntityVariant,
}
get_base :: proc(e: ^Entity) -> ^BaseEntity {
#partial switch &v in e.v {
case Player:
return cast(^BaseEntity)&v
case Enemy:
return cast(^BaseEntity)&v
case GroundItem:
return cast(^BaseEntity)&v
case FX:
return cast(^BaseEntity)&v
}
return nil
}
get_actor :: proc(e: ^Entity) -> ^Actor {
#partial switch &v in e.v {
case Player:
return &v.actor
case Enemy:
return &v.actor
}
return nil
}
get_player :: proc(state: ^GameState) -> (^Entity, ^Player) {
e := get_entity(state, state.player)
if e == nil do return nil, nil
p, ok := &e.v.(Player)
if !ok do return nil, nil
return e, p
}
+32
View File
@@ -0,0 +1,32 @@
package main
FX :: struct {
using base: BaseEntity,
parent: Handle,
lifetime: f32,
permanent: bool,
}
init_fx :: proc(state: ^GameState, fx: FX) -> Handle {
return spawn(state, Entity{v = fx})
}
fx_update :: proc(state: ^GameState, fx: ^FX, h: Handle, dt: f32) {
if fx.parent != {} {
if pe := get_entity(state, fx.parent); pe != nil {
fx.pos = get_base(pe).pos
} else {
fx.marked = true
return
}
}
if !fx.permanent {
if fx.lifetime > 0 {
fx.lifetime -= dt
} else {
fx.marked = true
}
}
}
-53
View File
@@ -1,6 +1,5 @@
package main package main
import hm "core:container/handle_map"
import rl "vendor:raylib" import rl "vendor:raylib"
Graphics :: union { Graphics :: union {
@@ -40,7 +39,6 @@ GfxCircleLines :: struct {
} }
GfxTexture :: struct { GfxTexture :: struct {
// TODO: Implement texture rendering
} }
gfx_draw :: proc(pos: rl.Vector2, g: Graphics) { gfx_draw :: proc(pos: rl.Vector2, g: Graphics) {
@@ -55,54 +53,3 @@ gfx_draw :: proc(pos: rl.Vector2, g: Graphics) {
rl.DrawCircleLinesV(pos + v.pos, v.radius, v.color) rl.DrawCircleLinesV(pos + v.pos, v.radius, v.color)
} }
} }
// ─── FX ───
FX :: struct {
handle: Handle,
parent: Handle, // 0 = свободно плавает
pos: rl.Vector2,
gfx: [MAX_GFX]Graphics,
lifetime: f32,
permanent: bool,
marked: bool,
}
init_fx :: proc(state: ^GameState, fx: FX) -> Handle {
return hm.add(&state.fxs, fx)
}
fx_update :: proc(state: ^GameState, fx: ^FX, dt: f32) {
if fx.marked do return
// следуем за родителем, пока жив
if fx.parent != {} {
if e := get_entity(state, fx.parent); e != nil {
fx.pos = get_base(e).pos
} else {
fx.marked = true // родитель умер
return
}
}
if !fx.permanent {
if fx.lifetime > 0 {
fx.lifetime -= dt
} else {
fx.marked = true
}
}
}
fx_draw :: proc(fx: ^FX) {
for g in fx.gfx {
gfx_draw(fx.pos, g)
}
}
sys_fx_update :: proc(state: ^GameState, dt: f32) {
it := hm.iterator_make(&state.fxs)
for fx, _ in hm.iterate(&it) {
fx_update(state, fx, dt)
}
}
+17 -19
View File
@@ -1,24 +1,22 @@
package main package main
import rl "vendor:raylib"
// Пикап лута игроком — по коллизионным парам (как урон) GroundItem :: struct {
sys_pickup :: proc(state: ^GameState) { using base: BaseEntity,
_, p := get_player(state) heal: i32,
if p == nil do return }
for c in state.collisions { make_ground_item :: proc(pos: rl.Vector2) -> Entity {
other_h: Handle return Entity{
if c.a == state.player { v = GroundItem{
other_h = c.b base = BaseEntity{
} else if c.b == state.player { pos = pos, col_size = 10,
other_h = c.a gfx = {0 = GfxCircle{pos = {}, radius = 6, color = {255, 220, 50, 255}}},
} else do continue },
heal = 25,
e := get_entity(state, other_h) },
if e == nil do continue
gi, ok := &e.v.(GroundItem)
if !ok || gi.marked do continue
p.hp = min(p.hp + gi.heal, p.max_hp)
gi.marked = true
} }
} }
item_update :: proc(state: ^GameState, i: ^GroundItem, h: Handle, dt: f32) {
}
+10 -10
View File
@@ -2,12 +2,17 @@ package main
import rl "vendor:raylib" import rl "vendor:raylib"
SCREEN_W :: 1280
SCREEN_H :: 720
TARGET_FPS :: 60
main :: proc() { main :: proc() {
rl.InitWindow(SCREEN_W, SCREEN_H, "Iso Arena") rl.InitWindow(SCREEN_W, SCREEN_H, "Iso Arena")
rl.SetTargetFPS(TARGET_FPS) rl.SetTargetFPS(TARGET_FPS)
defer rl.CloseWindow() defer rl.CloseWindow()
state := GameState{ state := GameState {
camera = {zoom = 1.5, offset = {SCREEN_W / 2, SCREEN_H / 2}}, camera = {zoom = 1.5, offset = {SCREEN_W / 2, SCREEN_H / 2}},
} }
state.collisions = make([dynamic]CollisionPair) state.collisions = make([dynamic]CollisionPair)
@@ -21,18 +26,13 @@ main :: proc() {
for !rl.WindowShouldClose() { for !rl.WindowShouldClose() {
dt := rl.GetFrameTime() dt := rl.GetFrameTime()
sys_update(&state, dt) // вход игрока + ИИ врагов (диспатчер) sys_input(&state, dt)
sys_move(&state, dt) // движение всех sys_update(&state, dt)
sys_collisions(&state) // пары + резолв sys_sweep(&state)
sys_damage(&state) // дамаг игроку от врагов
sys_death(&state) // враги умерли → лут
sys_pickup(&state) // игрок подбирает лут
sys_fx_update(&state, dt) // FX: таймеры + следование
sys_sweep(&state) // удалить помеченное
if pe := get_entity(&state, state.player); pe != nil { if pe := get_entity(&state, state.player); pe != nil {
state.camera.target = get_base(pe).pos state.camera.target = get_base(pe).pos
} }
sys_render(&state) sys_draw(&state)
} }
} }
+48
View File
@@ -0,0 +1,48 @@
package main
import "core:fmt"
import rl "vendor:raylib"
TILE_W :: 64
TILE_H :: 32
MAP_SIZE :: 60
Vector2I :: distinct [2]i32
world_to_tile_V2I :: #force_inline proc(pos: rl.Vector2) -> Vector2I {
return Vector2I{i32((pos.x - pos.y) * TILE_W * 0.5), i32((pos.x + pos.y) * TILE_H * 0.5)}
}
world_to_tile :: #force_inline proc(pos: rl.Vector2) -> rl.Vector2 {
return rl.Vector2{(pos.x - pos.y) * TILE_W * 0.5, (pos.x + pos.y) * TILE_H * 0.5}
}
//TODO: return i32?
depth_key :: #force_inline proc(pos: rl.Vector2) -> f32 {
return pos.x + pos.y
}
draw_tile :: #force_inline proc(wprldPos: rl.Vector2, color: rl.Color) {
tilePos := world_to_tile(wprldPos)
top := tilePos + rl.Vector2{0, -TILE_H * 0.5}
right := tilePos + rl.Vector2{TILE_W * 0.5, 0}
down := tilePos + rl.Vector2{0, TILE_H * 0.5}
left := tilePos + rl.Vector2{-TILE_W * 0.5, 0}
rl.DrawLineV(top, right, color)
rl.DrawLineV(right, down, color)
rl.DrawLineV(down, left, color)
rl.DrawLineV(left, top, color)
}
draw_grid :: #force_inline proc() {
middle := MAP_SIZE / 2
for x in 0 ..< MAP_SIZE {
for y in 0 ..< MAP_SIZE {
wx := f32(x - middle)
wy := f32(y - middle)
color := ((x + y) % 2 == 0) ? rl.Color{64, 64, 76, 255} : rl.Color{51, 51, 63, 255}
draw_tile(rl.Vector2{wx, wy}, color)
}
}
}
+69 -17
View File
@@ -3,32 +3,80 @@ package main
import hm "core:container/handle_map" import hm "core:container/handle_map"
import rl "vendor:raylib" import rl "vendor:raylib"
// Вход и атака игрока — вызывается из диспатчера sys_update Player :: struct {
player_update :: proc(state: ^GameState, p: ^Player, h: Handle, dt: f32) { using actor: Actor,
// движение attack_range: f32,
dir: rl.Vector2 attack_dmg: i32,
if rl.IsKeyDown(.W) || rl.IsKeyDown(.UP) do dir.y -= 1 attack_cd: f32,
if rl.IsKeyDown(.S) || rl.IsKeyDown(.DOWN) do dir.y += 1 attack_timer: f32,
if rl.IsKeyDown(.A) || rl.IsKeyDown(.LEFT) do dir.x -= 1 invuln_timer: f32,
if rl.IsKeyDown(.D) || rl.IsKeyDown(.RIGHT) do dir.x += 1 }
set_vel(&p.base, dir, p.speed)
PlayerInput :: struct {
move: rl.Vector2,
attack: bool,
}
make_player :: proc() -> Entity {
return Entity {
v = Player {
actor = Actor {
base = BaseEntity {
pos = {400, 300},
col_size = 16,
gfx = {0 = GfxCircle{pos = {}, radius = 14, color = {80, 220, 100, 255}}},
},
hp = 100,
max_hp = 100,
speed = 220,
},
attack_range = 75,
attack_dmg = 35,
attack_cd = 0.3,
},
}
}
sys_input :: proc(state: ^GameState, dt: f32) {
input: PlayerInput
if rl.IsKeyDown(.W) || rl.IsKeyDown(.UP) do input.move.y -= 1
if rl.IsKeyDown(.S) || rl.IsKeyDown(.DOWN) do input.move.y += 1
if rl.IsKeyDown(.A) || rl.IsKeyDown(.LEFT) do input.move.x -= 1
if rl.IsKeyDown(.D) || rl.IsKeyDown(.RIGHT) do input.move.x += 1
input.attack = rl.IsKeyPressed(.SPACE)
player_input(state, input, dt)
}
player_input :: proc(state: ^GameState, input: PlayerInput, dt: f32) {
_, p := get_player(state)
if p == nil do return
p.vel = rl.Vector2Normalize(input.move) * p.speed
// атака
if p.attack_timer > 0 do p.attack_timer -= dt if p.attack_timer > 0 do p.attack_timer -= dt
if rl.IsKeyPressed(.SPACE) && p.attack_timer <= 0 { if input.attack && p.attack_timer <= 0 {
p.attack_timer = p.attack_cd p.attack_timer = p.attack_cd
// визуал атаки — кольцо init_fx(
init_fx(state, FX{ state,
parent = h, FX {
base = BaseEntity {
gfx = { gfx = {
0 = GfxRing{inner_radius = p.attack_range * 0.7, outer_radius = p.attack_range, color = {255, 255, 255, 40}}, 0 = GfxRing {
inner_radius = p.attack_range * 0.7,
outer_radius = p.attack_range,
color = {255, 255, 255, 40},
},
1 = GfxCircleLines{radius = p.attack_range, color = {255, 255, 255, 80}}, 1 = GfxCircleLines{radius = p.attack_range, color = {255, 255, 255, 80}},
}, },
},
parent = state.player,
lifetime = p.attack_cd, lifetime = p.attack_cd,
}) },
)
// дамаг врагам в радиусе
it := hm.iterator_make(&state.entities) it := hm.iterator_make(&state.entities)
for e, _ in hm.iterate(&it) { for e, _ in hm.iterate(&it) {
#partial switch &v in e.v { #partial switch &v in e.v {
@@ -40,3 +88,7 @@ player_update :: proc(state: ^GameState, p: ^Player, h: Handle, dt: f32) {
} }
} }
} }
player_update :: proc(state: ^GameState, p: ^Player, h: Handle, dt: f32) {
if p.invuln_timer > 0 do p.invuln_timer -= dt
}
+62 -227
View File
@@ -3,177 +3,30 @@ package main
import hm "core:container/handle_map" import hm "core:container/handle_map"
import rl "vendor:raylib" import rl "vendor:raylib"
SCREEN_W :: 1280
SCREEN_H :: 720
TARGET_FPS :: 60
SQRT2_INV :: 0.7071067811865476 SQRT2_INV :: 0.7071067811865476
MAX_GFX :: 2
Handle :: hm.Handle32 CollisionPair :: struct {
a: Handle,
MAX_ENTITIES :: 1024 b: Handle,
MAX_FX :: 256
// ─── Общее — всё что есть у любой сущности ───
BaseEntity :: struct {
pos: rl.Vector2,
vel: rl.Vector2,
col_size: f32,
marked: bool,
gfx: [MAX_GFX]Graphics,
} }
// ─── Акторское — то что есть у живых ───
Actor :: struct {
using base: BaseEntity,
hp: i32,
max_hp: i32,
speed: f32,
}
// ─── Конкретные типы ───
Player :: struct {
using actor: Actor,
attack_range: f32,
attack_dmg: i32,
attack_cd: f32,
attack_timer: f32,
invuln_timer: f32,
}
Enemy :: struct {
using actor: Actor,
touch_dmg: i32,
}
GroundItem :: struct {
using base: BaseEntity,
heal: i32,
}
EntityVariant :: union { Player, Enemy, GroundItem }
// ─── Контейнер для handle_map ───
Entity :: struct {
handle: Handle,
v: EntityVariant,
}
// ─── Состояние игры ───
GameState :: struct { GameState :: struct {
player: Handle, player: Handle,
camera: rl.Camera2D, camera: rl.Camera2D,
entities: hm.Static_Handle_Map(MAX_ENTITIES, Entity, Handle), entities: hm.Static_Handle_Map(MAX_ENTITIES, Entity, Handle),
fxs: hm.Static_Handle_Map(MAX_FX, FX, Handle),
collisions: [dynamic]CollisionPair, collisions: [dynamic]CollisionPair,
} }
// ─── Доступ ───
get_entity :: proc(state: ^GameState, h: Handle) -> ^Entity { get_entity :: proc(state: ^GameState, h: Handle) -> ^Entity {
e, ok := hm.get(&state.entities, h) e, ok := hm.get(&state.entities, h)
return e if ok else nil return e if ok else nil
} }
// Общее из любой сущности (BaseEntity первым полем во всех вариантах)
get_base :: proc(e: ^Entity) -> ^BaseEntity {
#partial switch &v in e.v {
case Player:
return cast(^BaseEntity)&v
case Enemy:
return cast(^BaseEntity)&v
case GroundItem:
return cast(^BaseEntity)&v
}
return nil
}
// Акторское (есть только у живых)
get_actor :: proc(e: ^Entity) -> ^Actor {
#partial switch &v in e.v {
case Player:
return &v.actor
case Enemy:
return &v.actor
}
return nil
}
get_player :: proc(state: ^GameState) -> (^Entity, ^Player) {
e := get_entity(state, state.player)
if e == nil do return nil, nil
p, ok := &e.v.(Player)
if !ok do return nil, nil
return e, p
}
// ─── Фабрики ───
make_player :: proc() -> Entity {
return Entity{
v = Player{
actor = Actor{
base = BaseEntity{
pos = {400, 300}, col_size = 16,
gfx = {0 = GfxCircle{pos = {}, radius = 14, color = {80, 220, 100, 255}}},
},
hp = 100, max_hp = 100, speed = 220,
},
attack_range = 75,
attack_dmg = 35,
attack_cd = 0.3,
},
}
}
make_enemy :: proc(pos: rl.Vector2) -> Entity {
return Entity{
v = Enemy{
actor = Actor{
base = BaseEntity{
pos = pos, col_size = 16,
gfx = {0 = GfxCircle{pos = {}, radius = 11, color = {220, 70, 70, 255}}},
},
hp = 40, max_hp = 40, speed = 55,
},
touch_dmg = 10,
},
}
}
make_ground_item :: proc(pos: rl.Vector2) -> Entity {
return Entity{
v = GroundItem{
base = BaseEntity{
pos = pos, col_size = 10,
gfx = {0 = GfxCircle{pos = {}, radius = 6, color = {255, 220, 50, 255}}},
},
heal = 25,
},
}
}
spawn :: proc(state: ^GameState, e: Entity) -> Handle { spawn :: proc(state: ^GameState, e: Entity) -> Handle {
return hm.add(&state.entities, e) return hm.add(&state.entities, e)
} }
// ─── Хелперы ───
set_vel :: proc(b: ^BaseEntity, dir: rl.Vector2, speed: f32) {
if rl.Vector2Length(dir) > 0 {
b.vel = rl.Vector2Normalize(dir) * speed
} else {
b.vel = {}
}
}
// ─── Коллизия ───
entity_rect :: #force_inline proc(b: ^BaseEntity) -> rl.Rectangle { entity_rect :: #force_inline proc(b: ^BaseEntity) -> rl.Rectangle {
return rl.Rectangle{b.pos.x - b.col_size / 2, b.pos.y - b.col_size / 2, b.col_size, b.col_size} return rl.Rectangle{b.pos.x - b.col_size / 2, b.pos.y - b.col_size / 2, b.col_size, b.col_size}
} }
@@ -200,8 +53,6 @@ resolve_aabb :: proc(a, b: ^BaseEntity) -> bool {
return true return true
} }
// ─── Диспатчер: один свитч, типизированные функции ───
sys_update :: proc(state: ^GameState, dt: f32) { sys_update :: proc(state: ^GameState, dt: f32) {
it := hm.iterator_make(&state.entities) it := hm.iterator_make(&state.entities)
for e, h in hm.iterate(&it) { for e, h in hm.iterate(&it) {
@@ -211,32 +62,68 @@ sys_update :: proc(state: ^GameState, dt: f32) {
case Enemy: case Enemy:
enemy_update(state, &v, h, dt) enemy_update(state, &v, h, dt)
case GroundItem: case GroundItem:
// пока статичный item_update(state, &v, h, dt)
} }
} }
}
// ─── Общее движение ─── it2 := hm.iterator_make(&state.entities)
for e, _ in hm.iterate(&it2) {
sys_move :: proc(state: ^GameState, dt: f32) {
it := hm.iterator_make(&state.entities)
for e, _ in hm.iterate(&it) {
b := get_base(e) b := get_base(e)
b.pos += b.vel * dt b.pos += b.vel * dt
} }
it3 := hm.iterator_make(&state.entities)
for e, h in hm.iterate(&it3) {
#partial switch &v in e.v {
case FX:
fx_update(state, &v, h, dt)
}
}
resolve_collisions(state)
apply_collision_effects(state)
} }
// ─── Коллизии ─── sys_sweep :: proc(state: ^GameState) {
it := hm.iterator_make(&state.entities)
CollisionPair :: struct { for e, h in hm.iterate(&it) {
a: Handle, if get_base(e).marked {
b: Handle, hm.remove(&state.entities, h)
}
}
} }
sys_collisions :: proc(state: ^GameState) { entity_draw :: proc(e: ^Entity) {
b := get_base(e)
for g in b.gfx {
gfx_draw(b.pos, g)
}
}
sys_draw :: proc(state: ^GameState) {
rl.BeginDrawing()
rl.ClearBackground({20, 20, 30, 255})
rl.BeginMode2D(state.camera)
{
draw_grid()
it := hm.iterator_make(&state.entities)
for e, _ in hm.iterate(&it) {
entity_draw(e)
if a := get_actor(e); a != nil {
ui_draw_world_hp_bar(get_base(e), a)
}
}
}
rl.EndMode2D()
ui_draw(state)
rl.EndDrawing()
}
resolve_collisions :: proc(state: ^GameState) {
clear(&state.collisions) clear(&state.collisions)
// собрать все живые handle'ы
hs: [MAX_ENTITIES]Handle hs: [MAX_ENTITIES]Handle
n := 0 n := 0
it := hm.iterator_make(&state.entities) it := hm.iterator_make(&state.entities)
@@ -245,7 +132,6 @@ sys_collisions :: proc(state: ^GameState) {
n += 1 n += 1
} }
// O(n²): резолвим пересечения и собираем пары
for i in 0 ..< n { for i in 0 ..< n {
for j in i + 1 ..< n { for j in i + 1 ..< n {
a := get_entity(state, hs[i]) a := get_entity(state, hs[i])
@@ -257,14 +143,8 @@ sys_collisions :: proc(state: ^GameState) {
} }
} }
// Дамаг игроку от врагов по коллизионным парам apply_collision_effects :: proc(state: ^GameState) {
sys_damage :: proc(state: ^GameState) {
_, p := get_player(state) _, p := get_player(state)
if p == nil do return
if p.invuln_timer > 0 {
p.invuln_timer -= rl.GetFrameTime()
return
}
for c in state.collisions { for c in state.collisions {
other_h: Handle other_h: Handle
@@ -276,64 +156,19 @@ sys_damage :: proc(state: ^GameState) {
e := get_entity(state, other_h) e := get_entity(state, other_h)
if e == nil do continue if e == nil do continue
ev, ok := &e.v.(Enemy)
if !ok do continue
p.hp -= ev.touch_dmg #partial switch &v in e.v {
case Enemy:
if p != nil && p.invuln_timer <= 0 {
p.hp -= v.touch_dmg
p.invuln_timer = 0.5 p.invuln_timer = 0.5
if p.hp <= 0 do p.hp = 0 if p.hp <= 0 do p.hp = 0
break // один урон за кадр, дальше неуязвим
} }
} case GroundItem:
if p != nil && !v.marked {
// ─── Очистка помеченного ─── p.hp = min(p.hp + v.heal, p.max_hp)
v.marked = true
sys_sweep :: proc(state: ^GameState) {
it := hm.iterator_make(&state.entities)
for e, h in hm.iterate(&it) {
if get_base(e).marked {
hm.remove(&state.entities, h)
} }
} }
it2 := hm.iterator_make(&state.fxs)
for fx, h in hm.iterate(&it2) {
if fx.marked {
hm.remove(&state.fxs, h)
}
} }
} }
// ─── Рендер ───
entity_draw :: proc(e: ^Entity) {
b := get_base(e)
for g in b.gfx {
gfx_draw(b.pos, g)
}
}
sys_render :: proc(state: ^GameState) {
rl.BeginDrawing()
rl.ClearBackground({20, 20, 30, 255})
rl.BeginMode2D(state.camera)
{
it := hm.iterator_make(&state.entities)
for e, _ in hm.iterate(&it) {
entity_draw(e)
if a := get_actor(e); a != nil {
ui_draw_world_hp_bar(get_base(e), a)
}
}
it2 := hm.iterator_make(&state.fxs)
for fx, _ in hm.iterate(&it2) {
fx_draw(fx)
}
}
rl.EndMode2D()
ui_draw(state)
rl.EndDrawing()
}
+1 -1
View File
@@ -1,6 +1,6 @@
package main package main
import hm "core:container/handle_map" import hm "core:container/handle_map"
import rl "vendor:raylib" import rl "vendor:raylib"
ui_draw_world_hp_bar :: proc(b: ^BaseEntity, actor: ^Actor) { ui_draw_world_hp_bar :: proc(b: ^BaseEntity, actor: ^Actor) {
+62
View File
@@ -0,0 +1,62 @@
package main
import hm "core:container/handle_map"
import rl "vendor:raylib"
MAX_ATTACK_HITS :: 32
Attack :: struct {
using base: BaseEntity,
parent: Handle,
dmg: i32,
radius: f32,
lifetime: f32,
hit: [MAX_ATTACK_HITS]Handle,
hit_count: u8,
}
init_attack :: proc(state: ^GameState, a: Attack) -> Handle {
return spawn(state, Entity{v = a})
}
attack_update :: proc(state: ^GameState, a: ^Attack, h: Handle, dt: f32) {
if a.parent != {} {
if pe := get_entity(state, a.parent); pe != nil {
a.pos = get_base(pe).pos
} else {
a.marked = true
return
}
}
it := hm.iterator_make(&state.entities)
for e, eh in hm.iterate(&it) {
#partial switch &v in e.v {
case Enemy:
if v.hp <= 0 || v.marked do continue
if rl.Vector2Distance(a.pos, v.pos) <= a.radius && !attack_has_hit(a, eh) {
v.hp -= a.dmg
attack_add_hit(a, eh)
}
}
}
if a.lifetime > 0 {
a.lifetime -= dt
} else {
a.marked = true
}
}
attack_has_hit :: proc(a: ^Attack, h: Handle) -> bool {
for i in 0 ..< a.hit_count {
if a.hit[i] == h do return true
}
return false
}
attack_add_hit :: proc(a: ^Attack, h: Handle) {
if a.hit_count >= MAX_ATTACK_HITS do return
a.hit[a.hit_count] = h
a.hit_count += 1
}
+38
View File
@@ -0,0 +1,38 @@
package main
import rl "vendor:raylib"
Enemy :: struct {
using actor: Actor,
touch_dmg: i32,
}
make_enemy :: proc(pos: rl.Vector2) -> Entity {
return Entity {
v = Enemy {
actor = Actor {
base = BaseEntity {
pos = pos,
col_size = 16,
gfx = {0 = GfxCircle{pos = {}, radius = 11, color = {220, 70, 70, 255}}},
},
hp = 40,
max_hp = 40,
speed = 55,
},
touch_dmg = 10,
},
}
}
enemy_update :: proc(state: ^GameState, e: ^Enemy, h: Handle, dt: f32) {
pe := get_entity(state, state.player)
if pe != nil {
dir := get_base(pe).pos - e.pos
e.vel = rl.Vector2Normalize(dir) * e.speed
}
if e.hp <= 0 && !e.marked {
e.marked = true
spawn(state, make_ground_item(e.pos))
}
}
+72
View File
@@ -0,0 +1,72 @@
package main
import hm "core:container/handle_map"
import rl "vendor:raylib"
MAX_GFX :: 2
Handle :: hm.Handle32
MAX_ENTITIES :: 1024
BaseEntity :: struct {
pos: rl.Vector2,
vel: rl.Vector2,
col_size: f32,
marked: bool,
gfx: [MAX_GFX]Graphics,
}
Actor :: struct {
using base: BaseEntity,
hp: i32,
max_hp: i32,
speed: f32,
}
EntityVariant :: union {
Player,
Enemy,
GroundItem,
FX,
Attack,
}
Entity :: struct {
handle: Handle,
v: EntityVariant,
}
get_base :: proc(e: ^Entity) -> ^BaseEntity {
#partial switch &v in e.v {
case Player:
return cast(^BaseEntity)&v
case Enemy:
return cast(^BaseEntity)&v
case GroundItem:
return cast(^BaseEntity)&v
case FX:
return cast(^BaseEntity)&v
case Attack:
return cast(^BaseEntity)&v
}
return nil
}
get_actor :: proc(e: ^Entity) -> ^Actor {
#partial switch &v in e.v {
case Player:
return &v.actor
case Enemy:
return &v.actor
}
return nil
}
get_player :: proc(state: ^GameState) -> (^Entity, ^Player) {
e := get_entity(state, state.player)
if e == nil do return nil, nil
p, ok := &e.v.(Player)
if !ok do return nil, nil
return e, p
}
+32
View File
@@ -0,0 +1,32 @@
package main
FX :: struct {
using base: BaseEntity,
parent: Handle,
lifetime: f32,
permanent: bool,
}
init_fx :: proc(state: ^GameState, fx: FX) -> Handle {
return spawn(state, Entity{v = fx})
}
fx_update :: proc(state: ^GameState, fx: ^FX, h: Handle, dt: f32) {
if fx.parent != {} {
if pe := get_entity(state, fx.parent); pe != nil {
fx.pos = get_base(pe).pos
} else {
fx.marked = true
return
}
}
if !fx.permanent {
if fx.lifetime > 0 {
fx.lifetime -= dt
} else {
fx.marked = true
}
}
}
+55
View File
@@ -0,0 +1,55 @@
package main
import rl "vendor:raylib"
Graphics :: union {
GfxNone,
GfxCircle,
GfxRect,
GfxRing,
GfxCircleLines,
GfxTexture,
}
GfxNone :: struct {}
GfxCircle :: struct {
pos: rl.Vector2,
radius: f32,
color: rl.Color,
}
GfxRect :: struct {
pos: rl.Vector2,
size: rl.Vector2,
color: rl.Color,
}
GfxRing :: struct {
pos: rl.Vector2,
inner_radius: f32,
outer_radius: f32,
color: rl.Color,
}
GfxCircleLines :: struct {
pos: rl.Vector2,
radius: f32,
color: rl.Color,
}
GfxTexture :: struct {
}
gfx_draw :: proc(pos: rl.Vector2, g: Graphics) {
#partial switch v in g {
case GfxCircle:
rl.DrawCircleV(pos + v.pos, v.radius, v.color)
case GfxRect:
rl.DrawRectangleV(pos - v.size / 2 + v.pos, v.size, v.color)
case GfxRing:
rl.DrawRing(pos + v.pos, v.inner_radius, v.outer_radius, 0, 360, 0, v.color)
case GfxCircleLines:
rl.DrawCircleLinesV(pos + v.pos, v.radius, v.color)
}
}
+22
View File
@@ -0,0 +1,22 @@
package main
import rl "vendor:raylib"
GroundItem :: struct {
using base: BaseEntity,
heal: i32,
}
make_ground_item :: proc(pos: rl.Vector2) -> Entity {
return Entity{
v = GroundItem{
base = BaseEntity{
pos = pos, col_size = 10,
gfx = {0 = GfxCircle{pos = {}, radius = 6, color = {255, 220, 50, 255}}},
},
heal = 25,
},
}
}
item_update :: proc(state: ^GameState, i: ^GroundItem, h: Handle, dt: f32) {
}
+48
View File
@@ -0,0 +1,48 @@
package main
import rl "vendor:raylib"
SCREEN_W :: 1280
SCREEN_H :: 720
TARGET_FPS :: 60
main :: proc() {
rl.InitWindow(SCREEN_W, SCREEN_H, "Iso Arena 3D")
rl.SetTargetFPS(TARGET_FPS)
defer rl.CloseWindow()
camera := rl.Camera3D {
position = {0, 80, 80},
target = {0, 0, 0},
up = {0, 1, 0},
fovy = 400,
projection = .ORTHOGRAPHIC,
}
state := GameState {
camera = camera,
}
state.collisions = make([dynamic]CollisionPair)
defer delete(state.collisions)
state.player = spawn(&state, make_player())
spawn(&state, make_enemy({200, 200}))
spawn(&state, make_enemy({-200, 200}))
spawn(&state, make_enemy({200, -200}))
for !rl.WindowShouldClose() {
dt := rl.GetFrameTime()
sys_input(&state, dt)
sys_update(&state, dt)
sys_sweep(&state)
if pe := get_entity(&state, state.player); pe != nil {
p := get_base(pe).pos
state.camera.target = {p.x, 0, p.y}
state.camera.position = state.camera.target + {600, 600, 600}
}
sys_draw(&state)
}
}
+22
View File
@@ -0,0 +1,22 @@
package main
import rl "vendor:raylib"
TILE_W :: 64
MAP_SIZE :: 20
depth_key :: #force_inline proc(pos: rl.Vector2) -> f32 {
return pos.x + pos.y
}
draw_grid_3d :: proc() {
middle := MAP_SIZE / 2
for x in 0 ..< MAP_SIZE {
for y in 0 ..< MAP_SIZE {
wx := f32(x - middle) * TILE_W
wy := f32(y - middle) * TILE_W
color := ((x + y) % 2 == 0) ? rl.Color{64, 64, 76, 255} : rl.Color{51, 51, 63, 255}
rl.DrawPlane({wx, 0, wy}, {TILE_W, TILE_W}, color)
}
}
}
+85
View File
@@ -0,0 +1,85 @@
package main
import hm "core:container/handle_map"
import rl "vendor:raylib"
Player :: struct {
using actor: Actor,
attack_range: f32,
attack_dmg: i32,
attack_cd: f32,
attack_timer: f32,
invuln_timer: f32,
}
PlayerInput :: struct {
move: rl.Vector2,
attack: bool,
}
make_player :: proc() -> Entity {
return Entity {
v = Player {
actor = Actor {
base = BaseEntity {
pos = {0, 0},
col_size = 16,
gfx = {0 = GfxCircle{pos = {}, radius = 14, color = {80, 220, 100, 255}}},
},
hp = 100,
max_hp = 100,
speed = 220,
},
attack_range = 75,
attack_dmg = 35,
attack_cd = 0.3,
},
}
}
sys_input :: proc(state: ^GameState, dt: f32) {
input: PlayerInput
if rl.IsKeyDown(.W) || rl.IsKeyDown(.UP) do input.move += {-1, -1}
if rl.IsKeyDown(.S) || rl.IsKeyDown(.DOWN) do input.move += {1, 1}
if rl.IsKeyDown(.A) || rl.IsKeyDown(.LEFT) do input.move += {-1, 1}
if rl.IsKeyDown(.D) || rl.IsKeyDown(.RIGHT) do input.move += {1, -1}
input.attack = rl.IsKeyPressed(.SPACE)
player_input(state, input, dt)
}
player_input :: proc(state: ^GameState, input: PlayerInput, dt: f32) {
_, p := get_player(state)
if p == nil do return
p.vel = rl.Vector2Normalize(input.move) * p.speed
if p.attack_timer > 0 do p.attack_timer -= dt
if input.attack && p.attack_timer <= 0 {
p.attack_timer = p.attack_cd
init_attack(
state,
Attack {
base = BaseEntity {
gfx = {
0 = GfxRing {
inner_radius = p.attack_range * 0.7,
outer_radius = p.attack_range,
color = {255, 255, 255, 40},
},
},
},
parent = state.player,
dmg = p.attack_dmg,
radius = p.attack_range,
lifetime = p.attack_cd,
},
)
}
}
player_update :: proc(state: ^GameState, p: ^Player, h: Handle, dt: f32) {
if p.invuln_timer > 0 do p.invuln_timer -= dt
}
+202
View File
@@ -0,0 +1,202 @@
package main
import hm "core:container/handle_map"
import rl "vendor:raylib"
SQRT2_INV :: 0.7071067811865476
CollisionPair :: struct {
a: Handle,
b: Handle,
}
GameState :: struct {
player: Handle,
camera: rl.Camera3D,
entities: hm.Static_Handle_Map(MAX_ENTITIES, Entity, Handle),
collisions: [dynamic]CollisionPair,
}
get_entity :: proc(state: ^GameState, h: Handle) -> ^Entity {
e, ok := hm.get(&state.entities, h)
return e if ok else nil
}
spawn :: proc(state: ^GameState, e: Entity) -> Handle {
return hm.add(&state.entities, e)
}
entity_rect :: #force_inline proc(b: ^BaseEntity) -> rl.Rectangle {
return rl.Rectangle{b.pos.x - b.col_size / 2, b.pos.y - b.col_size / 2, b.col_size, b.col_size}
}
check_col :: proc(a, b: ^BaseEntity) -> (overlap: rl.Rectangle, colliding: bool) {
c := rl.GetCollisionRec(entity_rect(a), entity_rect(b))
if c.width <= 0 || c.height <= 0 do return rl.Rectangle{}, false
return c, true
}
resolve_aabb :: proc(a, b: ^BaseEntity) -> bool {
c, ok := check_col(a, b)
if !ok do return false
if c.width < c.height {
sign: f32 = 1 if a.pos.x < b.pos.x else -1
a.pos.x -= sign * c.width * 0.5
b.pos.x += sign * c.width * 0.5
} else {
sign: f32 = 1 if a.pos.y < b.pos.y else -1
a.pos.y -= sign * c.height * 0.5
b.pos.y += sign * c.height * 0.5
}
return true
}
sys_update :: proc(state: ^GameState, dt: f32) {
it := hm.iterator_make(&state.entities)
for e, h in hm.iterate(&it) {
#partial switch &v in e.v {
case Player:
player_update(state, &v, h, dt)
case Enemy:
enemy_update(state, &v, h, dt)
case GroundItem:
item_update(state, &v, h, dt)
}
}
it2 := hm.iterator_make(&state.entities)
for e, _ in hm.iterate(&it2) {
b := get_base(e)
b.pos += b.vel * dt
}
it3 := hm.iterator_make(&state.entities)
for e, h in hm.iterate(&it3) {
#partial switch &v in e.v {
case FX:
fx_update(state, &v, h, dt)
case Attack:
attack_update(state, &v, h, dt)
}
}
resolve_collisions(state)
apply_collision_effects(state)
}
sys_sweep :: proc(state: ^GameState) {
it := hm.iterator_make(&state.entities)
for e, h in hm.iterate(&it) {
if get_base(e).marked {
hm.remove(&state.entities, h)
}
}
}
entity_draw :: proc(state: ^GameState, e: ^Entity) {
b := get_base(e)
sp := rl.GetWorldToScreen({b.pos.x, 0, b.pos.y}, state.camera)
scale := SCREEN_H / state.camera.fovy
#partial switch v in b.gfx[0] {
case GfxCircle:
rl.DrawCircleV(sp, v.radius * scale, v.color)
case:
}
}
ground_effect_draw :: proc(state: ^GameState, e: ^Entity) {
b := get_base(e)
#partial switch v in b.gfx[0] {
case GfxRing:
rl.DrawCylinder({b.pos.x, 0.02, b.pos.y}, v.outer_radius, v.outer_radius, 0.04, 48, v.color)
case:
}
}
sys_draw :: proc(state: ^GameState) {
rl.BeginDrawing()
rl.ClearBackground({20, 20, 30, 255})
rl.BeginMode3D(state.camera)
draw_grid_3d()
it := hm.iterator_make(&state.entities)
for e, _ in hm.iterate(&it) {
ground_effect_draw(state, e)
}
rl.EndMode3D()
scale := SCREEN_H / state.camera.fovy
it2 := hm.iterator_make(&state.entities)
for e, _ in hm.iterate(&it2) {
entity_draw(state, e)
if a := get_actor(e); a != nil {
b := get_base(e)
sp := rl.GetWorldToScreen({b.pos.x, 0, b.pos.y}, state.camera)
hp_ratio := f32(a.hp) / f32(a.max_hp)
bar_w := b.col_size * 3 * scale
bar_pos := sp - rl.Vector2{bar_w / 2, b.col_size * 2 * scale + 12}
rl.DrawRectangleV(bar_pos, {bar_w, 5}, {60, 60, 60, 255})
rl.DrawRectangleV(bar_pos, {bar_w * hp_ratio, 5}, {220, 40, 40, 255})
}
}
ui_draw(state)
rl.EndDrawing()
}
resolve_collisions :: proc(state: ^GameState) {
clear(&state.collisions)
hs: [MAX_ENTITIES]Handle
n := 0
it := hm.iterator_make(&state.entities)
for _, h in hm.iterate(&it) {
hs[n] = h
n += 1
}
for i in 0 ..< n {
for j in i + 1 ..< n {
a := get_entity(state, hs[i])
b := get_entity(state, hs[j])
if resolve_aabb(get_base(a), get_base(b)) {
append(&state.collisions, CollisionPair{a = hs[i], b = hs[j]})
}
}
}
}
apply_collision_effects :: proc(state: ^GameState) {
_, p := get_player(state)
for c in state.collisions {
other_h: Handle
if c.a == state.player {
other_h = c.b
} else if c.b == state.player {
other_h = c.a
} else do continue
e := get_entity(state, other_h)
if e == nil do continue
#partial switch &v in e.v {
case Enemy:
if p != nil && p.invuln_timer <= 0 {
p.hp -= v.touch_dmg
p.invuln_timer = 0.5
if p.hp <= 0 do p.hp = 0
}
case GroundItem:
if p != nil && !v.marked {
p.hp = min(p.hp + v.heal, p.max_hp)
v.marked = true
}
}
}
}
+26
View File
@@ -0,0 +1,26 @@
package main
import hm "core:container/handle_map"
import rl "vendor:raylib"
ui_draw_world_hp_bar :: proc(b: ^BaseEntity, actor: ^Actor) {
hp_ratio := f32(actor.hp) / f32(actor.max_hp)
bar_w := b.col_size * 3
bar_pos := b.pos + rl.Vector2{-bar_w / 2, -b.col_size - 10}
rl.DrawRectangleV(bar_pos, {bar_w, 4}, {60, 60, 60, 255})
rl.DrawRectangleV(bar_pos, {bar_w * hp_ratio, 4}, {220, 40, 40, 255})
}
ui_draw :: proc(state: ^GameState) {
_, p := get_player(state)
hp: i32 = 0
max_hp: i32 = 1
if p != nil {
hp = p.hp
max_hp = p.max_hp
}
rl.DrawText(rl.TextFormat("HP: %d/%d", hp, max_hp), 12, 12, 20, rl.RAYWHITE)
rl.DrawText(rl.TextFormat("Entities: %d", hm.len(state.entities)), 12, 36, 16, {180, 180, 180, 255})
rl.DrawText("[SPACE] attack [WASD] move", 12, SCREEN_H - 28, 14, {120, 120, 120, 255})
}
+79
View File
@@ -0,0 +1,79 @@
package main
import rand "core:math/rand"
import hm "core:container/handle_map"
import rl "vendor:raylib"
MAX_ATTACK_HITS :: 32
Attack :: struct {
using base: BaseEntity,
parent: Handle,
dir: rl.Vector2,
dmg: i32,
crit: f32,
radius: f32,
arc: f32,
lifetime: f32,
color: rl.Color,
hit: [MAX_ATTACK_HITS]Handle,
hit_count: u8,
}
init_attack :: proc(state: ^GameState, a: Attack) -> Handle {
return spawn(state, Entity{v = a})
}
attack_update :: proc(state: ^GameState, a: ^Attack, h: Handle, dt: f32) {
if a.parent != {} {
if pe := get_entity(state, a.parent); pe != nil {
a.pos = get_base(pe).pos
} else {
a.marked = true
return
}
}
it := hm.iterator_make(&state.entities)
for e, eh in hm.iterate(&it) {
#partial switch &v in e.v {
case Enemy:
if v.hp <= 0 || v.marked do continue
if attack_has_hit(a, eh) do continue
to := v.pos - a.pos
dist := rl.Vector2Length(to)
if dist > a.radius do continue
// дуга: угол между dir и направлением на врага
angle := rl.Vector2Angle(a.dir, rl.Vector2Normalize(to))
if angle > a.arc * 0.5 do continue
dmg := a.dmg
crit_hit := rand.float32() < a.crit
if crit_hit do dmg *= 2
v.hp -= dmg
attack_add_hit(a, eh)
}
}
if a.lifetime > 0 {
a.lifetime -= dt
} else {
a.marked = true
}
}
attack_has_hit :: proc(a: ^Attack, h: Handle) -> bool {
for i in 0 ..< a.hit_count {
if a.hit[i] == h do return true
}
return false
}
attack_add_hit :: proc(a: ^Attack, h: Handle) {
if a.hit_count >= MAX_ATTACK_HITS do return
a.hit[a.hit_count] = h
a.hit_count += 1
}
+71
View File
@@ -0,0 +1,71 @@
package main
import math "core:math"
import rand "core:math/rand"
import rl "vendor:raylib"
EnemyKind :: enum {
Grunt,
Brute,
Scout,
}
Enemy :: struct {
using actor: Actor,
touch_dmg: i32,
kind: EnemyKind,
xp_reward: i32,
}
make_enemy :: proc(pos: rl.Vector2, kind: EnemyKind) -> Entity {
e: Enemy
switch kind {
case .Grunt:
e = Enemy{actor = Actor{hp = 45, max_hp = 45, speed = 55}, touch_dmg = 8, kind = .Grunt, xp_reward = 12}
e.base.gfx = {0 = GfxCircle{pos = {}, radius = 11, color = {220, 80, 80, 255}}}
case .Brute:
e = Enemy{actor = Actor{hp = 140, max_hp = 140, speed = 32}, touch_dmg = 20, kind = .Brute, xp_reward = 30}
e.base.gfx = {0 = GfxCircle{pos = {}, radius = 17, color = {160, 60, 220, 255}}}
case .Scout:
e = Enemy{actor = Actor{hp = 25, max_hp = 25, speed = 120}, touch_dmg = 5, kind = .Scout, xp_reward = 8}
e.base.gfx = {0 = GfxCircle{pos = {}, radius = 8, color = {240, 180, 60, 255}}}
}
e.base.pos = pos
e.base.col_size = 16
return Entity{v = e}
}
enemy_update :: proc(state: ^GameState, e: ^Enemy, h: Handle, dt: f32) {
pe := get_entity(state, state.player)
if pe != nil {
dir := get_base(pe).pos - e.pos
e.vel = rl.Vector2Normalize(dir) * e.speed
}
if e.hp <= 0 && !e.marked {
e.marked = true
enemy_give_rewards(e, state)
}
}
enemy_give_rewards :: proc(e: ^Enemy, state: ^GameState) {
_, p := get_player(state)
if p == nil do return
p.xp += e.xp_reward
add_log(state, "+%d XP", e.xp_reward)
check_level_up(state, p)
if rand.int_max(100) < 60 {
item := random_item(p.level)
spawn(state, make_ground_item(e.pos, item))
add_log(state, "Dropped: %s lvl %d", item.name, item.level)
}
if rand.int_max(100) < 45 {
ang := rand.float32() * 6.28
dist := 500 + rand.float32() * 300
pos := p.pos + rl.Vector2{math.cos_f32(ang), math.sin_f32(ang)} * dist
spawn(state, make_enemy(pos, EnemyKind(rand.int_max(3))))
}
}
+72
View File
@@ -0,0 +1,72 @@
package main
import hm "core:container/handle_map"
import rl "vendor:raylib"
MAX_GFX :: 2
Handle :: hm.Handle32
MAX_ENTITIES :: 1024
BaseEntity :: struct {
pos: rl.Vector2,
vel: rl.Vector2,
col_size: f32,
marked: bool,
gfx: [MAX_GFX]Graphics,
}
Actor :: struct {
using base: BaseEntity,
hp: i32,
max_hp: i32,
speed: f32,
}
EntityVariant :: union {
Player,
Enemy,
GroundItem,
FX,
Attack,
}
Entity :: struct {
handle: Handle,
v: EntityVariant,
}
get_base :: proc(e: ^Entity) -> ^BaseEntity {
#partial switch &v in e.v {
case Player:
return cast(^BaseEntity)&v
case Enemy:
return cast(^BaseEntity)&v
case GroundItem:
return cast(^BaseEntity)&v
case FX:
return cast(^BaseEntity)&v
case Attack:
return cast(^BaseEntity)&v
}
return nil
}
get_actor :: proc(e: ^Entity) -> ^Actor {
#partial switch &v in e.v {
case Player:
return &v.actor
case Enemy:
return &v.actor
}
return nil
}
get_player :: proc(state: ^GameState) -> (^Entity, ^Player) {
e := get_entity(state, state.player)
if e == nil do return nil, nil
p, ok := &e.v.(Player)
if !ok do return nil, nil
return e, p
}
+32
View File
@@ -0,0 +1,32 @@
package main
FX :: struct {
using base: BaseEntity,
parent: Handle,
lifetime: f32,
permanent: bool,
}
init_fx :: proc(state: ^GameState, fx: FX) -> Handle {
return spawn(state, Entity{v = fx})
}
fx_update :: proc(state: ^GameState, fx: ^FX, h: Handle, dt: f32) {
if fx.parent != {} {
if pe := get_entity(state, fx.parent); pe != nil {
fx.pos = get_base(pe).pos
} else {
fx.marked = true
return
}
}
if !fx.permanent {
if fx.lifetime > 0 {
fx.lifetime -= dt
} else {
fx.marked = true
}
}
}
+55
View File
@@ -0,0 +1,55 @@
package main
import rl "vendor:raylib"
Graphics :: union {
GfxNone,
GfxCircle,
GfxRect,
GfxRing,
GfxCircleLines,
GfxTexture,
}
GfxNone :: struct {}
GfxCircle :: struct {
pos: rl.Vector2,
radius: f32,
color: rl.Color,
}
GfxRect :: struct {
pos: rl.Vector2,
size: rl.Vector2,
color: rl.Color,
}
GfxRing :: struct {
pos: rl.Vector2,
inner_radius: f32,
outer_radius: f32,
color: rl.Color,
}
GfxCircleLines :: struct {
pos: rl.Vector2,
radius: f32,
color: rl.Color,
}
GfxTexture :: struct {
}
gfx_draw :: proc(pos: rl.Vector2, g: Graphics) {
#partial switch v in g {
case GfxCircle:
rl.DrawCircleV(pos + v.pos, v.radius, v.color)
case GfxRect:
rl.DrawRectangleV(pos - v.size / 2 + v.pos, v.size, v.color)
case GfxRing:
rl.DrawRing(pos + v.pos, v.inner_radius, v.outer_radius, 0, 360, 0, v.color)
case GfxCircleLines:
rl.DrawCircleLinesV(pos + v.pos, v.radius, v.color)
}
}
+107
View File
@@ -0,0 +1,107 @@
package main
import rand "core:math/rand"
import rl "vendor:raylib"
INV_SIZE :: 16
ItemKind :: enum {
None,
Weapon,
Armor,
Amulet,
Potion,
}
Item :: struct {
kind: ItemKind,
name: string,
level: i32,
dmg: i32,
crit: f32,
atk_spd: f32,
armor: i32,
hp_bonus: i32,
heal: i32,
}
GroundItem :: struct {
using base: BaseEntity,
item: Item,
}
item_color :: proc(kind: ItemKind) -> rl.Color {
#partial switch kind {
case .Weapon:
return {200, 120, 60, 255}
case .Armor:
return {80, 140, 220, 255}
case .Amulet:
return {180, 100, 220, 255}
case .Potion:
return {80, 220, 80, 255}
case .None:
return {120, 120, 120, 255}
}
return {120, 120, 120, 255}
}
item_kind_name :: proc(kind: ItemKind) -> string {
#partial switch kind {
case .Weapon:
return "Sword"
case .Armor:
return "Armor"
case .Amulet:
return "Amulet"
case .Potion:
return "Potion"
case .None:
return "Empty"
}
return "Empty"
}
make_ground_item :: proc(pos: rl.Vector2, item: Item) -> Entity {
return Entity {
v = GroundItem {
base = BaseEntity {
pos = pos,
col_size = 12,
gfx = {0 = GfxCircle{pos = {}, radius = 7, color = item_color(item.kind)}},
},
item = item,
},
}
}
random_item :: proc(level: i32) -> Item {
kind := ItemKind(rand.int_max(4) + 1)
item := Item {
kind = kind,
name = item_kind_name(kind),
level = level,
}
switch kind {
case .Weapon:
item.dmg = 6 + level * 3 + i32(rand.int_max(6))
item.crit = 0.05 + f32(rand.int_max(15)) * 0.01
item.atk_spd = f32(rand.int_max(20)) * 0.01
case .Armor:
item.armor = 3 + level * 2 + i32(rand.int_max(4))
item.hp_bonus = 10 + level * 4 + i32(rand.int_max(10))
case .Amulet:
item.crit = 0.03 + f32(rand.int_max(12)) * 0.01
item.hp_bonus = 5 + level * 2 + i32(rand.int_max(8))
item.atk_spd = f32(rand.int_max(15)) * 0.01
case .Potion:
item.heal = 30 + level * 8 + i32(rand.int_max(15))
case .None:
{}
}
return item
}
item_update :: proc(state: ^GameState, i: ^GroundItem, h: Handle, dt: f32) {
}
+52
View File
@@ -0,0 +1,52 @@
package main
import rl "vendor:raylib"
SCREEN_W :: 1280
SCREEN_H :: 720
TARGET_FPS :: 60
main :: proc() {
rl.InitWindow(SCREEN_W, SCREEN_H, "ARPG Demo")
rl.SetTargetFPS(TARGET_FPS)
defer rl.CloseWindow()
camera := rl.Camera3D{
position = {0, 80, 80},
target = {0, 0, 0},
up = {0, 1, 0},
fovy = 900,
projection = .ORTHOGRAPHIC,
}
state := new(GameState)
state.camera = camera
state.collisions = make([dynamic]CollisionPair)
defer delete(state.collisions)
state.player = spawn(state, make_player())
spawn(state, make_enemy({400, 300}, .Brute))
spawn(state, make_enemy({-400, 200}, .Scout))
spawn(state, make_enemy({300, -350}, .Grunt))
spawn(state, make_enemy({-300, -300}, .Scout))
spawn(state, make_enemy({100, 400}, .Grunt))
spawn(state, make_enemy({-150, 350}, .Grunt))
spawn(state, make_enemy({450, -150}, .Brute))
spawn(state, make_enemy({-450, 100}, .Scout))
for !rl.WindowShouldClose() {
dt := rl.GetFrameTime()
sys_input(state, dt)
sys_update(state, dt)
sys_sweep(state)
if pe := get_entity(state, state.player); pe != nil {
p := get_base(pe).pos
state.camera.target = {p.x, 0, p.y}
state.camera.position = state.camera.target + {600, 600, 600}
}
sys_draw(state)
}
}
+22
View File
@@ -0,0 +1,22 @@
package main
import rl "vendor:raylib"
TILE_W :: 64
MAP_SIZE :: 20
depth_key :: #force_inline proc(pos: rl.Vector2) -> f32 {
return pos.x + pos.y
}
draw_grid_3d :: proc() {
middle := MAP_SIZE / 2
for x in 0 ..< MAP_SIZE {
for y in 0 ..< MAP_SIZE {
wx := f32(x - middle) * TILE_W
wy := f32(y - middle) * TILE_W
color := ((x + y) % 2 == 0) ? rl.Color{64, 64, 76, 255} : rl.Color{51, 51, 63, 255}
rl.DrawPlane({wx, 0, wy}, {TILE_W, TILE_W}, color)
}
}
}
+224
View File
@@ -0,0 +1,224 @@
package main
import rl "vendor:raylib"
SKILL_COUNT :: 3
SkillKind :: enum {
Dash,
Spin,
Heal,
}
Skill :: struct {
kind: SkillKind,
cd: f32,
timer: f32,
}
Equipment :: struct {
weapon: Item,
armor: Item,
amulet: Item,
}
Player :: struct {
using actor: Actor,
level: i32,
xp: i32,
xp_next: i32,
base_dmg: i32,
base_crit: f32,
base_atk_cd: f32,
base_max_hp: i32,
equip: Equipment,
inventory: [INV_SIZE]Item,
inv_count: i32,
selected_slot: i32,
skills: [SKILL_COUNT]Skill,
aim: rl.Vector2,
attack_range: f32,
attack_timer: f32,
invuln_timer: f32,
dash_timer: f32,
}
make_player :: proc() -> Entity {
p := Player {
level = 1,
xp = 0,
xp_next = 50,
base_dmg = 10,
base_crit = 0.1,
base_atk_cd = 0.35,
base_max_hp = 100,
attack_range = 90,
selected_slot = -1,
skills = {
0 = Skill{kind = .Dash, cd = 2.0},
1 = Skill{kind = .Spin, cd = 4.0},
2 = Skill{kind = .Heal, cd = 6.0},
},
}
p.base.pos = {0, 0}
p.base.col_size = 16
p.base.gfx = {0 = GfxCircle{pos = {}, radius = 14, color = {80, 220, 100, 255}}}
p.hp = p.base_max_hp
p.max_hp = p.base_max_hp
p.speed = 220
return Entity{v = p}
}
// ─── Итоговые статы из экипировки ───
player_total_dmg :: proc(p: ^Player) -> i32 {
dmg := p.base_dmg
if p.equip.weapon.kind != .None do dmg += p.equip.weapon.dmg
return dmg
}
player_crit :: proc(p: ^Player) -> f32 {
crit := p.base_crit
if p.equip.weapon.kind != .None do crit += p.equip.weapon.crit
if p.equip.amulet.kind != .None do crit += p.equip.amulet.crit
return min(crit, 0.8)
}
player_atk_cd :: proc(p: ^Player) -> f32 {
bonus: f32 = 0
if p.equip.weapon.kind != .None do bonus += p.equip.weapon.atk_spd
if p.equip.amulet.kind != .None do bonus += p.equip.amulet.atk_spd
return max(p.base_atk_cd * (1 - bonus), 0.1)
}
player_max_hp :: proc(p: ^Player) -> i32 {
hp := p.base_max_hp
if p.equip.armor.kind != .None do hp += p.equip.armor.hp_bonus
if p.equip.amulet.kind != .None do hp += p.equip.amulet.hp_bonus
return hp
}
recalc_stats :: proc(p: ^Player) {
p.max_hp = player_max_hp(p)
if p.hp > p.max_hp do p.hp = p.max_hp
}
// ─── Экипировка / использование ───
equip_item :: proc(state: ^GameState, p: ^Player, item: Item) {
switch item.kind {
case .Weapon:
if p.equip.weapon.kind != .None {
add_log(state, "Dropped %s", p.equip.weapon.name)
}
p.equip.weapon = item
case .Armor:
if p.equip.armor.kind != .None {
add_log(state, "Dropped %s", p.equip.armor.name)
}
p.equip.armor = item
case .Amulet:
if p.equip.amulet.kind != .None {
add_log(state, "Dropped %s", p.equip.amulet.name)
}
p.equip.amulet = item
case .Potion:
p.hp = min(p.hp + item.heal, player_max_hp(p))
add_log(state, "Drank potion +%d HP", item.heal)
return
case .None:
return
}
recalc_stats(p)
add_log(state, "Equipped: %s", item.name)
}
// ─── Уровни ───
check_level_up :: proc(state: ^GameState, p: ^Player) {
for p.xp >= p.xp_next {
p.xp -= p.xp_next
p.level += 1
p.xp_next = i32(f32(p.xp_next) * 1.4)
p.base_dmg += 2
p.base_max_hp += 12
p.hp = min(p.hp + 20, player_max_hp(p))
recalc_stats(p)
add_log(state, "LEVEL %d! +dmg +HP", p.level)
}
}
// ─── Ввод ───
sys_input :: proc(state: ^GameState, dt: f32) {
_, p := get_player(state)
if p == nil do return
// направление к мыши (по земле)
ray := rl.GetScreenToWorldRay(rl.GetMousePosition(), state.camera)
if ray.direction.y != 0 {
t := -ray.position.y / ray.direction.y
hit := ray.position + ray.direction * t
p.aim = rl.Vector2Normalize({hit.x - p.pos.x, hit.z - p.pos.y})
}
// движение (45° поворот под изометрию)
move: rl.Vector2
if rl.IsKeyDown(.W) || rl.IsKeyDown(.UP) do move += {-1, -1}
if rl.IsKeyDown(.S) || rl.IsKeyDown(.DOWN) do move += {1, 1}
if rl.IsKeyDown(.A) || rl.IsKeyDown(.LEFT) do move += {-1, 1}
if rl.IsKeyDown(.D) || rl.IsKeyDown(.RIGHT) do move += {1, -1}
if p.dash_timer > 0 {
p.dash_timer -= dt
} else {
p.vel = rl.Vector2Normalize(move) * p.speed
}
// инвентарь: клик по слоту (не атакуем при клике по UI)
if rl.IsMouseButtonPressed(.LEFT) {
if slot := ui_inventory_slot_at(rl.GetMousePosition()); slot >= 0 {
if slot < int(p.inv_count) {
item := p.inventory[slot]
equip_item(state, p, item)
for i in slot ..< int(p.inv_count) - 1 {
p.inventory[i] = p.inventory[i + 1]
}
p.inventory[int(p.inv_count) - 1] = Item{}
p.inv_count -= 1
}
return
}
}
// атака ЛКМ — сектор к мыши
if p.attack_timer > 0 do p.attack_timer -= dt
if rl.IsMouseButtonPressed(.LEFT) && p.attack_timer <= 0 {
p.attack_timer = player_atk_cd(p)
init_attack(state, Attack{
parent = state.player,
dir = p.aim,
dmg = player_total_dmg(p),
crit = player_crit(p),
radius = p.attack_range,
arc = 1.3,
lifetime = player_atk_cd(p),
color = {255, 255, 255, 60},
})
}
// навыки
if rl.IsKeyPressed(.Q) do cast_skill(state, p, .Dash)
if rl.IsKeyPressed(.W) do cast_skill(state, p, .Spin)
if rl.IsKeyPressed(.E) do cast_skill(state, p, .Heal)
}
// ─── Тик ───
player_update :: proc(state: ^GameState, p: ^Player, h: Handle, dt: f32) {
if p.invuln_timer > 0 do p.invuln_timer -= dt
for &s in p.skills {
if s.timer > 0 do s.timer -= dt
}
}
+255
View File
@@ -0,0 +1,255 @@
package main
import "core:fmt"
import math "core:math"
import hm "core:container/handle_map"
import rl "vendor:raylib"
SQRT2_INV :: 0.7071067811865476
LOG_SIZE :: 8
CollisionPair :: struct {
a: Handle,
b: Handle,
}
GameState :: struct {
player: Handle,
camera: rl.Camera3D,
entities: hm.Static_Handle_Map(MAX_ENTITIES, Entity, Handle),
collisions: [dynamic]CollisionPair,
log: [LOG_SIZE]string,
log_count: i32,
kills: i32,
}
get_entity :: proc(state: ^GameState, h: Handle) -> ^Entity {
e, ok := hm.get(&state.entities, h)
return e if ok else nil
}
spawn :: proc(state: ^GameState, e: Entity) -> Handle {
return hm.add(&state.entities, e)
}
add_log :: proc(state: ^GameState, format: string, args: ..any) {
msg := fmt.tprintf(format, ..args)
if state.log_count >= LOG_SIZE {
for i in 0 ..< LOG_SIZE - 1 {
state.log[i] = state.log[i + 1]
}
state.log[LOG_SIZE - 1] = msg
} else {
state.log[state.log_count] = msg
state.log_count += 1
}
}
entity_rect :: #force_inline proc(b: ^BaseEntity) -> rl.Rectangle {
return rl.Rectangle{b.pos.x - b.col_size / 2, b.pos.y - b.col_size / 2, b.col_size, b.col_size}
}
check_col :: proc(a, b: ^BaseEntity) -> (overlap: rl.Rectangle, colliding: bool) {
c := rl.GetCollisionRec(entity_rect(a), entity_rect(b))
if c.width <= 0 || c.height <= 0 do return rl.Rectangle{}, false
return c, true
}
resolve_aabb :: proc(a, b: ^BaseEntity) -> bool {
c, ok := check_col(a, b)
if !ok do return false
if c.width < c.height {
sign: f32 = 1 if a.pos.x < b.pos.x else -1
a.pos.x -= sign * c.width * 0.5
b.pos.x += sign * c.width * 0.5
} else {
sign: f32 = 1 if a.pos.y < b.pos.y else -1
a.pos.y -= sign * c.height * 0.5
b.pos.y += sign * c.height * 0.5
}
return true
}
sys_update :: proc(state: ^GameState, dt: f32) {
it := hm.iterator_make(&state.entities)
for e, h in hm.iterate(&it) {
#partial switch &v in e.v {
case Player:
player_update(state, &v, h, dt)
case Enemy:
enemy_update(state, &v, h, dt)
case GroundItem:
item_update(state, &v, h, dt)
}
}
it2 := hm.iterator_make(&state.entities)
for e, _ in hm.iterate(&it2) {
b := get_base(e)
b.pos += b.vel * dt
}
it3 := hm.iterator_make(&state.entities)
for e, h in hm.iterate(&it3) {
#partial switch &v in e.v {
case FX:
fx_update(state, &v, h, dt)
case Attack:
attack_update(state, &v, h, dt)
}
}
resolve_collisions(state)
apply_collision_effects(state)
}
sys_sweep :: proc(state: ^GameState) {
it := hm.iterator_make(&state.entities)
for e, h in hm.iterate(&it) {
if get_base(e).marked {
hm.remove(&state.entities, h)
}
}
}
entity_draw :: proc(state: ^GameState, e: ^Entity) {
b := get_base(e)
sp := rl.GetWorldToScreen({b.pos.x, 0, b.pos.y}, state.camera)
scale := SCREEN_H / state.camera.fovy
#partial switch v in b.gfx[0] {
case GfxCircle:
rl.DrawCircleV(sp, v.radius * scale, v.color)
case:
}
}
ground_effect_draw :: proc(state: ^GameState, e: ^Entity) {
b := get_base(e)
#partial switch v in e.v {
case Attack:
draw_arc_sector(b.pos, v.dir, v.radius, v.arc, v.color)
case GroundItem:
rl.DrawCylinder({b.pos.x, 0.02, b.pos.y}, 8, 8, 0.03, 24, {255, 255, 255, 30})
case:
}
}
draw_arc_sector :: proc(pos: rl.Vector2, dir: rl.Vector2, radius: f32, arc: f32, color: rl.Color) {
SEGMENTS :: 24
base := math.atan2_f32(dir.y, dir.x)
start := base - arc * 0.5
for i in 0 ..< SEGMENTS {
a1 := start + arc * f32(i) / f32(SEGMENTS)
a2 := start + arc * f32(i + 1) / f32(SEGMENTS)
p1 := pos + rl.Vector2{math.cos_f32(a1), math.sin_f32(a1)} * radius
p2 := pos + rl.Vector2{math.cos_f32(a2), math.sin_f32(a2)} * radius
rl.DrawTriangle3D(
{pos.x, 0.03, pos.y},
{p1.x, 0.03, p1.y},
{p2.x, 0.03, p2.y},
color,
)
}
}
sys_draw :: proc(state: ^GameState) {
rl.BeginDrawing()
rl.ClearBackground({20, 20, 30, 255})
rl.BeginMode3D(state.camera)
draw_grid_3d()
if ray := rl.GetScreenToWorldRay(rl.GetMousePosition(), state.camera); ray.direction.y != 0 {
t := -ray.position.y / ray.direction.y
hit := ray.position + ray.direction * t
rl.DrawCylinder({hit.x, 0.02, hit.z}, 4, 4, 0.02, 16, {255, 255, 255, 90})
}
it := hm.iterator_make(&state.entities)
for e, _ in hm.iterate(&it) {
ground_effect_draw(state, e)
}
rl.EndMode3D()
scale := SCREEN_H / state.camera.fovy
it2 := hm.iterator_make(&state.entities)
for e, _ in hm.iterate(&it2) {
entity_draw(state, e)
if a := get_actor(e); a != nil {
b := get_base(e)
sp := rl.GetWorldToScreen({b.pos.x, 0, b.pos.y}, state.camera)
hp_ratio := f32(a.hp) / f32(a.max_hp)
bar_w := b.col_size * 3 * scale
bar_pos := sp - rl.Vector2{bar_w / 2, b.col_size * 2 * scale + 12}
rl.DrawRectangleV(bar_pos, {bar_w, 5}, {60, 60, 60, 255})
rl.DrawRectangleV(bar_pos, {bar_w * hp_ratio, 5}, {220, 40, 40, 255})
}
}
ui_draw(state)
rl.EndDrawing()
}
resolve_collisions :: proc(state: ^GameState) {
clear(&state.collisions)
hs: [MAX_ENTITIES]Handle
n := 0
it := hm.iterator_make(&state.entities)
for _, h in hm.iterate(&it) {
hs[n] = h
n += 1
}
for i in 0 ..< n {
for j in i + 1 ..< n {
a := get_entity(state, hs[i])
b := get_entity(state, hs[j])
if resolve_aabb(get_base(a), get_base(b)) {
append(&state.collisions, CollisionPair{a = hs[i], b = hs[j]})
}
}
}
}
apply_collision_effects :: proc(state: ^GameState) {
_, p := get_player(state)
for c in state.collisions {
other_h: Handle
if c.a == state.player {
other_h = c.b
} else if c.b == state.player {
other_h = c.a
} else do continue
e := get_entity(state, other_h)
if e == nil do continue
#partial switch &v in e.v {
case Enemy:
if p != nil && p.invuln_timer <= 0 {
dmg := v.touch_dmg
armor: i32 = 0
if p.equip.armor.kind != .None do armor = p.equip.armor.armor
dmg = max(dmg - armor / 2, 1)
p.hp -= dmg
p.invuln_timer = 0.5
if p.hp <= 0 do p.hp = 0
}
case GroundItem:
if p != nil && !v.marked && p.inv_count < INV_SIZE {
p.inventory[p.inv_count] = v.item
p.inv_count += 1
v.marked = true
add_log(state, "Picked up: %s", v.item.name)
}
}
}
}
+44
View File
@@ -0,0 +1,44 @@
package main
import rl "vendor:raylib"
// Навыки: Q — рывок, W — спин (AoE), E — хил
cast_skill :: proc(state: ^GameState, p: ^Player, kind: SkillKind) {
slot := skill_slot(p, kind)
if slot == -1 do return
s := &p.skills[slot]
if s.timer > 0 do return
s.timer = s.cd
switch kind {
case .Dash:
p.dash_timer = 0.2
p.vel = p.aim * 750
p.invuln_timer = 0.25
case .Spin:
init_attack(state, Attack{
parent = state.player,
dir = {1, 0},
dmg = player_total_dmg(p) * 2,
crit = player_crit(p),
radius = p.attack_range * 1.2,
arc = 6.28,
lifetime = 0.4,
color = {255, 200, 60, 80},
})
case .Heal:
p.hp = min(p.hp + player_max_hp(p) / 3, player_max_hp(p))
init_fx(state, FX{
parent = state.player,
lifetime = 0.5,
base = BaseEntity{gfx = {0 = GfxCircle{pos = {}, radius = 20, color = {80, 255, 120, 50}}}},
})
}
}
skill_slot :: proc(p: ^Player, kind: SkillKind) -> int {
for i in 0 ..< SKILL_COUNT {
if p.skills[i].kind == kind do return i
}
return -1
}
+155
View File
@@ -0,0 +1,155 @@
package main
import rl "vendor:raylib"
ui_draw_world_hp_bar :: proc(b: ^BaseEntity, actor: ^Actor) {
hp_ratio := f32(actor.hp) / f32(actor.max_hp)
bar_w := b.col_size * 3
bar_pos := b.pos + rl.Vector2{-bar_w / 2, -b.col_size - 10}
rl.DrawRectangleV(bar_pos, {bar_w, 4}, {60, 60, 60, 255})
rl.DrawRectangleV(bar_pos, {bar_w * hp_ratio, 4}, {220, 40, 40, 255})
}
// ─── Инвентарь: геометрия слотов ───
INV_SLOT_SIZE :: 44
INV_PAD :: 6
INV_COLS :: 4
inv_panel_rect :: proc() -> rl.Rectangle {
w := INV_COLS * (INV_SLOT_SIZE + INV_PAD) + INV_PAD
h := (INV_SIZE / INV_COLS) * (INV_SLOT_SIZE + INV_PAD) + INV_PAD
return rl.Rectangle{f32(SCREEN_W) - f32(w) - 12, 60, f32(w), f32(h)}
}
inv_slot_rect :: proc(i: int) -> rl.Rectangle {
panel := inv_panel_rect()
col := i % INV_COLS
row := i / INV_COLS
x := panel.x + INV_PAD + f32(col) * (INV_SLOT_SIZE + INV_PAD)
y := panel.y + INV_PAD + f32(row) * (INV_SLOT_SIZE + INV_PAD)
return rl.Rectangle{x, y, INV_SLOT_SIZE, INV_SLOT_SIZE}
}
ui_inventory_slot_at :: proc(mouse: rl.Vector2) -> int {
if !rl.CheckCollisionPointRec(mouse, inv_panel_rect()) do return -1
for i in 0 ..< INV_SIZE {
if rl.CheckCollisionPointRec(mouse, inv_slot_rect(i)) do return i
}
return -1
}
// ─── Отрисовка ───
ui_draw :: proc(state: ^GameState) {
_, p := get_player(state)
if p == nil do return
// HP + XP
hp_ratio := f32(p.hp) / f32(p.max_hp)
rl.DrawRectangle(12, 12, 300, 22, {40, 40, 40, 255})
rl.DrawRectangle(12, 12, i32(300 * hp_ratio), 22, {200, 40, 40, 255})
rl.DrawText(rl.TextFormat("%d/%d HP", p.hp, p.max_hp), 22, 16, 14, rl.RAYWHITE)
xp_ratio := f32(p.xp) / f32(p.xp_next)
rl.DrawRectangle(12, 38, 300, 12, {40, 40, 40, 255})
rl.DrawRectangle(12, 38, i32(300 * xp_ratio), 12, {80, 160, 220, 255})
rl.DrawText(rl.TextFormat("Lvl %d %d/%d XP Kills: %d", p.level, p.xp, p.xp_next, state.kills), 22, 40, 10, rl.WHITE)
// статы
rl.DrawText(rl.TextFormat("DMG %d CRIT %d%% SPD x%.2f", player_total_dmg(p), i32(player_crit(p) * 100), 1 / player_atk_cd(p)), 12, 56, 14, {220, 220, 220, 255})
// навыки
for i in 0 ..< SKILL_COUNT {
s := p.skills[i]
rect := rl.Rectangle{f32(12 + i * 56), f32(SCREEN_H - 60), 48, 48}
rl.DrawRectangleRec(rect, {50, 50, 60, 255})
key: cstring = "Q"
switch s.kind {
case .Dash:
key = "Q"
case .Spin:
key = "W"
case .Heal:
key = "E"
}
rl.DrawText(key, i32(rect.x) + 6, i32(rect.y) + 4, 14, rl.RAYWHITE)
if s.timer > 0 {
ratio := s.timer / s.cd
rl.DrawRectangleRec(rect, {0, 0, 0, u8(160 * ratio)})
rl.DrawText(rl.TextFormat("%.1f", s.timer), i32(rect.x) + 8, i32(rect.y) + 28, 12, rl.RAYWHITE)
}
}
// инвентарь
panel := inv_panel_rect()
rl.DrawRectangleRec(panel, {35, 35, 40, 220})
rl.DrawRectangleLinesEx(panel, 1, {90, 90, 90, 255})
for i in 0 ..< INV_SIZE {
rect := inv_slot_rect(i)
rl.DrawRectangleRec(rect, {55, 55, 60, 255})
rl.DrawRectangleLinesEx(rect, 1, {90, 90, 90, 255})
if i < int(p.inv_count) {
item := p.inventory[i]
rl.DrawCircleV({rect.x + rect.width / 2, rect.y + rect.height / 2}, 10, item_color(item.kind))
rl.DrawText(rl.TextFormat("%d", item.level), i32(rect.x) + 4, i32(rect.y) + 28, 10, rl.WHITE)
}
}
// тултип выбранного (под курсором)
if slot := ui_inventory_slot_at(rl.GetMousePosition()); slot >= 0 && slot < int(p.inv_count) {
item := p.inventory[slot]
draw_item_tooltip(item, rl.GetMousePosition())
}
// лог
for i in 0 ..< state.log_count {
rl.DrawText(rl.TextFormat("%s", state.log[i]), 12, SCREEN_H - 160 + i * 18, 14, {200, 200, 200, 255})
}
rl.DrawText("LMB attack Q dash W spin E heal", 12, SCREEN_H - 28, 12, {120, 120, 120, 255})
}
draw_item_tooltip :: proc(item: Item, at: rl.Vector2) {
lines: [8]cstring
count := 0
lines[count] = rl.TextFormat("%s lvl %d", item.name, item.level)
count += 1
if item.dmg > 0 {
lines[count] = rl.TextFormat("Damage: %d", item.dmg)
count += 1
}
if item.crit > 0 {
lines[count] = rl.TextFormat("Crit: %d%%", i32(item.crit * 100))
count += 1
}
if item.atk_spd > 0 {
lines[count] = rl.TextFormat("Atk speed: +%d%%", i32(item.atk_spd * 100))
count += 1
}
if item.armor > 0 {
lines[count] = rl.TextFormat("Armor: %d", item.armor)
count += 1
}
if item.hp_bonus > 0 {
lines[count] = rl.TextFormat("+HP: %d", item.hp_bonus)
count += 1
}
if item.heal > 0 {
lines[count] = rl.TextFormat("Heals: %d", item.heal)
count += 1
}
tip_x := at.x + 16
tip_y := at.y + 16
if tip_x + 180 > SCREEN_W do tip_x = at.x - 196
if tip_y + f32(count) * 18 + 8 > SCREEN_H do tip_y = at.y - f32(count) * 18 - 8
rl.DrawRectangle(i32(tip_x), i32(tip_y), 180, i32(count * 18 + 8), {20, 20, 25, 235})
rl.DrawRectangleLines(i32(tip_x), i32(tip_y), 180, i32(count * 18 + 8), {150, 150, 150, 255})
for i in 0 ..< count {
rl.DrawText(lines[i], i32(tip_x) + 6, i32(tip_y) + 6 + i32(i) * 18, 13, rl.RAYWHITE)
}
}