225 lines
5.5 KiB
Odin
225 lines
5.5 KiB
Odin
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
|
|
}
|
|
}
|