A => .cdb.json +17 -0
@@ 1,17 @@
+{
+ "type": "MOD",
+ "name": "hide_and_seek",
+ "title": "Hide and Seek",
+ "dev_state": "WIP",
+ "tags": [
+ "mini-game",
+ "multiplayer"
+ ],
+ "license": "AGPL-3.0-or-later",
+ "media_license": "CC-BY-SA-4.0",
+ "repo": "https://git.sr.ht/~fgaz/minetest-hide_and_seek",
+ "website": "https://sr.ht/~fgaz/minetest-hide_and_seek/",
+ "issue_tracker": "https://todo.sr.ht/~fgaz/minetest-mods",
+ "short_description": "Hide and Seek minigame using arena_lib",
+ "long_description": "# Minetest Hide and Seek\n\nHide and Seek minigame in [Minetest](https://minetest.net)\nusing [arena_lib](https://content.minetest.net/packages/Zughy/arena_lib/)\n\n## Rules\n\nThe game is fairly simple: seekers (marked with a red skin) have to punch hiders\nwith their stick, and hiders have to avoid getting punched.\nThe game ends when all hiders are found, or after a timeout.\n\n## Scoring\n\n* Each player starts with 1 point\n* Seeker(s) gain 1 point for every hider they find\n * Initial seekers always get a point even if the hider was found by someone else\n * Infected seekers only get a point for hiders they personally found\n* Hiders gain a point every time another hider gets found\n == hiders get increasing points depending on the order they get caught\n* Initial (not infected) seeker(s) gain 3 points if they catch every hider\n* If a hider makes it to the end, they gain 3 bonus points\n\n## Play styles\n\nDepending on how the arena is built, different play styles can emerge:\n\n* Big arena with hiding places, no nametags: hiders should try to find a good\n hiding place, there will be more seeking, the game will be slower-paced.\n* Small parkourish arena, nametags: hiders should keep moving, there will be\n more chasing, the game will be faster and dynamic.\n\n## License\n\n* Code is `AGPL-3.0-or-later`\n* Assets are `CC-BY-SA-4.0`\n\n## Credits\n\n* [arena_lib](https://content.minetest.net/packages/Zughy/arena_lib/)\n* [panel_lib](https://content.minetest.net/packages/Zughy/panel_lib/)\n* [MKWii HideNSeek](https://wiki.tockdom.com/wiki/Hide_and_Seek) for the point system.\n"
+}
A => .luacheckrc +70 -0
@@ 1,70 @@
+local used_minetest_fields = {
+ get_modpath = {},
+ get_translator = {},
+ register_tool = {},
+ register_privilege = {},
+ get_player_by_name = {},
+ chat_send_player = {},
+}
+
+local used_arena_lib_fields = {
+ register_minigame = {},
+ create_arena = {},
+ print_arena_info = {},
+ print_arenas = {},
+ rename_arena = {},
+ enter_editor = {},
+ set_sign = {},
+ set_timer = {},
+ change_arena_property = {},
+ get_arena_by_player = {},
+ remove_player_from_arena = {},
+ HUD_send_msg = {},
+ HUD_send_msg_all = {},
+ on_load = {},
+ on_start = {},
+ on_timeout = {},
+ on_celebration = {},
+ on_end = {},
+ load_celebration = {},
+}
+
+stds.minetest = {
+ read_globals = {
+ minetest = {
+ fields = used_minetest_fields,
+ },
+ ItemStack = {},
+ vector = {
+ fields = {
+ zero = {},
+ },
+ },
+ declarative_chatcmd = {
+ fields = {
+ register_chatcommand = {},
+ },
+ },
+ arena_lib = {
+ fields = used_arena_lib_fields,
+ },
+ Panel = {
+ fields = {
+ new = {},
+ },
+ },
+ enderpearl = {
+ fields = {
+ block_teleport = {},
+ },
+ },
+ },
+}
+
+std = "luajit+minetest"
+
+globals = {
+ hide_and_seek = {
+ other_fields = true,
+ },
+}
A => Makefile +15 -0
@@ 1,15 @@
+MODNAME=hide_and_seek
+
+.PHONY: dist
+dist: check
+ git archive --format=zip --prefix=$(MODNAME)/ --output=minetest-$(MODNAME).zip HEAD
+
+# We also check that the cdb file is up to date.
+# It needs to be committed so that CDB can auto-import/update the mod
+.PHONY: check
+check:
+ luacheck .
+ jq --rawfile readme README.md '.long_description=$$readme' cdb-template.json | diff .cdb.json -
+
+.cdb.json: cdb-template.json README.md
+ jq --rawfile readme README.md '.long_description=$$readme' cdb-template.json > .cdb.json
A => README.md +41 -0
@@ 1,41 @@
+# Minetest Hide and Seek
+
+Hide and Seek minigame in [Minetest](https://minetest.net)
+using [arena_lib](https://content.minetest.net/packages/Zughy/arena_lib/)
+
+## Rules
+
+The game is fairly simple: seekers (marked with a red skin) have to punch hiders
+with their stick, and hiders have to avoid getting punched.
+The game ends when all hiders are found, or after a timeout.
+
+## Scoring
+
+* Each player starts with 1 point
+* Seeker(s) gain 1 point for every hider they find
+ * Initial seekers always get a point even if the hider was found by someone else
+ * Infected seekers only get a point for hiders they personally found
+* Hiders gain a point every time another hider gets found
+ == hiders get increasing points depending on the order they get caught
+* Initial (not infected) seeker(s) gain 3 points if they catch every hider
+* If a hider makes it to the end, they gain 3 bonus points
+
+## Play styles
+
+Depending on how the arena is built, different play styles can emerge:
+
+* Big arena with hiding places, no nametags: hiders should try to find a good
+ hiding place, there will be more seeking, the game will be slower-paced.
+* Small parkourish arena, nametags: hiders should keep moving, there will be
+ more chasing, the game will be faster and dynamic.
+
+## License
+
+* Code is `AGPL-3.0-or-later`
+* Assets are `CC-BY-SA-4.0`
+
+## Credits
+
+* [arena_lib](https://content.minetest.net/packages/Zughy/arena_lib/)
+* [panel_lib](https://content.minetest.net/packages/Zughy/panel_lib/)
+* [MKWii HideNSeek](https://wiki.tockdom.com/wiki/Hide_and_Seek) for the point system.
A => cdb-template.json +12 -0
@@ 1,12 @@
+{ "type": "MOD"
+, "name": "hide_and_seek"
+, "title": "Hide and Seek"
+, "dev_state": "WIP"
+, "tags": ["mini-game", "multiplayer"]
+, "license": "AGPL-3.0-or-later"
+, "media_license": "CC-BY-SA-4.0"
+, "repo": "https://git.sr.ht/~fgaz/minetest-hide_and_seek"
+, "website": "https://sr.ht/~fgaz/minetest-hide_and_seek/"
+, "issue_tracker": "https://todo.sr.ht/~fgaz/minetest-mods"
+, "short_description": "Hide and Seek minigame using arena_lib"
+}
A => commands.lua +79 -0
@@ 1,79 @@
+local modname = "hide_and_seek"
+
+local arg_arena = {
+ name = "arena",
+ description = "The name of the arena to affect",
+}
+
+local simple_arena_action = function (description, func) return {
+ description = description,
+ args = { arg_arena, },
+ func = function(sender, arena_name)
+ func(sender, modname, arena_name)
+ end,
+} end
+
+declarative_chatcmd.register_chatcommand("hide_and_seek_admin", {
+ description = "Manage Hide and Seek arenas",
+ privs = { hide_and_seek_admin = true },
+ subcommands = {
+ create = simple_arena_action("Create a new arena", arena_lib.create_arena),
+ remove = simple_arena_action("Remove an existing arena", arena_lib.print_arena_info),
+ rename = {
+ description = "Rename an existing arena",
+ args = {
+ arg_arena,
+ {
+ name = "new_name",
+ description = "The new name to give the arena";
+ },
+ },
+ func = function(sender, arena_name, new_name)
+ arena_lib.rename_arena(sender, modname, arena_name, new_name)
+ end
+ },
+ list = {
+ description = "List existing arenas",
+ func = function(sender)
+ arena_lib.print_arenas(sender, modname)
+ end,
+ },
+ info = simple_arena_action("Print information about an existing arena", arena_lib.print_arena_info),
+ edit = simple_arena_action("Modify an existing arena", arena_lib.enter_editor),
+ -- TODO pos is now mandatory
+ setsign = simple_arena_action("Link a sign to an existing arena", arena_lib.set_sign),
+ infection = {
+ description = [[
+ Enable or disable infection mode. When infection mode is enabled,
+ found players become seekers instead of being eliminated.
+ ]],
+ args = {
+ arg_arena,
+ {
+ name = "enabled",
+ type = "boolean",
+ description = "Whether to enable infection mode",
+ },
+ },
+ func = function (sender, arena, enable)
+ arena_lib.change_arena_property(sender, modname, arena, "infection", enable)
+ end,
+ },
+ timer = {
+ description = [[
+ Change how long a match should last
+ ]],
+ args = {
+ arg_arena,
+ {
+ name = "time",
+ type = "number",
+ description = "The duration in seconds",
+ },
+ },
+ func = function (sender, arena, time)
+ arena_lib.set_timer(sender, modname, arena, time)
+ end,
+ },
+ },
+})
A => init.lua +150 -0
@@ 1,150 @@
+local modname = "hide_and_seek"
+
+dofile(minetest.get_modpath(modname) .. "/commands.lua")
+local lib = dofile(minetest.get_modpath(modname) .. "/lib.lua")
+local items = dofile(minetest.get_modpath(modname) .. "/items.lua")
+
+minetest.register_privilege("hide_and_seek_admin")
+
+
+arena_lib.register_minigame(modname, {
+ name = "Hide and Seek",
+ prefix = "[H&S] ",
+ hotbar = { slots = 2 }, -- stick/arm and end pearl
+ show_nametags = true, -- TODO depends on the arena shape
+ time_mode = "decremental",
+ -- MAYBE make these two configurable
+ load_time = 10, -- Amount of time the seeker is locked in place
+ celebration_time = 10,
+ disabled_damage_types = {
+ "set_hp", -- avoid damage from enderpearls
+ "punch",
+ "fall",
+ "node_damage",
+ "drown",
+ "respawn",
+ },
+ disable_inventory = true,
+ properties = {
+ infection = false,
+ seeker_pearls = 5,
+ hider_pearls = 3,
+ -- TODO:
+ -- seeker location?
+ -- setting to give speed to seekers?
+ -- setting to make the first player that clicked the sign the seeker?
+ -- initial seeker amount?
+ },
+ temp_properties = {
+ started = false,
+ -- [pl_name] = bool
+ initial_seekers = {},
+ -- [n] = {pl_name,score}
+ final_scores = {},
+ },
+ player_properties = {
+ seeker = false,
+ initial_seeker = false,
+ points = 1,
+ },
+})
+
+arena_lib.on_load(modname, function(arena)
+ local player_names = lib.table_keys(arena.players)
+ local initial_seeker = player_names[math.random(1, #player_names)]
+ for pl_name, pl_props in pairs(arena.players) do
+ local player = minetest.get_player_by_name(pl_name)
+ if pl_name == initial_seeker then
+ pl_props.seeker = true
+ pl_props.initial_seeker = true
+ arena.initial_seekers[pl_name] = true
+ lib.set_seeker_skin(player)
+ pl_props.physics = player:get_physics_override()
+ -- Lock the seeker in place
+ player:set_physics_override()
+ player:set_velocity(vector.zero())
+ arena_lib.HUD_send_msg("title", pl_name, "You are a Seeker! Wait a bit...", 5, nil, 0xFF2222)
+ else
+ items.set_hider_inventory(player, arena)
+ arena_lib.HUD_send_msg("title", pl_name, "You are a Hider! Run!", 5, nil, 0x22FF22)
+ end
+ end
+end)
+
+arena_lib.on_start(modname, function(arena)
+ arena.started = true
+ for pl_name, _ in pairs(arena.initial_seekers) do
+ local player = minetest.get_player_by_name(pl_name)
+ items.set_seeker_inventory(player, arena)
+ player:set_physics_override(arena.players[pl_name].physics)
+ end
+ arena_lib.HUD_send_msg_all("title", arena, "Let the hunt begin!", 3, nil, 0xFF2222)
+end)
+
+arena_lib.on_timeout(modname, function(arena)
+ -- Add final bonus
+ for pl_name, pl_props in pairs(arena.players) do
+ if not pl_props.seeker then
+ pl_props.points = pl_props.points + 3
+ end
+ end
+ arena_lib.load_celebration(modname, arena, nil)
+end)
+
+arena_lib.on_celebration(modname, function(arena)
+ -- Add remaining players to final_scores
+ for pl_name, pl_props in pairs(arena.players) do
+ table.insert(arena.final_scores, {
+ pl_name = pl_name,
+ score = pl_props.points,
+ })
+ end
+
+ table.sort(arena.final_scores, function(a, b) return a.score > b.score end)
+
+ -- Create scoreboard
+ local scores_panel_elems = {}
+ local offset = 0
+ for _, score_pair in pairs(arena.final_scores) do
+ offset = offset + 16
+ table.insert(scores_panel_elems, {
+ text = score_pair.pl_name,
+ alignment = {x = 0, y = 0},
+ offset = {x = -64, y = offset},
+ })
+ table.insert(scores_panel_elems, {
+ text = tostring(score_pair.score),
+ alignment = {x = 0, y = 0},
+ offset = {x = 64, y = offset},
+ })
+ end
+ -- TODO send scoreboard to spectators too
+ for pl_name, pl_props in pairs(arena.players) do
+ local scoreboard = Panel:new(modname .. ":scoreboard", {
+ player = pl_name,
+ title = "Hide and Seek results",
+ title_offset = {x = 0, y = -128},
+ position = {x = 0.5, y = 0.5},
+ alignment = {x = 0, y = 0},
+ bg_scale = {x = 24, y = 24},
+ sub_txt_elems = scores_panel_elems,
+ })
+ pl_props.scoreboard = scoreboard
+ end
+
+ -- TODO sound
+end)
+
+arena_lib.on_end(modname, function(arena, players, winners, spectators, is_forced)
+ -- TODO do most of this stuff in on_quit/on_eliminate too
+ for pl_name, pl_props in pairs(players) do
+ local player = minetest.get_player_by_name(pl_name)
+ -- TODO remove scoreboard from spectators too
+ if pl_props.scoreboard then pl_props.scoreboard:remove() end
+ lib.reset_seeker_skin(minetest.get_player_by_name(pl_name))
+ -- Prevent pearl shenanigans
+ enderpearl.block_teleport(player, 10)
+ end
+end)
+
+-- TODO on_quit, terminate the game if there are no more seekers/hiders (or assign a new seeker)
A => items.lua +97 -0
@@ 1,97 @@
+local modname = "hide_and_seek"
+local items = {}
+
+local lib = dofile(minetest.get_modpath(modname) .. "/lib.lua")
+
+function items.set_seeker_inventory(player, arena)
+ local sword = ItemStack(modname .. ":stick")
+ local enderpearls = ItemStack("enderpearl:ender_pearl " .. arena.seeker_pearls)
+ player:get_inventory():set_stack("main", 1, sword)
+ player:get_inventory():set_stack("main", 2, enderpearls)
+end
+function items.set_hider_inventory(player, arena)
+ local enderpearls = ItemStack("enderpearl:ender_pearl " .. arena.hider_pearls)
+ player:get_inventory():set_stack("main", 2, enderpearls)
+end
+
+local function punch(itemstack, puncher, pointed_thing)
+ if not puncher:is_player() then return end
+ if pointed_thing.type ~= "object" then return end
+ if not pointed_thing.ref:is_player() then return end
+ local pointed_player = pointed_thing.ref
+ local puncher_name = puncher:get_player_name()
+ local pointed_name = pointed_player:get_player_name()
+ local arena = arena_lib.get_arena_by_player(puncher_name)
+ if not arena.started then return end
+ -- Check that everything exists, in case players inside and outside manage to interact
+ if not arena then return end
+ if not arena.players[puncher_name] then return end
+ if not arena.players[pointed_name] then return end
+ -- Just to be sure (only seekers can have this tool anyway):
+ if not arena.players[puncher_name].seeker then return end
+ if arena.players[pointed_name].seeker then return end
+
+ -- Now we know that an actual seeker-to-hider punch happened
+ minetest.chat_send_player(puncher_name, "Found " .. pointed_name .. "!")
+ minetest.chat_send_player(pointed_name, "Found by " .. puncher_name .. "!")
+
+ -- TODO sound
+ -- TODO chat announcement
+ -- TODO hud with remaining hider number and timer
+
+ -- Infect or eliminate
+ if arena.infection then
+ arena.players[pointed_name].seeker = true
+ items.set_seeker_inventory(pointed_player, arena)
+ lib.set_seeker_skin(pointed_player)
+ arena_lib.HUD_send_msg("title", pointed_name, "You got infected!", 5, nil, 0xFF2222)
+ else
+ -- MAYBE move some of this to on_eliminate
+ arena_lib.HUD_send_msg("title", pointed_name, "You got found!", 10, nil, 0xFF2222)
+ table.insert(arena.final_scores, {
+ pl_name = pointed_name,
+ score = arena.players[pointed_name].points,
+ })
+ -- Prevent pearl shenanigans
+ enderpearl.block_teleport(pointed_player, 10)
+ arena_lib.remove_player_from_arena(pointed_player:get_player_name(), 1, puncher:get_player_name())
+ end
+
+ -- Give points.
+ -- If the player was infected (not initial seeker), they will not receive
+ -- the initial seeker point, so give the point now:
+ if not arena.players[puncher_name].initial_seeker then
+ arena.players[puncher_name].points = arena.players[puncher_name].points + 1
+ end
+ -- Give point to every initial seeker and to every not yet found hider:
+ for _, pl_props in pairs(arena.players) do
+ if pl_props.initial_seeker or not pl_props.seeker then
+ pl_props.points = pl_props.points + 1
+ end
+ end
+
+ -- Check for game end, give points and terminate
+ local end_game = true
+ for _, pl_props in pairs(arena.players) do
+ if not pl_props.seeker then end_game = false; break end
+ end
+ if end_game then
+ for pl_name, _ in pairs(arena.initial_seekers) do
+ arena.players[pl_name].points = arena.players[pl_name].points + 3
+ end
+ arena_lib.load_celebration(modname, arena, nil)
+ end
+end
+
+-- TODO better name and texture
+minetest.register_tool(modname .. ":stick", {
+ description = "Seeker's Stick\nHit players to eliminate or infect them",
+ inventory_image = "hide_and_seek_stick.png",
+ -- TODO 0 damage (even though it's already disabled in the arena props)
+ on_use = punch,
+ on_drop = function(itemstack, dropper, pos)
+ -- Make it undroppable
+ end,
+})
+
+return items
A => lib.lua +29 -0
@@ 1,29 @@
+local lib = {}
+
+function lib.table_keys(t)
+ local keys = {}
+ for k,_ in pairs(t) do
+ table.insert(keys, k)
+ end
+ return keys
+end
+
+
+function lib.set_seeker_skin(player)
+ player:set_properties({
+ textures = {
+ player:get_properties().textures[1] ..
+ "^[colorize:red:85",
+ },
+ })
+end
+
+function lib.reset_seeker_skin(player)
+ player:set_properties({
+ textures = {
+ string.gsub(player:get_properties().textures[1], "%^%[colorize:red:85", ""),
+ },
+ })
+end
+
+return lib
A => mod.conf +4 -0
@@ 1,4 @@
+name = hide_and_seek
+title = Hide and Seek
+description = Hide and Seek minigame using arena_lib
+depends = arena_lib, panel_lib, enderpearl, declarative_chatcmd
A => shell.nix +10 -0
@@ 1,10 @@
+{ pkgs ? import <nixpkgs> {} }:
+
+pkgs.mkShell {
+ nativeBuildInputs = [
+ pkgs.lua
+ pkgs.luajit
+ pkgs.luaPackages.luacheck
+ pkgs.jq
+ ];
+}
A => textures/hide_and_seek_stick.png +0 -0