From 06e41fc7da8a6dd63539b88e7935ec65fe2e0d0f Mon Sep 17 00:00:00 2001 From: Alexey Bagno Date: Mon, 3 Aug 2026 21:11:00 +0300 Subject: [PATCH] 3d --- src/enemy.odin | 51 ++++---- src/entity.odin | 69 +++++++++++ src/fx.odin | 32 +++++ src/gfx.odin | 53 --------- src/item.odin | 36 +++--- src/main.odin | 20 ++-- src/map.odin | 48 ++++++++ src/player.odin | 92 +++++++++++---- src/scene.odin | 301 +++++++++++------------------------------------ src/ui.odin | 2 +- src2/attack.odin | 62 ++++++++++ src2/enemy.odin | 38 ++++++ src2/entity.odin | 72 ++++++++++++ src2/fx.odin | 32 +++++ src2/gfx.odin | 55 +++++++++ src2/item.odin | 22 ++++ src2/main.odin | 48 ++++++++ src2/map.odin | 22 ++++ src2/player.odin | 85 +++++++++++++ src2/scene.odin | 202 +++++++++++++++++++++++++++++++ src2/ui.odin | 26 ++++ src3/attack.odin | 79 +++++++++++++ src3/enemy.odin | 71 +++++++++++ src3/entity.odin | 72 ++++++++++++ src3/fx.odin | 32 +++++ src3/gfx.odin | 55 +++++++++ src3/item.odin | 107 +++++++++++++++++ src3/main.odin | 52 ++++++++ src3/map.odin | 22 ++++ src3/player.odin | 224 +++++++++++++++++++++++++++++++++++ src3/scene.odin | 255 +++++++++++++++++++++++++++++++++++++++ src3/skill.odin | 44 +++++++ src3/ui.odin | 155 ++++++++++++++++++++++++ 33 files changed, 2180 insertions(+), 356 deletions(-) create mode 100644 src/entity.odin create mode 100644 src/fx.odin create mode 100644 src/map.odin create mode 100644 src2/attack.odin create mode 100644 src2/enemy.odin create mode 100644 src2/entity.odin create mode 100644 src2/fx.odin create mode 100644 src2/gfx.odin create mode 100644 src2/item.odin create mode 100644 src2/main.odin create mode 100644 src2/map.odin create mode 100644 src2/player.odin create mode 100644 src2/scene.odin create mode 100644 src2/ui.odin create mode 100644 src3/attack.odin create mode 100644 src3/enemy.odin create mode 100644 src3/entity.odin create mode 100644 src3/fx.odin create mode 100644 src3/gfx.odin create mode 100644 src3/item.odin create mode 100644 src3/main.odin create mode 100644 src3/map.odin create mode 100644 src3/player.odin create mode 100644 src3/scene.odin create mode 100644 src3/skill.odin create mode 100644 src3/ui.odin diff --git a/src/enemy.odin b/src/enemy.odin index b3a1303..736856b 100644 --- a/src/enemy.odin +++ b/src/enemy.odin @@ -1,27 +1,38 @@ package main - -import hm "core:container/handle_map" import rl "vendor:raylib" -// ИИ врага — летит к игроку. Вызывается из диспатчера sys_update -enemy_update :: proc(state: ^GameState, e: ^Enemy, h: Handle, dt: f32) { - pe := get_entity(state, state.player) - if pe == nil do return - - dir := get_base(pe).pos - e.pos - set_vel(&e.base, dir, e.speed) +Enemy :: struct { + using actor: Actor, + touch_dmg: i32, } -// Мёртвые враги → лут -sys_death :: proc(state: ^GameState) { - it := hm.iterator_make(&state.entities) - for e, _ in hm.iterate(&it) { - #partial switch &v in e.v { - case Enemy: - if v.hp <= 0 && !v.marked { - v.marked = true - spawn(state, make_ground_item(v.pos)) - } - } +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)) } } diff --git a/src/entity.odin b/src/entity.odin new file mode 100644 index 0000000..7cbb09d --- /dev/null +++ b/src/entity.odin @@ -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 +} diff --git a/src/fx.odin b/src/fx.odin new file mode 100644 index 0000000..6eb8414 --- /dev/null +++ b/src/fx.odin @@ -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 + } + } +} diff --git a/src/gfx.odin b/src/gfx.odin index 31c90ce..e5af18c 100644 --- a/src/gfx.odin +++ b/src/gfx.odin @@ -1,6 +1,5 @@ package main -import hm "core:container/handle_map" import rl "vendor:raylib" Graphics :: union { @@ -40,7 +39,6 @@ GfxCircleLines :: struct { } GfxTexture :: struct { - // TODO: Implement texture rendering } 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) } } - -// ─── 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) - } -} diff --git a/src/item.odin b/src/item.odin index 6fb9966..b58895f 100644 --- a/src/item.odin +++ b/src/item.odin @@ -1,24 +1,22 @@ package main +import rl "vendor:raylib" -// Пикап лута игроком — по коллизионным парам (как урон) -sys_pickup :: proc(state: ^GameState) { - _, p := get_player(state) - if p == nil do return +GroundItem :: struct { + using base: BaseEntity, + heal: i32, +} - 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 - gi, ok := &e.v.(GroundItem) - if !ok || gi.marked do continue - - p.hp = min(p.hp + gi.heal, p.max_hp) - gi.marked = true +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) { +} diff --git a/src/main.odin b/src/main.odin index 919f6e0..e612be1 100644 --- a/src/main.odin +++ b/src/main.odin @@ -2,12 +2,17 @@ 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{ + state := GameState { camera = {zoom = 1.5, offset = {SCREEN_W / 2, SCREEN_H / 2}}, } state.collisions = make([dynamic]CollisionPair) @@ -21,18 +26,13 @@ main :: proc() { for !rl.WindowShouldClose() { dt := rl.GetFrameTime() - sys_update(&state, dt) // вход игрока + ИИ врагов (диспатчер) - sys_move(&state, dt) // движение всех - sys_collisions(&state) // пары + резолв - sys_damage(&state) // дамаг игроку от врагов - sys_death(&state) // враги умерли → лут - sys_pickup(&state) // игрок подбирает лут - sys_fx_update(&state, dt) // FX: таймеры + следование - sys_sweep(&state) // удалить помеченное + 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_render(&state) + sys_draw(&state) } } diff --git a/src/map.odin b/src/map.odin new file mode 100644 index 0000000..50acd56 --- /dev/null +++ b/src/map.odin @@ -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) + } + } +} diff --git a/src/player.odin b/src/player.odin index d3b2562..42ebb0e 100644 --- a/src/player.odin +++ b/src/player.odin @@ -3,32 +3,80 @@ package main import hm "core:container/handle_map" import rl "vendor:raylib" -// Вход и атака игрока — вызывается из диспатчера sys_update -player_update :: proc(state: ^GameState, p: ^Player, h: Handle, dt: f32) { - // движение - dir: rl.Vector2 - if rl.IsKeyDown(.W) || rl.IsKeyDown(.UP) do dir.y -= 1 - if rl.IsKeyDown(.S) || rl.IsKeyDown(.DOWN) do dir.y += 1 - if rl.IsKeyDown(.A) || rl.IsKeyDown(.LEFT) do dir.x -= 1 - if rl.IsKeyDown(.D) || rl.IsKeyDown(.RIGHT) do dir.x += 1 - set_vel(&p.base, dir, p.speed) +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 rl.IsKeyPressed(.SPACE) && p.attack_timer <= 0 { + if input.attack && p.attack_timer <= 0 { p.attack_timer = p.attack_cd - // визуал атаки — кольцо - init_fx(state, FX{ - parent = h, - 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}}, + 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, }, - lifetime = p.attack_cd, - }) + ) - // дамаг врагам в радиусе it := hm.iterator_make(&state.entities) for e, _ in hm.iterate(&it) { #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 +} diff --git a/src/scene.odin b/src/scene.odin index 0592c07..3af4b92 100644 --- a/src/scene.odin +++ b/src/scene.odin @@ -3,177 +3,30 @@ package main import hm "core:container/handle_map" import rl "vendor:raylib" -SCREEN_W :: 1280 -SCREEN_H :: 720 -TARGET_FPS :: 60 SQRT2_INV :: 0.7071067811865476 -MAX_GFX :: 2 -Handle :: hm.Handle32 - -MAX_ENTITIES :: 1024 -MAX_FX :: 256 - -// ─── Общее — всё что есть у любой сущности ─── - -BaseEntity :: struct { - pos: rl.Vector2, - vel: rl.Vector2, - col_size: f32, - marked: bool, - gfx: [MAX_GFX]Graphics, +CollisionPair :: struct { + a: Handle, + b: Handle, } -// ─── Акторское — то что есть у живых ─── - -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 { - player: Handle, - camera: rl.Camera2D, - entities: hm.Static_Handle_Map(MAX_ENTITIES, Entity, Handle), - fxs: hm.Static_Handle_Map(MAX_FX, FX, Handle), + 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 } -// Общее из любой сущности (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 { 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 { 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 } -// ─── Диспатчер: один свитч, типизированные функции ─── - sys_update :: proc(state: ^GameState, dt: f32) { it := hm.iterator_make(&state.entities) for e, h in hm.iterate(&it) { @@ -211,32 +62,68 @@ sys_update :: proc(state: ^GameState, dt: f32) { 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) } } } -// ─── Общее движение ─── - -sys_move :: proc(state: ^GameState, dt: f32) { - it := hm.iterator_make(&state.entities) - for e, _ in hm.iterate(&it) { - b := get_base(e) - b.pos += b.vel * dt +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() -CollisionPair :: struct { - a: Handle, - b: Handle, + ui_draw(state) + rl.EndDrawing() } -sys_collisions :: proc(state: ^GameState) { +resolve_collisions :: proc(state: ^GameState) { clear(&state.collisions) - // собрать все живые handle'ы hs: [MAX_ENTITIES]Handle n := 0 it := hm.iterator_make(&state.entities) @@ -245,7 +132,6 @@ sys_collisions :: proc(state: ^GameState) { n += 1 } - // O(n²): резолвим пересечения и собираем пары for i in 0 ..< n { for j in i + 1 ..< n { a := get_entity(state, hs[i]) @@ -257,14 +143,8 @@ sys_collisions :: proc(state: ^GameState) { } } -// Дамаг игроку от врагов по коллизионным парам -sys_damage :: proc(state: ^GameState) { +apply_collision_effects :: proc(state: ^GameState) { _, 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 { other_h: Handle @@ -276,64 +156,19 @@ sys_damage :: proc(state: ^GameState) { e := get_entity(state, other_h) if e == nil do continue - ev, ok := &e.v.(Enemy) - if !ok do continue - p.hp -= ev.touch_dmg - p.invuln_timer = 0.5 - if p.hp <= 0 do p.hp = 0 - break // один урон за кадр, дальше неуязвим - } -} - -// ─── Очистка помеченного ─── - -sys_sweep :: proc(state: ^GameState) { - it := hm.iterator_make(&state.entities) - for e, h in hm.iterate(&it) { - if get_base(e).marked { - hm.remove(&state.entities, h) - } - } - - it2 := hm.iterator_make(&state.fxs) - for fx, h in hm.iterate(&it2) { - if fx.marked { - hm.remove(&state.fxs, h) - } - } -} - -// ─── Рендер ─── - -entity_draw :: proc(e: ^Entity) { - b := get_base(e) - for g in b.gfx { - gfx_draw(b.pos, g) - } -} - -sys_render :: proc(state: ^GameState) { - rl.BeginDrawing() - rl.ClearBackground({20, 20, 30, 255}) - rl.BeginMode2D(state.camera) - { - it := hm.iterator_make(&state.entities) - for e, _ in hm.iterate(&it) { - entity_draw(e) - if a := get_actor(e); a != nil { - ui_draw_world_hp_bar(get_base(e), a) + #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 } } - - it2 := hm.iterator_make(&state.fxs) - for fx, _ in hm.iterate(&it2) { - fx_draw(fx) - } } - rl.EndMode2D() - - ui_draw(state) - - rl.EndDrawing() } diff --git a/src/ui.odin b/src/ui.odin index 14a2237..4645041 100644 --- a/src/ui.odin +++ b/src/ui.odin @@ -1,6 +1,6 @@ package main - import hm "core:container/handle_map" + import rl "vendor:raylib" ui_draw_world_hp_bar :: proc(b: ^BaseEntity, actor: ^Actor) { diff --git a/src2/attack.odin b/src2/attack.odin new file mode 100644 index 0000000..bb5255b --- /dev/null +++ b/src2/attack.odin @@ -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 +} diff --git a/src2/enemy.odin b/src2/enemy.odin new file mode 100644 index 0000000..736856b --- /dev/null +++ b/src2/enemy.odin @@ -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)) + } +} diff --git a/src2/entity.odin b/src2/entity.odin new file mode 100644 index 0000000..3e9b1f6 --- /dev/null +++ b/src2/entity.odin @@ -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 +} diff --git a/src2/fx.odin b/src2/fx.odin new file mode 100644 index 0000000..6eb8414 --- /dev/null +++ b/src2/fx.odin @@ -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 + } + } +} diff --git a/src2/gfx.odin b/src2/gfx.odin new file mode 100644 index 0000000..e5af18c --- /dev/null +++ b/src2/gfx.odin @@ -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) + } +} diff --git a/src2/item.odin b/src2/item.odin new file mode 100644 index 0000000..b58895f --- /dev/null +++ b/src2/item.odin @@ -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) { +} diff --git a/src2/main.odin b/src2/main.odin new file mode 100644 index 0000000..ef76296 --- /dev/null +++ b/src2/main.odin @@ -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) + } +} diff --git a/src2/map.odin b/src2/map.odin new file mode 100644 index 0000000..d1fe328 --- /dev/null +++ b/src2/map.odin @@ -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) + } + } +} diff --git a/src2/player.odin b/src2/player.odin new file mode 100644 index 0000000..b0b8100 --- /dev/null +++ b/src2/player.odin @@ -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 +} diff --git a/src2/scene.odin b/src2/scene.odin new file mode 100644 index 0000000..ee5d23d --- /dev/null +++ b/src2/scene.odin @@ -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 + } + } + } +} diff --git a/src2/ui.odin b/src2/ui.odin new file mode 100644 index 0000000..4645041 --- /dev/null +++ b/src2/ui.odin @@ -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}) +} diff --git a/src3/attack.odin b/src3/attack.odin new file mode 100644 index 0000000..f8d5fa1 --- /dev/null +++ b/src3/attack.odin @@ -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 +} diff --git a/src3/enemy.odin b/src3/enemy.odin new file mode 100644 index 0000000..df6de5b --- /dev/null +++ b/src3/enemy.odin @@ -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)))) + } +} diff --git a/src3/entity.odin b/src3/entity.odin new file mode 100644 index 0000000..3e9b1f6 --- /dev/null +++ b/src3/entity.odin @@ -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 +} diff --git a/src3/fx.odin b/src3/fx.odin new file mode 100644 index 0000000..6eb8414 --- /dev/null +++ b/src3/fx.odin @@ -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 + } + } +} diff --git a/src3/gfx.odin b/src3/gfx.odin new file mode 100644 index 0000000..e5af18c --- /dev/null +++ b/src3/gfx.odin @@ -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) + } +} diff --git a/src3/item.odin b/src3/item.odin new file mode 100644 index 0000000..ae030c4 --- /dev/null +++ b/src3/item.odin @@ -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) { +} diff --git a/src3/main.odin b/src3/main.odin new file mode 100644 index 0000000..844978c --- /dev/null +++ b/src3/main.odin @@ -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) + } +} diff --git a/src3/map.odin b/src3/map.odin new file mode 100644 index 0000000..d1fe328 --- /dev/null +++ b/src3/map.odin @@ -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) + } + } +} diff --git a/src3/player.odin b/src3/player.odin new file mode 100644 index 0000000..0b92e5b --- /dev/null +++ b/src3/player.odin @@ -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 + } +} diff --git a/src3/scene.odin b/src3/scene.odin new file mode 100644 index 0000000..8a6a3c6 --- /dev/null +++ b/src3/scene.odin @@ -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) + } + } + } +} diff --git a/src3/skill.odin b/src3/skill.odin new file mode 100644 index 0000000..c5c7cb3 --- /dev/null +++ b/src3/skill.odin @@ -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 +} diff --git a/src3/ui.odin b/src3/ui.odin new file mode 100644 index 0000000..9516c4d --- /dev/null +++ b/src3/ui.odin @@ -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) + } +}