Files
odin-iso-test/src3/player.odin
T
2026-08-05 16:57:44 +03:00

253 lines
5.9 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(gfx: ^GraphicsAssetStore) -> 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}},
1 = GfxAnimatedSprite {
pos = {},
atlas = player_atlas_get(gfx, .IDLE),
frame = 0,
speed = 0.5,
update = update_player_sprite,
},
}
p.hp = p.base_max_hp
p.max_hp = p.base_max_hp
p.speed = 220
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 {
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
}
for &gfx in p.gfx {
#partial switch &v in gfx {
case GfxAnimatedSprite:
v->update(dt)
}
}
}