3d
This commit is contained in:
+31
-20
@@ -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 {
|
||||||
enemy_update :: proc(state: ^GameState, e: ^Enemy, h: Handle, dt: f32) {
|
using actor: Actor,
|
||||||
pe := get_entity(state, state.player)
|
touch_dmg: i32,
|
||||||
if pe == nil do return
|
|
||||||
|
|
||||||
dir := get_base(pe).pos - e.pos
|
|
||||||
set_vel(&e.base, dir, e.speed)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Мёртвые враги → лут
|
make_enemy :: proc(pos: rl.Vector2) -> Entity {
|
||||||
sys_death :: proc(state: ^GameState) {
|
return Entity {
|
||||||
it := hm.iterator_make(&state.entities)
|
v = Enemy {
|
||||||
for e, _ in hm.iterate(&it) {
|
actor = Actor {
|
||||||
#partial switch &v in e.v {
|
base = BaseEntity {
|
||||||
case Enemy:
|
pos = pos,
|
||||||
if v.hp <= 0 && !v.marked {
|
col_size = 16,
|
||||||
v.marked = true
|
gfx = {0 = GfxCircle{pos = {}, radius = 11, color = {220, 70, 70, 255}}},
|
||||||
spawn(state, make_ground_item(v.pos))
|
},
|
||||||
}
|
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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -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
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+72
-20
@@ -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 {
|
||||||
gfx = {
|
base = BaseEntity {
|
||||||
0 = GfxRing{inner_radius = p.attack_range * 0.7, outer_radius = p.attack_range, color = {255, 255, 255, 40}},
|
gfx = {
|
||||||
1 = GfxCircleLines{radius = p.attack_range, color = {255, 255, 255, 80}},
|
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}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|||||||
+68
-233
@@ -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) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(e: ^Entity) {
|
||||||
|
b := get_base(e)
|
||||||
sys_move :: proc(state: ^GameState, dt: f32) {
|
for g in b.gfx {
|
||||||
it := hm.iterator_make(&state.entities)
|
gfx_draw(b.pos, g)
|
||||||
for e, _ in hm.iterate(&it) {
|
|
||||||
b := get_base(e)
|
|
||||||
b.pos += b.vel * dt
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Коллизии ───
|
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()
|
||||||
|
|
||||||
CollisionPair :: struct {
|
ui_draw(state)
|
||||||
a: Handle,
|
rl.EndDrawing()
|
||||||
b: Handle,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sys_collisions :: proc(state: ^GameState) {
|
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 {
|
||||||
p.invuln_timer = 0.5
|
case Enemy:
|
||||||
if p.hp <= 0 do p.hp = 0
|
if p != nil && p.invuln_timer <= 0 {
|
||||||
break // один урон за кадр, дальше неуязвим
|
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 {
|
||||||
sys_sweep :: proc(state: ^GameState) {
|
p.hp = min(p.hp + v.heal, p.max_hp)
|
||||||
it := hm.iterator_make(&state.entities)
|
v.marked = true
|
||||||
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
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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) {
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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})
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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))))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -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) {
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user