144 lines
7.8 KiB
Lua
Executable File
144 lines
7.8 KiB
Lua
Executable File
-- M1 presentation only: no metadata, persisted preferences, chat or camera bob.
|
|
local C = core
|
|
local frames = {idle = {0, 19, 12}, walk = {20, 39, 24}, run = {40, 59, 32},
|
|
strike = {60, 69, 28}, guard = {70, 79, 12}, swim = {80, 99, 20}}
|
|
local function now() return C.get_us_time() / 1000000 end
|
|
local function session(player) return hearth.sessions[player:get_player_name()] end
|
|
local function text(value)
|
|
-- Bound HUD payloads and strip control/escape sequences from item descriptions.
|
|
return tostring(value or ""):gsub("\27%([^)]*%)", ""):gsub("[%c]", " "):sub(1, 200)
|
|
end
|
|
function hearth.feedback_sound(name, player, gain)
|
|
local spec = {pos = player:get_pos(), gain = gain or 0.25, max_hear_distance = 12}
|
|
-- Optional sound adapter contract: sound(name, SoundParameters, ephemeral).
|
|
if hearth.sound then return hearth.sound(name, spec, true) end
|
|
return C.sound_play(name, spec, true)
|
|
end
|
|
local colors = {info = 0xcdd6f4, success = 0xa6e3a1, warning = 0xf9e2af, error = 0xf38ba8}
|
|
function hearth.feedback(player, message, kind)
|
|
local s = session(player); if not s then return end
|
|
s.feedback_text, s.feedback_kind, s.feedback_until = text(message), kind, now() + 3
|
|
end
|
|
local function target_text(player)
|
|
local pos, look = player:get_pos(), player:get_look_dir()
|
|
local props = player:get_properties()
|
|
local start = {x = pos.x, y = pos.y + (props.eye_height or 1.55), z = pos.z}
|
|
local finish = {x = start.x + look.x * 5, y = start.y + look.y * 5, z = start.z + look.z * 5}
|
|
-- No object hits and no liquids: water never obscures useful node targeting.
|
|
for hit in C.raycast(start, finish, false, false) do
|
|
if hit.type == "node" then
|
|
local node = C.get_node_or_nil and C.get_node_or_nil(hit.under) or nil
|
|
if not node or node.name == "ignore" then return "" end
|
|
local def = C.registered_nodes[node.name]
|
|
if not def then return "" end
|
|
if node.name == "hearth:fire" or node.name == "hearth:fire_lit" then
|
|
local status = C.get_meta(hit.under):get_string("infotext")
|
|
return text(def.description) .. (status ~= "" and (" | " .. text(status)) or "")
|
|
end
|
|
local B = hearth.botany
|
|
local known = B and B.kind and B.kind(node.name)
|
|
local inspect_hint = known and " | /inspect species" or ""
|
|
local wield = player:get_wielded_item()
|
|
local caps = wield:get_tool_capabilities()
|
|
local dig = C.get_dig_params(def.groups or {}, caps, wield:get_wear())
|
|
local wield_def = C.registered_nodes[wield:get_name()]
|
|
if wield_def and hearth.construction then
|
|
local destination = def.buildable_to and hit.under or hit.above
|
|
local ok, reason = hearth.construction.preview(destination, player, wield:get_name())
|
|
local reasons = {ok = "Ready to place", support = "Needs nearby structural support",
|
|
protected = "Protected homestead", unloaded = "Terrain not loaded", occupied = "Space occupied",
|
|
distance = "Move closer", item = "Not a building material", invalid = "Invalid position"}
|
|
return text(wield_def.description):sub(1,120) .. " | " .. text(reasons[reason] or (ok and "Ready to place" or "Cannot place here")) .. inspect_hint
|
|
end
|
|
return text(def.description):sub(1,140)
|
|
.. (dig.diggable and " | Tool suitable" or " | Different tool needed")
|
|
.. inspect_hint
|
|
end
|
|
end
|
|
return ""
|
|
end
|
|
local function surface(player, s)
|
|
if s.swimming then return "water" end
|
|
local p = player:get_pos()
|
|
local feet = C.registered_nodes[C.get_node({x = p.x, y = p.y + 0.1, z = p.z}).name] or {}
|
|
if feet.groups and (feet.groups.water or 0) > 0 then return "water" end
|
|
local def = C.registered_nodes[C.get_node({x = p.x, y = p.y - 0.1, z = p.z}).name] or {}
|
|
local g = def.groups or {}
|
|
if (g.water or 0) > 0 then return "water" end
|
|
if (g.wood or 0) > 0 or (g.tree or 0) > 0 then return "wood" end
|
|
if (g.stone or 0) > 0 or (g.cracky or 0) > 0 then return "stone" end
|
|
return "soil"
|
|
end
|
|
function hearth.feedback_step(player, dt)
|
|
if hearth.presentation_step then hearth.presentation_step(player) end
|
|
local s = session(player); if not s or s.waiting or player:get_hp() <= 0 then return end
|
|
dt = hearth.movement.delta(dt)
|
|
local t, pos, velocity = now(), player:get_pos(), player:get_velocity()
|
|
local controls = player:get_player_control()
|
|
-- Rising edge only; holding dig never replays a strike every frame. Combat
|
|
-- may set session.strike_until using the same monotonic seconds clock.
|
|
if controls.LMB and not s.feedback_lmb then
|
|
s.strike_until = t + 10 / 28
|
|
local swing = ({["hearth:knife"] = "hearth_swing_knife",
|
|
["hearth:axe"] = "hearth_swing_axe", ["hearth:spear"] = "hearth_swing_spear"})
|
|
local sound = swing[player:get_wielded_item():get_name()]
|
|
if sound then hearth.feedback_sound(sound, player, 0.18) end
|
|
end
|
|
s.feedback_lmb = controls.LMB
|
|
local animation = s.swimming and "swim" or (s.blocking and "guard"
|
|
or ((s.strike_until or 0) > t and "strike" or (s.moving and (s.sprinting and "run" or "walk") or "idle")))
|
|
if s.animation ~= animation then
|
|
local f = frames[animation]
|
|
player:set_animation({x = f[1], y = f[2]}, f[3], 0.12, animation ~= "strike")
|
|
s.animation = animation
|
|
end
|
|
-- FOV opt-in only. Default comfort_fov=nil never touches client settings.
|
|
if s.comfort_fov ~= nil and s.applied_fov ~= s.comfort_fov then
|
|
player:set_fov(s.comfort_fov, false, 0.2); s.applied_fov = s.comfort_fov
|
|
end
|
|
local previous = s.feedback_pos
|
|
s.step_time = (s.step_time or 0) + dt
|
|
if s.grounded and s.feedback_grounded == false and (s.feedback_fall or 0) < -3 then
|
|
hearth.feedback_sound("hearth_land", player, 0.35)
|
|
s.step_time, s.step_distance = 0, 0
|
|
elseif previous and s.grounded and s.feedback_grounded and s.moving then
|
|
local distance = math.sqrt((pos.x - previous.x)^2 + (pos.z - previous.z)^2)
|
|
-- Ignore teleport deltas rather than bursting queued footfalls.
|
|
if distance <= 2 then s.step_distance = (s.step_distance or 0) + distance end
|
|
if s.step_time >= (s.sprinting and 0.28 or 0.4) and (s.step_distance or 0) >= 1.4 then
|
|
hearth.feedback_sound("hearth_step_" .. surface(player, s), player)
|
|
s.step_time, s.step_distance = 0, 0
|
|
end
|
|
elseif not s.moving or not s.grounded then s.step_distance = 0 end
|
|
if s.grounded then s.feedback_fall = 0
|
|
else s.feedback_fall = math.min(s.feedback_fall or 0, velocity.y) end
|
|
s.feedback_pos = {x = pos.x, y = pos.y, z = pos.z}
|
|
s.feedback_grounded = s.grounded
|
|
-- Bounded single-entry target cache; all raycasts/HUD traffic <= 10 Hz.
|
|
if t < (s.feedback_next or 0) then return end
|
|
s.feedback_next = t + 0.1
|
|
local message = (s.feedback_until or 0) > t and s.feedback_text or target_text(player)
|
|
local color = (s.feedback_until or 0) > t and (colors[s.feedback_kind] or colors.info) or colors.info
|
|
if not s.feedback_hud then
|
|
s.feedback_hud = player:hud_add({type = "text", position = {x = 0.5, y = 0.72},
|
|
offset = {x = 0, y = 0}, alignment = {x = 0, y = 0}, number = color, text = message})
|
|
elseif message ~= s.feedback_display then player:hud_change(s.feedback_hud, "text", message) end
|
|
if s.feedback_color and color ~= s.feedback_color then player:hud_change(s.feedback_hud, "number", color) end
|
|
s.feedback_display, s.feedback_color = message, color
|
|
end
|
|
C.register_chatcommand("comfort", {
|
|
params = "[default|fov <60..100>]", description = "Session-only static FOV; no imposed camera bob",
|
|
func = function(name, param)
|
|
local player = C.get_player_by_name(name)
|
|
local s = player and session(player)
|
|
if not s then return false, "Player session unavailable." end
|
|
if param == "" then return true, "FOV unchanged by default. Use /comfort fov 75 or /comfort default." end
|
|
local value = param:match("^fov (%d+)$")
|
|
value = value and tonumber(value)
|
|
if param == "default" then value = 0
|
|
elseif not value or value < 60 or value > 100 then return false, "Use default or fov 60..100." end
|
|
s.comfort_fov = value
|
|
return true, "Session camera preference updated."
|
|
end,
|
|
})
|