This commit is contained in:
2026-08-05 16:57:44 +03:00
parent 2ac0413d78
commit da416fe5e2
27 changed files with 92 additions and 1293 deletions
+9
View File
@@ -0,0 +1,9 @@
-- .nvim.lua — локальные настройки проекта
-- Создан :LocalInit. Автоматически грузится nvim-config-local (первый раз — через :ConfigLocalTrust).
-- Раннер: команда, выполняемая в нижнем терминале edgy
-- по :Run или <leader>dr. Можно строкой или функцией.
require("core.runner").setup({
cmd = "make run",
focus = true, -- фокус на терминал при запуске
})
+1 -1
View File
@@ -1,2 +1,2 @@
run:
odin run ./src/
odin run ./src3/
+1
View File
@@ -4,3 +4,4 @@ t.git
https://craftpix.net/freebies/free-basic-pixel-art-fantasy-icons-16x16-for-ui/
https://craftpix.net/freebies/free-water-and-fire-magic-sprite-vector-pack/
https://craftpix.net/freebies/free-swordsman-1-3-level-pixel-top-down-sprite-character-pack/
-38
View File
@@ -1,38 +0,0 @@
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))
}
}
-69
View File
@@ -1,69 +0,0 @@
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
@@ -1,32 +0,0 @@
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
@@ -1,55 +0,0 @@
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
@@ -1,22 +0,0 @@
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) {
}
-38
View File
@@ -1,38 +0,0 @@
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")
rl.SetTargetFPS(TARGET_FPS)
defer rl.CloseWindow()
state := GameState {
camera = {zoom = 1.5, offset = {SCREEN_W / 2, SCREEN_H / 2}},
}
state.collisions = make([dynamic]CollisionPair)
defer delete(state.collisions)
state.player = spawn(&state, make_player())
spawn(&state, make_enemy({600, 500}))
spawn(&state, make_enemy({200, 500}))
spawn(&state, make_enemy({600, 100}))
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 {
state.camera.target = get_base(pe).pos
}
sys_draw(&state)
}
}
-48
View File
@@ -1,48 +0,0 @@
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)
}
}
}
-94
View File
@@ -1,94 +0,0 @@
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 = {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 input.attack && p.attack_timer <= 0 {
p.attack_timer = p.attack_cd
init_fx(
state,
FX {
base = BaseEntity {
gfx = {
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,
},
)
it := hm.iterator_make(&state.entities)
for e, _ in hm.iterate(&it) {
#partial switch &v in e.v {
case Enemy:
if rl.Vector2Distance(p.pos, v.pos) <= p.attack_range {
v.hp -= p.attack_dmg
}
}
}
}
}
player_update :: proc(state: ^GameState, p: ^Player, h: Handle, dt: f32) {
if p.invuln_timer > 0 do p.invuln_timer -= dt
}
-174
View File
@@ -1,174 +0,0 @@
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.Camera2D,
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)
}
}
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)
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)
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
@@ -1,26 +0,0 @@
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})
}
-62
View File
@@ -1,62 +0,0 @@
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
@@ -1,38 +0,0 @@
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
@@ -1,72 +0,0 @@
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
@@ -1,32 +0,0 @@
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
@@ -1,55 +0,0 @@
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
@@ -1,22 +0,0 @@
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
@@ -1,48 +0,0 @@
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
@@ -1,22 +0,0 @@
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
@@ -1,85 +0,0 @@
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
@@ -1,202 +0,0 @@
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
@@ -1,26 +0,0 @@
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})
}
+61 -28
View File
@@ -1,6 +1,5 @@
package main
import "core:fmt"
import rl "vendor:raylib"
Graphics :: union {
@@ -44,11 +43,11 @@ GfxTexture :: struct {}
GfxAnimatedSprite :: struct {
pos: rl.Vector2,
rect: rl.Rectangle,
tex: ^rl.Texture2D,
frame: u32,
atlas: ^Atlas,
frame: i32,
speed: f32,
current_time: f32,
update: proc(g: ^GfxAnimatedSprite, dt: f32),
}
PlayerTexFiles :: enum {
@@ -60,33 +59,65 @@ PlayerTexFiles :: enum {
ATTACK,
}
GraphicsAssetStore :: struct {
playerTex: map[PlayerTexFiles]rl.Texture2D,
Atlas :: struct {
tex: rl.Texture2D,
frame_size: rl.Vector2,
cols: i32,
rows: i32,
frames: i32,
get_offset: proc(a: ^Atlas, f: i32) -> rl.Vector2,
}
gfx_get :: proc(g: ^GraphicsAssetStore, f: PlayerTexFiles) -> ^rl.Texture2D {
GraphicsAssetStore :: struct {
playerTex: map[PlayerTexFiles]Atlas,
}
player_atlas_get :: proc(g: ^GraphicsAssetStore, f: PlayerTexFiles) -> ^Atlas {
return &g.playerTex[f]
}
gfx_init :: proc() -> GraphicsAssetStore {
st := GraphicsAssetStore{}
st.playerTex = make(map[PlayerTexFiles]rl.Texture2D)
st.playerTex[.IDLE] = rl.LoadTexture("assets/3rdparty/player_idle.png")
st.playerTex = make(map[PlayerTexFiles]Atlas)
st.playerTex[.IDLE] = init_atlas(
texture_path = "assets/3rdparty/player_idle.png",
size = rl.Vector2{64, 64},
)
return st
}
init_atlas :: proc(texture_path: cstring, size: rl.Vector2) -> Atlas {
atlas := Atlas {
tex = rl.LoadTexture(texture_path),
frame_size = size,
get_offset = get_offset,
}
atlas.rows = atlas.tex.height / (i32(size.y))
atlas.cols = atlas.tex.width / i32(size.x)
atlas.frames = atlas.cols * atlas.rows
return atlas
}
get_offset :: proc(a: ^Atlas, f: i32) -> rl.Vector2 {
col := f % a.cols
row := f / a.cols
return rl.Vector2{f32(col) * a.frame_size.x, f32(row) * a.frame_size.y}
}
gfx_free :: proc(g: GraphicsAssetStore) {
for _, tex in g.playerTex {
rl.UnloadTexture(tex)
rl.UnloadTexture(tex.tex)
}
delete(g.playerTex)
}
gfx_draw :: proc(pos: rl.Vector2, g: ^Graphics) {
#partial switch &v in g {
#partial switch v in g {
case GfxCircle:
rl.DrawCircleV(pos + v.pos, v.radius, v.color)
case GfxRect:
@@ -96,29 +127,31 @@ gfx_draw :: proc(pos: rl.Vector2, g: ^Graphics) {
case GfxCircleLines:
rl.DrawCircleLinesV(pos + v.pos, v.radius, v.color)
case GfxAnimatedSprite:
fmt.println(pos.x, pos.y)
//TODO: Count from L to R row-wize
v.rect.x = v.rect.width * f32(v.frame)
v.rect.y = v.rect.height * f32(v.frame)
frame_offset := v.atlas->get_offset(v.frame)
rl.DrawTexturePro(
v.tex^,
v.rect,
v.atlas^.tex,
rl.Rectangle {
pos.x - v.rect.width * 3 / 2,
pos.y - v.rect.height * 3 / 2,
v.rect.width * 3,
v.rect.height * 3,
frame_offset.x,
frame_offset.y,
v.atlas.frame_size.x,
v.atlas.frame_size.y,
},
rl.Rectangle {
pos.x - v.atlas.frame_size.x * 3 / 2,
pos.y - v.atlas.frame_size.y * 3 / 2,
v.atlas.frame_size.x * 3,
v.atlas.frame_size.y * 3,
},
rl.Vector2{0, 0},
0,
rl.WHITE,
)
v.current_time += rl.GetFrameTime()
if v.current_time > v.speed {
v.current_time = v.current_time - v.speed
v.frame += 1
}
// v.current_time += rl.GetFrameTime()
// if v.current_time > v.speed {
// v.current_time = v.current_time - v.speed
// v.frame += 1
// }
// rl.DrawTextureRec(v.tex, v.rect, rl.Vector2{pos.x, pos.y}, rl.Color{})
}
}
+1 -1
View File
@@ -4,7 +4,7 @@ import rl "vendor:raylib"
SCREEN_W :: 1280
SCREEN_H :: 720
TARGET_FPS :: 60
TARGET_FPS :: 120 // test edit
main :: proc() {
rl.InitWindow(SCREEN_W, SCREEN_H, "ARPG Demo")
+19 -3
View File
@@ -66,10 +66,10 @@ make_player :: proc(gfx: ^GraphicsAssetStore) -> Entity {
0 = GfxCircle{pos = {}, radius = 14, color = {80, 220, 100, 255}},
1 = GfxAnimatedSprite {
pos = {},
rect = {0, 0, 64, 64},
tex = gfx_get(gfx, .IDLE),
atlas = player_atlas_get(gfx, .IDLE),
frame = 0,
speed = 1,
speed = 0.5,
update = update_player_sprite,
},
}
p.hp = p.base_max_hp
@@ -78,6 +78,15 @@ make_player :: proc(gfx: ^GraphicsAssetStore) -> Entity {
return Entity{v = p}
}
update_player_sprite :: proc(g: ^GfxAnimatedSprite, dt: f32) {
g.current_time += dt
if g.current_time > g.speed {
g.current_time = g.current_time - g.speed
g.frame += 1
if g.frame > 3 do g.frame = 0
}
}
// ─── Итоговые статы из экипировки ───
player_total_dmg :: proc(p: ^Player) -> i32 {
@@ -233,4 +242,11 @@ player_update :: proc(state: ^GameState, p: ^Player, h: Handle, dt: f32) {
for &s in p.skills {
if s.timer > 0 do s.timer -= dt
}
for &gfx in p.gfx {
#partial switch &v in gfx {
case GfxAnimatedSprite:
v->update(dt)
}
}
}