2024-12-22 03:52:11 +01:00

501 lines
17 KiB
Lua

local S = minetest.get_translator("lzr_treasure")
local NS = function(s) return s end
local CHEST_LID_LIFETIME = 1.0
local victory_job
lzr_treasure = {}
local registered_after_found_treasures = {}
-- Register a function callback(pos, player) that is called
-- when player found a treasure by opening a chest.
-- pos is position of treasure chest that contains treasure.
-- player is player who found it.
lzr_treasure.register_after_found_treasure = function(callback)
table.insert(registered_after_found_treasures, callback)
end
-- Register treasure blocks --
minetest.register_node("lzr_treasure:gold_block", {
description = S("Gold Block"),
tiles = {"lzr_treasure_gold_block.png"},
-- In Lazarr!, the gold block is only for decoration for the
-- treasure stash in the main menu ship. It MUST NOT
-- be used in levels. Use the chests instead to add
-- gold blocks in levels.
groups = { breakable = 1, not_in_creative_inventory = 1 },
sounds = lzr_sounds.node_sound_metal_defaults(),
})
minetest.register_entity("lzr_treasure:chest_lid", {
initial_properties = {
visual = "mesh",
mesh = "lzr_treasure_chest_lid.obj",
visual_size = { x=10, y=10, z=10 },
textures = {
"lzr_treasure_chest_top.png",
"lzr_treasure_chest_top.png",
"lzr_treasure_chest_side.png",
"lzr_treasure_chest_front.png",
},
backface_culling = true,
static_save = false,
physical = false,
collide_with_objects = false,
selectionbox = {
-0.5, 2/16, -0.5, 0.5, 0.5, 0.5,
},
},
_lifetimer = 0,
on_activate = function(self)
self.object:set_armor_groups({immortal=1})
self.object:set_velocity({x=0,y=3,z=0})
end,
on_step = function(self, dtime)
local vel = self.object:get_velocity()
if vel.y > 0 then
vel.y = vel.y * 0.9
if vel.y < 0.001 then
vel.y = 0
end
self.object:set_velocity(vel)
end
self._lifetimer = self._lifetimer + dtime
if self._lifetimer > CHEST_LID_LIFETIME then
if self._chest_node_name then
local cpos = self.object:get_pos()
minetest.add_particlespawner({
amount = 16,
time = 0.001,
minpos = vector.add(cpos, vector.new(-0.5, 7/16, -0.5)),
maxpos = vector.add(cpos, vector.new(0.5, 0.5, 0.5)),
minvel = vector.new(-0.9, -0.1, -0.9),
maxvel = vector.new(0.9, 0.6, 0.9),
minacc = vector.new(0, -lzr_globals.GRAVITY, 0),
maxacc = vector.new(0, -lzr_globals.GRAVITY, 0),
minsize = 0.6,
maxsize = 0.6,
minexptime = 0.5,
maxexptime = 0.55,
node = {name = self._chest_node_name},
})
end
self.object:remove()
return
end
end,
})
-- Register chests --
local register_chest = function(id, def)
-- Every chest comes in 4 states:
-- * Locked
-- * Unlocked
-- * Open with treasure
-- * Open and empty
-- The locked chests break their lock open. One chest variant can also lock itself when in
-- unlocked state.
local treasure_id = "gold"
local sound_open = def.sound_open or "lzr_treasure_chest_open"
local sound_open_fail = def.sound_open_fail or "lzr_treasure_chest_open_fail"
local sound_lock_break = def.sound_lock_break or "lzr_treasure_chest_lock_break"
local sound_lock_regen = def.sound_lock_regen or "lzr_treasure_chest_lock_regen"
local lock_chest = function(pos, node, check_gamestate)
local add_lock = true
local gs = lzr_gamestate.get_state()
if check_gamestate and gs ~= lzr_gamestate.LEVEL and gs ~= lzr_gamestate.LEVEL_TEST and not def.regen_lock then
add_lock = false
end
if add_lock then
minetest.swap_node(pos, {name="lzr_treasure:chest_"..id.."_locked", param2=node.param2})
minetest.sound_play({name=sound_lock_regen, gain=0.8}, {pos=pos}, true)
local dir = minetest.fourdir_to_dir(node.param2)
local w = 3/16
local k = 9/16
local l = 8/16
local minoff, maxoff
if dir.x > 0 then
minoff = vector.new(-k, -w, -w)
maxoff = vector.new(-l, w, w)
elseif dir.x < 0 then
minoff = vector.new(l, -w, -w)
maxoff = vector.new(k, w, w)
elseif dir.z > 0 then
minoff = vector.new(-w, -w, -k)
maxoff = vector.new(w, w, -l)
elseif dir.z < 0 then
minoff = vector.new(-w, -w, l)
maxoff = vector.new(w, w, k)
end
local avgoff = vector.new()
avgoff.x = (minoff.x + maxoff.x) / 2
avgoff.y = (minoff.y + maxoff.y) / 2
avgoff.z = (minoff.z + maxoff.z) / 2
if minoff then
minetest.add_particlespawner({
amount = 42,
time = 0.001,
minpos = vector.add(pos, minoff),
maxpos = vector.add(pos, maxoff),
minsize = 0.5,
maxsize = 0.5,
minexptime = 1.60,
maxexptime = 1.65,
radius = {
min = 0.4,
max = 0.5,
bias = 0.5,
},
attract = {
kind = "point",
strength = 4,
origin = vector.add(pos, avgoff),
},
texture = "lzr_treasure_particle_lock.png",
})
else
minetest.log("error", "[lzr_treasure] Could not find correct position to spawn treasure chest lock particles")
end
end
end
local on_toggle_unlocked
local g_unlocked_receiver
local g_orphan_receiver
local g_unlocked_element
local g_locked_element
local g_unlocked_nici
local tt_help_unlocked = S("Contains a gold block")
local tt_help_locked = S("Contains a gold block")
local element_group = "chest_element"
local after_place_node
local after_dig_node
if def.regen_lock then
on_toggle_unlocked = function(pos, node)
lock_chest(pos, node, true)
end
g_unlocked_receiver = 1
g_unlocked_element = 2
g_locked_element = 1
-- Hide unlocked chest from creative inventory when the lock
-- is able to regenerate to enforce some consistency in the editor.
g_unlocked_nici = 1
tt_help_unlocked = tt_help_unlocked .. "\n"..S("Gets locked when triggered off")
tt_help_locked = tt_help_locked .. "\n"..S("Lock breaks when triggered on, but it re-appears when triggered off")
after_place_node = lzr_laser.trigger_after_place_node
after_dig_node = lzr_laser.trigger_after_dig_node
else
tt_help_locked = tt_help_locked .. "\n"..S("Lock breaks when triggered on")
g_orphan_receiver = 1
end
-- Unlocked chest: Player can punch it to open the chest and reveal a gold block.
-- If all chests have been opened, the level is won.
minetest.register_node("lzr_treasure:chest_"..id.."_unlocked", {
description = def.description_unlocked,
_tt_help = tt_help_unlocked,
paramtype2 = "4dir",
tiles = { def.tile_top, def.tile_bottom, def.tile_side, def.tile_side, def.tile_side, def.tile_front },
groups = { breakable = 1, chest = 1, chest_closed = 1, rotatable = 3, receiver = g_unlocked_receiver, orphan_receiver = 1, chest_element = g_unlocked_element, not_in_creative_inventory = g_unlocked_nici },
sounds = def.node_sounds,
on_punch = function(pos, node, puncher)
-- Open chest lid
local gs = lzr_gamestate.get_state()
if gs ~= lzr_gamestate.LEVEL and gs ~= lzr_gamestate.LEVEL_TEST then
return
end
minetest.log("action", "[lzr_treasure] "..puncher:get_player_name().." opens "..node.name.." at "..minetest.pos_to_string(pos))
minetest.set_node(pos, {name="lzr_treasure:chest_"..id.."_open_"..treasure_id, param2=node.param2})
minetest.sound_play({name=sound_open, gain=0.5}, {pos=pos}, true)
local pos_above = vector.offset(pos, 0, 1, 0)
local node_above = minetest.get_node(pos_above)
local def_above = minetest.registered_nodes[node_above.name]
if not def_above or not def_above.walkable then
-- Spawn chest lid
local lidpos = vector.offset(pos, 0, 0.01, 0)
local obj = minetest.add_entity(lidpos, "lzr_treasure:chest_lid")
if obj then
obj:set_properties({
textures = {
def.tile_top, def.tile_bottom, def.tile_side, def.tile_front,
},
})
local yaw = (3-((node.param2-1) % 4)) * (math.pi/2)
obj:set_yaw(yaw)
local ent = obj:get_luaentity()
if ent then
ent._chest_node_name = "lzr_treasure:chest_"..id.."_unlocked"
end
end
end
-- call callbacks
for c=1, #registered_after_found_treasures do
registered_after_found_treasures[c](pos, puncher)
end
-- Check for level victory after 1 second
if victory_job then
-- cancel old job, restart timer
victory_job:cancel()
end
victory_job = minetest.after(1, function()
local done = lzr_laser.check_level_won()
if done then
lzr_levels.level_complete()
end
victory_job = nil
end)
end,
after_place_node = after_place_node,
after_dig_node = after_dig_node,
_lzr_on_toggle = on_toggle_unlocked,
_lzr_element_group = element_group,
})
local unlock_chest = function(pos, node, check_gamestate)
local break_lock = true
local gs = lzr_gamestate.get_state()
if check_gamestate and gs ~= lzr_gamestate.LEVEL and gs ~= lzr_gamestate.LEVEL_TEST and not def.regen_lock then
break_lock = false
end
if break_lock then
minetest.swap_node(pos, {name="lzr_treasure:chest_"..id.."_unlocked", param2=node.param2})
end
minetest.sound_play({name=sound_lock_break, gain=0.8}, {pos=pos}, true)
local dir = minetest.fourdir_to_dir(node.param2)
local w = 3/16
local k = 9/16
local l = 8/16
local minoff, maxoff
if dir.x > 0 then
minoff = vector.new(-k, -w, -w)
maxoff = vector.new(-l, w, w)
elseif dir.x < 0 then
minoff = vector.new(l, -w, -w)
maxoff = vector.new(k, w, w)
elseif dir.z > 0 then
minoff = vector.new(-w, -w, -k)
maxoff = vector.new(w, w, -l)
elseif dir.z < 0 then
minoff = vector.new(-w, -w, l)
maxoff = vector.new(w, w, k)
end
if minoff then
minetest.add_particlespawner({
amount = 12,
time = 0.001,
minpos = vector.add(pos, minoff),
maxpos = vector.add(pos, maxoff),
minvel = vector.new(-0.5, -0.2, -0.5),
maxvel = vector.new(0.5, 0.5, 0.5),
minacc = vector.new(0, -lzr_globals.GRAVITY, 0),
maxacc = vector.new(0, -lzr_globals.GRAVITY, 0),
minsize = 0.5,
maxsize = 0.5,
minexptime = 0.60,
maxexptime = 0.65,
texture = "lzr_treasure_particle_lock.png",
})
else
minetest.log("error", "[lzr_treasure] Could not find correct position to spawn treasure chest lock particles")
end
end
-- Locked chest: Unlocks itself when receiving an ON signal.
-- One chest variant may re-lock itself again when receiving an OFF signal.
minetest.register_node("lzr_treasure:chest_"..id.."_locked", {
description = def.description_locked,
_tt_help = tt_help_locked,
paramtype2 = "4dir",
tiles = { def.tile_top, def.tile_bottom, def.tile_side, def.tile_side, def.tile_side, def.tile_front_lock },
groups = { breakable = 1, chest = 2, chest_closed = 1, rotatable = 3, receiver = 1, chest_element = g_locked_element },
sounds = def.node_sounds,
on_punch = function(pos, node, puncher)
local gs = lzr_gamestate.get_state()
if gs ~= lzr_gamestate.LEVEL and gs ~= lzr_gamestate.LEVEL_TEST then
return
end
minetest.sound_play({name=sound_open_fail, gain=0.3}, {pos=pos}, true)
end,
after_place_node = lzr_laser.trigger_after_place_node,
after_dig_node = lzr_laser.trigger_after_dig_node,
-- Unlock chest, always
_lzr_unlock = function(pos, node)
unlock_chest(pos, node)
end,
-- Unlocks the chest, but only if playing a level
_lzr_on_toggle = function(pos, node)
unlock_chest(pos, node, true)
end,
_lzr_element_group = element_group,
})
-- Open empty chest. For the player, this is just
-- an obstacle.
-- This element is laser-compatible. A laser can be fired
-- into it from above for a visual-only effect.
lzr_laser.register_element("lzr_treasure:chest_"..id.."_open", {
description = def.description_open,
paramtype = "light",
drawtype = "mesh",
__mesh_off = "lzr_treasure_chest_open.obj",
__mesh_on = "lzr_treasure_chest_open_laser.obj",
__tiles_off = {
{ name = def.tile_top, backface_culling = true },
{ name = def.tile_bottom, backface_culling = true },
{ name = def.tile_side, backface_culling = true },
{ name = def.tile_front, backface_culling = true },
},
__tiles_on = {
{ name = def.tile_top, backface_culling = true },
{ name = def.tile_bottom, backface_culling = true },
{ name = def.tile_side, backface_culling = true },
{ name = def.tile_front, backface_culling = true },
lzr_laser.LASER_TILE,
},
__use_texture_alpha_off = "clip",
__use_texture_alpha_on = lzr_laser.ALPHA_LASER,
__light_source_on = lzr_globals.LASER_GLOW,
collision_box = {
type = "fixed",
fixed = { -0.5, -0.5, -0.5, 0.5, 2/16, 0.5 },
},
selection_box = {
type = "fixed",
fixed = { -0.5, -0.5, -0.5, 0.5, 2/16, 0.5 },
},
paramtype2 = "4dir",
groups = { breakable = 1, chest = 3, laser_block = 1, rotatable = 3, orphan_receiver = 1 },
sounds = def.node_sounds,
}, { group = "chest_open" })
-- Open chest with treasure (gold block).
-- Appears when the player opened an unlocked chest.
-- This block does nothing on its own, but when all chests
-- in the level have been opened and reached this
-- state (ignoring empty chests), the level counts as won.
local treasure = "lzr_treasure:gold_block"
local treasure_tile_top = "lzr_treasure_gold_block_in_chest_top.png"
local treasure_tile_side = "lzr_treasure_gold_block_in_chest_side.png"
local treasure_def = minetest.registered_nodes[treasure]
local sounds_open_treasure = table.copy(def.node_sounds)
sounds_open_treasure.footstep = table.copy(treasure_def.sounds.footstep)
sounds_open_treasure.dig = nil
sounds_open_treasure.dug = table.copy(treasure_def.sounds.dug)
minetest.register_node("lzr_treasure:chest_"..id.."_open_"..treasure_id, {
description = S(def.description_open_with, treasure_def.description),
drawtype = "nodebox",
paramtype = "light",
node_box = {
type = "fixed",
fixed = {
{ -0.5, -0.5, -0.5, 0.5, 2/16, 0.5 },
{ -7/16, 2/16, -7/16, 7/16, 0.5, 7/16 },
},
},
tiles = {
"("..def.tile_top..")^("..treasure_tile_top.."^[mask:lzr_treasure_mask_block_in_chest_top.png)",
def.tile_bottom,
"("..def.tile_side..")^("..treasure_tile_side.."^[mask:lzr_treasure_mask_block_in_chest_side.png)",
"("..def.tile_side..")^("..treasure_tile_side.."^[mask:lzr_treasure_mask_block_in_chest_side.png)",
"("..def.tile_side..")^("..treasure_tile_side.."^[mask:lzr_treasure_mask_block_in_chest_side.png)",
"("..def.tile_front..")^("..treasure_tile_side.."^[mask:lzr_treasure_mask_block_in_chest_side.png)",
},
-- Removes the treasure from the chest (turning it into an open empty chest)
-- and spawns particles (for level finish)
_lzr_send_treasure = function(pos, node)
local jobs = {}
for i=0,30 do
local job = minetest.after(i*0.0625, function(it)
if lzr_gamestate.get_state() ~= lzr_gamestate.LEVEL_COMPLETE then
for j=1, #jobs do
jobs[j]:cancel()
end
return
end
local amount = 10
if it > 20 then
amount = 31 - it
end
minetest.add_particlespawner({
amount = amount,
time = 0.001,
minpos = vector.add(pos, vector.new(-7/16, -4/16+it*0.5, -7/16)),
maxpos = vector.add(pos, vector.new(7/16, 4/16+it*0.5, 7/16)),
minvel = vector.new(-0.2, -0.2, -0.2),
maxvel = vector.new(0.2, 0.2, 0.2),
minsize = 0.5,
maxsize = 0.5,
node = {name=treasure},
})
end, i)
table.insert(jobs, job)
end
minetest.set_node(pos, {name="lzr_treasure:chest_"..id.."_open", param2=node.param2})
end,
paramtype2 = "4dir",
groups = { breakable = 1, chest = 4, chest_open = 1, chest_open_treasure = 1, rotatable = 3, orphan_receiver = 1, },
sounds = sounds_open_treasure,
})
end
register_chest("wood", {
--~ Block description
description_unlocked = S("Wooden Chest"),
--~ Block description
description_locked = S("Locked Wooden Chest"),
--~ Block description
description_open = S("Open Wooden Chest"),
--~ Block description
description_open_laser = S("Open Wooden Chest with Laser"),
--~ Block description. @1 = a treasure
description_open_with = NS("Open Wooden Chest with @1"),
tile_top = "lzr_treasure_chest_top.png",
tile_bottom = "lzr_treasure_chest_top.png",
tile_side = "lzr_treasure_chest_side.png",
tile_front = "lzr_treasure_chest_front.png",
tile_front_lock = "lzr_treasure_chest_lock.png",
node_sounds = lzr_sounds.node_sound_wood_defaults(),
})
register_chest("dark", {
--~ Block description
description_unlocked = S("Onyx Chest"),
--~ Block description
description_locked = S("Locked Onyx Chest"),
--~ Block description
description_open = S("Open Onyx Chest"),
--~ Block description
description_open_laser = S("Open Onyx Chest with Laser"),
--~ Block description. @1 = a treasure
description_open_with = NS("Open Onyx Chest with @1"),
tile_top = "lzr_treasure_dark_chest_top.png",
tile_bottom = "lzr_treasure_dark_chest_top.png",
tile_side = "lzr_treasure_dark_chest_side.png",
tile_front = "lzr_treasure_dark_chest_front.png",
tile_front_lock = "lzr_treasure_dark_chest_lock.png",
node_sounds = lzr_sounds.node_sound_stone_defaults(),
-- The dark chest can regenerate its lock when toggled off
regen_lock = true,
})
minetest.register_alias("lzr_treasure:chest_wood_open", "lzr_treasure:chest_wood_open_fixed")
minetest.register_alias("lzr_treasure:chest_dark_open", "lzr_treasure:chest_dark_open_fixed")
minetest.register_alias("lzr_treasure:chest_wood_open_laser", "lzr_treasure:chest_wood_open_fixed_on_1")
minetest.register_alias("lzr_treasure:chest_dark_open_laser", "lzr_treasure:chest_dark_open_fixed_on_1")