76 lines
4.3 KiB
Lua
Executable File
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})
|