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})