commit 3ce70bd00a2074e9a25369a704a7018e82cb64df Author: Alexey Bagno Date: Sat Aug 1 21:48:30 2026 +0300 Init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e9669d9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +src.bin diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3f58322 --- /dev/null +++ b/Makefile @@ -0,0 +1,2 @@ +run: + odin run ./src/ diff --git a/ols.json b/ols.json new file mode 100644 index 0000000..c488e1b --- /dev/null +++ b/ols.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://raw.githubusercontent.com/DanielGavin/ols/master/misc/ols.schema.json", + "enable_semantic_tokens": true, + "enable_document_symbols": true, + "enable_hover": true, + "enable_snippets": true, + "enable_checker_only_saved": true, + "enable_inlay_hints_params": true, + "enable_inlay_hints_default_params": true, + "enable_inlay_hints_implicit_return": true, + "enable_inlay_hints_optional_result": true, + "enable_inlay_hints_struct_fields": true +} diff --git a/src/enemy.odin b/src/enemy.odin new file mode 100644 index 0000000..6e1386e --- /dev/null +++ b/src/enemy.odin @@ -0,0 +1,38 @@ +package main + +import hm "core:container/handle_map" +import rl "vendor:raylib" + +Enemy :: struct { + using actor: Actor, + touch_dmg: i32, +} + +enemy_update :: proc(state: ^GameState, p, e: ^Entity) { + p := hm.get(&state.entities, state.player) +} + +// sys_enemy_ai :: proc(state: ^GameState) { +// for &enemy in &state.enemies { +// dir := state.player.pos - enemy.pos +// set_vel(cast(^Entity2)&enemy, dir, enemy.speed) +// } +// } +// +// sys_death :: proc(state: ^GameState3) { +// for &enemy in &state.enemies { +// if enemy.hp > 0 || enemy.marked do continue +// enemy.marked = true +// init_entity( +// state, +// GroundItem3 { +// ent = Entity2 { +// GroundItem3 = enemy.pos, +// GroundItem3 = {GroundItem3 = GfxCircle{GroundItem3 = {0, 0}, GroundItem3 = 6, GroundItem3 = {255, 220, 50, 255}}}, +// GroundItem3 = 10, +// }, +// heal = 25, +// }, +// ) +// } +// } diff --git a/src/gfx.odin b/src/gfx.odin new file mode 100644 index 0000000..477536a --- /dev/null +++ b/src/gfx.odin @@ -0,0 +1,87 @@ +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 { + // TODO: Implement texure rendering +} + +FX :: struct { + parentId: EntityID, + lifetime: f32, + permanent: bool, + pos: rl.Vector2, + gfx: [MAX_GFX]Graphics, + marked: bool, +} + +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) + } +} + +fx_update :: proc(state: ^GameState3, fx: ^FX, dt: f32) { + if fx.marked do return + if fx.parentId != 0 { + parent, ok := get_ent_by_id(state, fx.parentId) + if ok do fx.pos = parent.pos + } + 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) + } +} + +init_fx :: proc(state: ^GameState3, fx: FX) { + append(&state.fxs, fx) +} + +//TODO: Создание и удаление FX с лайфтаймами diff --git a/src/item.odin b/src/item.odin new file mode 100644 index 0000000..be09781 --- /dev/null +++ b/src/item.odin @@ -0,0 +1,17 @@ +package main + +GroundItem3 :: struct { + using ent: Entity2, + heal: i32, +} + +sys_pickup :: proc(state: ^GameState3) { + for &item in &state.items { + if item.marked do continue + _, ok := check_col(cast(^Entity2)&item, cast(^Entity2)&state.player) + if ok { + state.player.hp = min(state.player.hp + item.heal, state.player.max_hp) + item.marked = true + } + } +} diff --git a/src/main.odin b/src/main.odin new file mode 100644 index 0000000..3e5fc33 --- /dev/null +++ b/src/main.odin @@ -0,0 +1,108 @@ +package main + +import hm "core:container/handle_map" +import "core:fmt" +import rl "vendor:raylib" + +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}}, + } + + p := hm.add( + &state.entities, + Entity { + pos = {400, 300}, + col_size = 16, + variant = Player { + actor = Actor { + hp = 100, + max_hp = 100, + speed = 220, + gfx = {0 = GfxCircle{pos = {0, 0}, radius = 14, color = {80, 220, 100, 255}}}, + }, + attack_range = 75, + attack_dmg = 35, + attack_cd = 0.3, + invuln_timer = 0, + }, + }, + ) + + state.player = p + + //TODO: Коллизия + + // state.collisions = make([dynamic]CollisionEntry) + // defer delete(state.collisions) + + hm.add( + &state.entities, + Entity { + pos = {600, 500}, + col_size = 16, + variant = Enemy { + actor = Actor { + hp = 40, + max_hp = 40, + speed = 55, + gfx = {0 = GfxCircle{pos = {0, 0}, radius = 11, color = {220, 70, 70, 255}}}, + }, + }, + }, + ) + + hm.add( + &state.entities, + Entity { + pos = {200, 500}, + col_size = 16, + variant = Enemy { + actor = Actor { + hp = 40, + max_hp = 40, + speed = 55, + gfx = {0 = GfxCircle{pos = {0, 0}, radius = 11, color = {220, 70, 70, 255}}}, + }, + }, + }, + ) + + hm.add( + &state.entities, + Entity { + pos = {600, 500}, + col_size = 16, + variant = Enemy { + actor = Actor { + hp = 40, + max_hp = 40, + speed = 55, + gfx = {0 = GfxCircle{pos = {0, 0}, radius = 11, color = {220, 70, 70, 255}}}, + }, + }, + }, + ) + + + for !rl.WindowShouldClose() { + dt := rl.GetFrameTime() + + sys_input(&state) + sys_enemy_ai(&state) + sys_move(&state, dt) + sys_collisions(&state) + sys_damage(&state) + sys_death(&state) + sys_pickup(&state) + sys_update(&state, dt) + sys_sweep(&state) + + state.camera.target = state.player.pos + sys_render(&state) + } +} diff --git a/src/player.odin b/src/player.odin new file mode 100644 index 0000000..e453457 --- /dev/null +++ b/src/player.odin @@ -0,0 +1,49 @@ +package main + +import rl "vendor:raylib" + +Player2 :: struct { + using actor: Actor, + attack_range: f32, + attack_dmg: i32, + attack_cd: f32, + attack_timer: f32, + invuln_timer: f32, +} + +sys_input :: proc(state: ^GameState3) { + p := &state.player + + 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(cast(^Entity2)p, dir, p.speed) + + if p.attack_timer > 0 do p.attack_timer -= rl.GetFrameTime() + if rl.IsKeyPressed(.SPACE) && p.attack_timer <= 0 { + p.attack_timer = p.attack_cd + init_fx( + state, + FX { + parentId = p.id, + lifetime = p.attack_cd, + permanent = false, + 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}}, + }, + }, + ) + for &enemy in &state.enemies { + if dist(Entity2(p), Entity2(enemy)) <= p.attack_range { + enemy.hp -= p.attack_dmg + } + } + } +} diff --git a/src/scene.odin b/src/scene.odin new file mode 100644 index 0000000..1cd1548 --- /dev/null +++ b/src/scene.odin @@ -0,0 +1,249 @@ +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 +MAX_ENTITIES :: 1024 + +Handle :: hm.Handle32 + +// ─── Коллизии ─── + +// EntityKind :: enum { +// Player, +// Enemy, +// } + +// CollisionEntry :: struct { +// a_kind: EntityKind, +// a_idx: int, +// b_kind: EntityKind, +// b_idx: int, +// } + +// ─── Игровое состояние ─── + + +Entity :: struct { + handle: Handle, + pos: rl.Vector2, + col_size: f32, + marked: bool, + v: EntityVariant, +} + +Actor :: struct { + vel: rl.Vector2, + hp: i32, + max_hp: i32, + speed: f32, + gfx: [MAX_GFX]Graphics, +} + +Player :: struct { + using actor: Actor, + attack_range: f32, + attack_dmg: i32, + attack_cd: f32, + attack_timer: f32, + invuln_timer: f32, +} + + +GroundItem :: struct { + heal: i32, + gfx: [MAX_GFX]Graphics, +} + +EntityVariant :: union { + Player, + Enemy, + GroundItem, +} + +GameState :: struct { + player: Handle, + camera: rl.Camera2D, + entities: hm.Static_Handle_Map(MAX_ENTITIES, Entity, Handle), + // particles: hm.Static_Handle_Map(MAX_PARTICLE_FX, Fx2, Handle), +} + + +// ─── Хелперы ─── + +set_vel :: #force_inline proc(e: ^Entity, dir: rl.Vector2, speed: f32) { + if rl.Vector2Length(dir) > 0 { + e.v.vel = rl.Vector2Normalize(dir) * speed + } else { + e.v.vel = {} + } +} + +move_ent :: #force_inline proc(e: ^Entity, dt: f32) { + e.v.pos += e.v.vel * dt +} + +dist :: #force_inline proc(a, b: Entity) -> f32 { + return rl.Vector2Distance(a.v.pos, b.v.pos) +} + +// entity_rect :: #force_inline proc(e: ^Entity2) -> rl.Rectangle { +// return rl.Rectangle{e.pos.x - e.col_size / 2, e.pos.y - e.col_size / 2, e.col_size, e.col_size} +// } +// +// check_col :: proc(a, b: ^Entity2) -> (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: ^Entity2) -> 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_move :: proc(state: ^GameState, dt: f32) { + it := hm.iterator_make(&state.entities) + for e, _ in hm.iterate(&it) { + #partial switch &v in e.v { + case Player: + e.pos += v.vel * dt + case Enemy: + e.pos += v.vel * dt + } + } +} + +// sys_collisions :: proc(state: ^GameState3) { +// clear(&state.collisions) +// +// p_ent := cast(^Entity2)&state.player +// +// for &enemy, i in &state.enemies { +// if resolve_aabb(p_ent, cast(^Entity2)&enemy) { +// append( +// &state.collisions, +// CollisionEntry{a_kind = .Player, a_idx = 0, b_kind = .Enemy, b_idx = i}, +// ) +// } +// } +// +// for i in 0 ..< len(state.enemies) { +// for j in i + 1 ..< len(state.enemies) { +// resolve_aabb(cast(^Entity2)&state.enemies[i], cast(^Entity2)&state.enemies[j]) +// } +// } +// } + +sys_damage :: proc(state: ^GameState) { + p := hm.get(&state.entities, state.player) + if p.invuln_timer > 0 { + p.invuln_timer -= rl.GetFrameTime() + return + } + + // for c in state.collisions { + // if c.a_kind == .Player && c.b_kind == .Enemy { + // state.player.hp -= state.enemies[c.b_idx].touch_dmg + // state.player.invuln_timer = 0.5 + // if state.player.hp <= 0 { + // state.player.hp = 0 + // } + // } + // } +} + +//TODO: УБрать вообще? мапа фиксит инвалидацию поинтеров +sys_sweep :: proc(state: ^GameState) { + + it := hm.iterator_make(&state.entities) + for e, i in hm.iterate(&it) { + if e.marked do hm.remove(&state.entities, i) + } + + // i = 0 + // for i < len(state.fxs) { + // if state.fxs[i].marked { + // ordered_remove(&state.fxs, i) + // } else { + // i += 1 + // } + // } +} + +// entity_draw :: proc(e: ^Entity2) { +// for g in e.gfx { +// gfx_draw(e.pos, g) +// } +// } +// +sys_update :: proc(state: ^GameState, dt: f32) { + p := hm.get(&state.entities, state.player) + + it := hm.iterator_make(&state.entities) + for e, _ in hm.iterate(&it) { + #partial switch &v in e.v { + case Enemy: + enemy_update(state, p, e) + } + } +} + +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) { + #partial switch &v in e.v { + case Player: + for g in &v.gfx do gfx_draw(e.pos, g) + case Enemy: + for g in &v.gfx do gfx_draw(e.pos, g) + } + } + + // for &e in state.enemies { + // entity_draw(&e) + // ui_draw_world_hp_bar(cast(^Actor)&e) + // } + // + // for &it in state.items { + // entity_draw(&it) + // } + // + // for &fx in state.fxs { + // fx_draw(&fx) + // } + // + // entity_draw(&state.player) + // ui_draw_world_hp_bar(cast(^Actor)&state.player) + } + rl.EndMode2D() + + // ui_draw(state) + + rl.EndDrawing() +} diff --git a/src/ui.odin b/src/ui.odin new file mode 100644 index 0000000..c229fee --- /dev/null +++ b/src/ui.odin @@ -0,0 +1,24 @@ +package main + +import rl "vendor:raylib" + + +ui_draw_world_hp_bar :: proc(actor: ^Actor) { + hp_ratio := f32(actor.hp) / f32(actor.max_hp) + bar_w := actor.col_size * 3 + bar_pos := actor.pos + rl.Vector2{-bar_w / 2, -actor.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: ^GameState3) { + rl.DrawText( + rl.TextFormat("HP: %d/%d", state.player.hp, state.player.max_hp), + 12, + 12, + 20, + rl.RAYWHITE, + ) + rl.DrawText(rl.TextFormat("Enemies: %d", len(state.enemies)), 12, 36, 16, {180, 180, 180, 255}) + rl.DrawText("[SPACE] attack [WASD] move", 12, SCREEN_H - 28, 14, {120, 120, 120, 255}) +}