From 60b92762d5e0700ffe9b4297c9fc80c42054805d Mon Sep 17 00:00:00 2001 From: Francesco Gazzetta Date: Sat, 29 Oct 2022 22:54:55 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .cdb.json | 17 ++++ .luacheckrc | 70 +++++++++++++++ Makefile | 15 ++++ README.md | 41 +++++++++ cdb-template.json | 12 +++ commands.lua | 79 ++++++++++++++++ init.lua | 150 +++++++++++++++++++++++++++++++ items.lua | 97 ++++++++++++++++++++ lib.lua | 29 ++++++ mod.conf | 4 + shell.nix | 10 +++ textures/hide_and_seek_stick.png | Bin 0 -> 106 bytes 12 files changed, 524 insertions(+) create mode 100644 .cdb.json create mode 100644 .luacheckrc create mode 100644 Makefile create mode 100644 README.md create mode 100644 cdb-template.json create mode 100644 commands.lua create mode 100644 init.lua create mode 100644 items.lua create mode 100644 lib.lua create mode 100644 mod.conf create mode 100644 shell.nix create mode 100644 textures/hide_and_seek_stick.png diff --git a/.cdb.json b/.cdb.json new file mode 100644 index 0000000..30bc91f --- /dev/null +++ b/.cdb.json @@ -0,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" +} diff --git a/.luacheckrc b/.luacheckrc new file mode 100644 index 0000000..43d0ecb --- /dev/null +++ b/.luacheckrc @@ -0,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, + }, +} diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..11000dd --- /dev/null +++ b/Makefile @@ -0,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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..533389e --- /dev/null +++ b/README.md @@ -0,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. diff --git a/cdb-template.json b/cdb-template.json new file mode 100644 index 0000000..f786747 --- /dev/null +++ b/cdb-template.json @@ -0,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" +} diff --git a/commands.lua b/commands.lua new file mode 100644 index 0000000..18df123 --- /dev/null +++ b/commands.lua @@ -0,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, + }, + }, +}) diff --git a/init.lua b/init.lua new file mode 100644 index 0000000..110d715 --- /dev/null +++ b/init.lua @@ -0,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) diff --git a/items.lua b/items.lua new file mode 100644 index 0000000..67ac223 --- /dev/null +++ b/items.lua @@ -0,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 diff --git a/lib.lua b/lib.lua new file mode 100644 index 0000000..d64563f --- /dev/null +++ b/lib.lua @@ -0,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 diff --git a/mod.conf b/mod.conf new file mode 100644 index 0000000..01565e4 --- /dev/null +++ b/mod.conf @@ -0,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 diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..0b9f3f2 --- /dev/null +++ b/shell.nix @@ -0,0 +1,10 @@ +{ pkgs ? import {} }: + +pkgs.mkShell { + nativeBuildInputs = [ + pkgs.lua + pkgs.luajit + pkgs.luaPackages.luacheck + pkgs.jq + ]; +} diff --git a/textures/hide_and_seek_stick.png b/textures/hide_and_seek_stick.png new file mode 100644 index 0000000000000000000000000000000000000000..44c6987fe54bf2f05bb8c88baf81cd265ede8d16 GIT binary patch literal 106 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61SBU+%rFB|Y$ZW{!9W@a@|Lkr1XA*zE{-7; xjL8aeM^-ZnYOb1nwn3#gw11UcsBh&WR|W=~LaF<1h9&MGEuOA^F6*2UngBIa7@`0G literal 0 HcmV?d00001 -- 2.45.2