Files
last-hearth/mods/hearth/encounters.lua
T
2026-11-01 19:35:46 -05:00

76 lines
4.3 KiB
Lua
Executable File

local C = core
-- A deliberately artificial sparring target: opt-in, persistent, non-lethal.
-- No claim of production combat animation/prediction: validates the first loop.
C.register_entity("hearth:practice", {
initial_properties = {physical = true, collide_with_objects = false,
collisionbox = {-0.3, 0, -0.3, 0.3, 1.6, 0.3}, visual = "cube",
visual_size = {x = 0.6, y = 1.6, z = 0.6},
textures = {hearth.texture("#c7aa83"), hearth.texture("#947853"), hearth.texture("#947853"),
hearth.texture("#947853"), hearth.texture("#947853"), hearth.texture("#947853")},
pointable = true, hp_max = 100, static_save = true,
nametag = "PRACTICE / strike to spar · step back to stop", nametag_color = "#e5c890"},
on_activate = function(self)
self.object:set_armor_groups({immortal = 1})
self.object:set_acceleration({x = 0, y = -9.81, z = 0})
self.health, self.cooldowns = 30, {}
end,
on_punch = function(self, puncher)
if not puncher or not puncher:is_player() then return true end
local pos, p = self.object:get_pos(), puncher:get_pos()
if not pos or not p or vector.distance(pos, p) > 3.5 then return true end
if not C.line_of_sight(vector.offset(p, 0, 1.2, 0), vector.offset(pos, 0, 0.8, 0), 0.25) then return true end
local name = puncher:get_player_name()
local state = hearth.players[name]
if not state then return true end
local tool = puncher:get_wielded_item():get_name()
local attacks = {['hearth:spear'] = {5, 1.2}, ['hearth:axe'] = {4, 1.1}, ['hearth:knife'] = {3, 0.7}}
local attack = attacks[tool] or {1, 1}
local now = C.get_us_time() / 1000000
if now - (self.cooldowns[name] or -100) < attack[2] or state.stamina < 10 then return true end
self.cooldowns[name] = now
state.stamina = state.stamina - 10
self.health = self.health - attack[1]
-- A faster weapon cannot postpone an already committed counter forever.
if not self.target then self.target, self.delay = name, 0.75 end
self.object:set_properties({nametag = "COUNTER INCOMING / brace or step away", nametag_color = "#f38ba8"})
if self.health <= 0 then
self.health, self.target = 30, nil
self.object:set_properties({nametag = "ROUND COMPLETE / ready again", nametag_color = "#a6e3a1"})
hearth.notify(puncher, "Practice complete. Good footwork saves stamina; right-click your spear to brace.")
else hearth.notify(puncher, "Hit confirmed / target " .. self.health .. "/30") end
return true
end,
on_step = function(self, dt)
if not self.target then return end
self.delay = self.delay - math.min(dt, 0.1)
if self.delay > 0 then return end
local player = C.get_player_by_name(self.target)
if player and player:get_hp() > 2 then
local p, pos = player:get_pos(), self.object:get_pos()
if vector.distance(p, pos) < 2.5 and C.line_of_sight(vector.offset(pos, 0, 1, 0), vector.offset(p, 0, 1, 0), 0.25) then
local state, session = hearth.players[self.target], hearth.sessions[self.target]
local blocking = state and session and session.blocking and state.stamina >= 8
if blocking then state.stamina = state.stamina - 8 end
player:set_hp(math.max(2, player:get_hp() - (blocking and 1 or 3)), {type = "set_hp", from = "mod"})
hearth.notify(player, blocking and "Braced / reduced impact" or "Counter landed / step back beyond reach")
end
end
self.target = nil
self.object:set_properties({nametag = "PRACTICE / " .. self.health .. "/30 · strike to spar", nametag_color = "#e5c890"})
end,
})
C.register_chatcommand("practice", {description = "Place one practice opponent near you (local play only)",
func = function(name)
if not C.is_singleplayer() then return false, "Practice placement is local-play only." end
local player = C.get_player_by_name(name)
if not player then return false end
local pos = vector.round(vector.add(player:get_pos(), vector.multiply(player:get_look_dir(), 3)))
for _, obj in ipairs(C.get_objects_inside_radius(pos, 30)) do
local e = obj:get_luaentity()
if e and e.name == "hearth:practice" then return false, "A practice target is already nearby." end
end
if C.get_node(pos).name ~= "air" or C.is_protected(pos, name) then return false, "Look at an open space nearby." end
return C.add_entity(pos, "hearth:practice") ~= nil, "Practice target placed. Strike it to start; step away to stop."
end})