diff --git a/README.md b/README.md index 97e1c57..7fed062 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,17 @@ -# sumo_with_spectate +# sumo -adds a spectate functionality, which is in beta phase. Needs testing. For now, you can only spectate 1 player until they die, then you switch. Also, you die when the player you are spectating dies. \ No newline at end of file +Knock other players off the platform to win + +![](screenshot.png) + +Contributors: Zughy, MisterE + +Schematics for arenas can be found here: + +https://notabug.org/MisterE123/sumo_arenas + + + ^ + // \\ + // ! \\ Caution: This mod gives players the interact priv when they join the server. This is because interact is temporarily revoked during spectation mode. + ------- diff --git a/chatcmdbuilder.lua b/chatcmdbuilder.lua new file mode 100644 index 0000000..705b965 --- /dev/null +++ b/chatcmdbuilder.lua @@ -0,0 +1,306 @@ +ChatCmdBuilder = {} + +function ChatCmdBuilder.new(name, func, def) + def = def or {} + local cmd = ChatCmdBuilder.build(func) + cmd.def = def + def.func = cmd.run + minetest.register_chatcommand(name, def) + return cmd +end + +local STATE_READY = 1 +local STATE_PARAM = 2 +local STATE_PARAM_TYPE = 3 +local bad_chars = {} +bad_chars["("] = true +bad_chars[")"] = true +bad_chars["."] = true +bad_chars["%"] = true +bad_chars["+"] = true +bad_chars["-"] = true +bad_chars["*"] = true +bad_chars["?"] = true +bad_chars["["] = true +bad_chars["^"] = true +bad_chars["$"] = true +local function escape(char) + if bad_chars[char] then + return "%" .. char + else + return char + end +end + +local dprint = function() end + +ChatCmdBuilder.types = { + pos = "%(? *(%-?[%d.]+) *, *(%-?[%d.]+) *, *(%-?[%d.]+) *%)?", + text = "(.+)", + number = "(%-?[%d.]+)", + int = "(%-?[%d]+)", + word = "([^ ]+)", + alpha = "([A-Za-z]+)", + modname = "([a-z0-9_]+)", + alphascore = "([A-Za-z_]+)", + alphanumeric = "([A-Za-z0-9]+)", + username = "([A-Za-z0-9-_]+)", +} + +function ChatCmdBuilder.build(func) + local cmd = { + _subs = {} + } + function cmd:sub(route, func, def) + dprint("Parsing " .. route) + + def = def or {} + if string.trim then + route = string.trim(route) + end + + local sub = { + pattern = "^", + params = {}, + func = func + } + + -- End of param reached: add it to the pattern + local param = "" + local param_type = "" + local should_be_eos = false + local function finishParam() + if param ~= "" and param_type ~= "" then + dprint(" - Found param " .. param .. " type " .. param_type) + + local pattern = ChatCmdBuilder.types[param_type] + if not pattern then + error("Unrecognised param_type=" .. param_type) + end + + sub.pattern = sub.pattern .. pattern + + table.insert(sub.params, param_type) + + param = "" + param_type = "" + end + end + + -- Iterate through the route to find params + local state = STATE_READY + local catching_space = false + local match_space = " " -- change to "%s" to also catch tabs and newlines + local catch_space = match_space.."+" + for i = 1, #route do + local c = route:sub(i, i) + if should_be_eos then + error("Should be end of string. Nothing is allowed after a param of type text.") + end + + if state == STATE_READY then + if c == ":" then + dprint(" - Found :, entering param") + state = STATE_PARAM + param_type = "word" + catching_space = false + elseif c:match(match_space) then + print(" - Found space") + if not catching_space then + catching_space = true + sub.pattern = sub.pattern .. catch_space + end + else + catching_space = false + sub.pattern = sub.pattern .. escape(c) + end + elseif state == STATE_PARAM then + if c == ":" then + dprint(" - Found :, entering param type") + state = STATE_PARAM_TYPE + param_type = "" + elseif c:match(match_space) then + print(" - Found whitespace, leaving param") + state = STATE_READY + finishParam() + catching_space = true + sub.pattern = sub.pattern .. catch_space + elseif c:match("%W") then + dprint(" - Found nonalphanum, leaving param") + state = STATE_READY + finishParam() + sub.pattern = sub.pattern .. escape(c) + else + param = param .. c + end + elseif state == STATE_PARAM_TYPE then + if c:match(match_space) then + print(" - Found space, leaving param type") + state = STATE_READY + finishParam() + catching_space = true + sub.pattern = sub.pattern .. catch_space + elseif c:match("%W") then + dprint(" - Found nonalphanum, leaving param type") + state = STATE_READY + finishParam() + sub.pattern = sub.pattern .. escape(c) + else + param_type = param_type .. c + end + end + end + dprint(" - End of route") + finishParam() + sub.pattern = sub.pattern .. "$" + dprint("Pattern: " .. sub.pattern) + + table.insert(self._subs, sub) + end + + if func then + func(cmd) + end + + cmd.run = function(name, param) + for i = 1, #cmd._subs do + local sub = cmd._subs[i] + local res = { string.match(param, sub.pattern) } + if #res > 0 then + local pointer = 1 + local params = { name } + for j = 1, #sub.params do + local param = sub.params[j] + if param == "pos" then + local pos = { + x = tonumber(res[pointer]), + y = tonumber(res[pointer + 1]), + z = tonumber(res[pointer + 2]) + } + table.insert(params, pos) + pointer = pointer + 3 + elseif param == "number" or param == "int" then + table.insert(params, tonumber(res[pointer])) + pointer = pointer + 1 + else + table.insert(params, res[pointer]) + pointer = pointer + 1 + end + end + if table.unpack then + -- lua 5.2 or later + return sub.func(table.unpack(params)) + else + -- lua 5.1 or earlier + return sub.func(unpack(params)) + end + end + end + return false, "Invalid command" + end + + return cmd +end + +local function run_tests() + if not (ChatCmdBuilder.build(function(cmd) + cmd:sub("bar :one and :two:word", function(name, one, two) + if name == "singleplayer" and one == "abc" and two == "def" then + return true + end + end) + end)).run("singleplayer", "bar abc and def") then + error("Test 1 failed") + end + + local move = ChatCmdBuilder.build(function(cmd) + cmd:sub("move :target to :pos:pos", function(name, target, pos) + if name == "singleplayer" and target == "player1" and + pos.x == 0 and pos.y == 1 and pos.z == 2 then + return true + end + end) + end).run + if not move("singleplayer", "move player1 to 0,1,2") then + error("Test 2 failed") + end + if not move("singleplayer", "move player1 to (0,1,2)") then + error("Test 3 failed") + end + if not move("singleplayer", "move player1 to 0, 1,2") then + error("Test 4 failed") + end + if not move("singleplayer", "move player1 to 0 ,1, 2") then + error("Test 5 failed") + end + if not move("singleplayer", "move player1 to 0, 1, 2") then + error("Test 6 failed") + end + if not move("singleplayer", "move player1 to 0 ,1 ,2") then + error("Test 7 failed") + end + if not move("singleplayer", "move player1 to ( 0 ,1 ,2)") then + error("Test 8 failed") + end + if move("singleplayer", "move player1 to abc,def,sdosd") then + error("Test 9 failed") + end + if move("singleplayer", "move player1 to abc def sdosd") then + error("Test 10 failed") + end + + if not (ChatCmdBuilder.build(function(cmd) + cmd:sub("does :one:int plus :two:int equal :three:int", function(name, one, two, three) + if name == "singleplayer" and one + two == three then + return true + end + end) + end)).run("singleplayer", "does 1 plus 2 equal 3") then + error("Test 11 failed") + end + + local checknegint = ChatCmdBuilder.build(function(cmd) + cmd:sub("checknegint :x:int", function(name, x) + return x + end) + end).run + if checknegint("checker","checknegint -2") ~= -2 then + error("Test 12 failed") + end + + local checknegnumber = ChatCmdBuilder.build(function(cmd) + cmd:sub("checknegnumber :x:number", function(name, x) + return x + end) + end).run + if checknegnumber("checker","checknegnumber -3.3") ~= -3.3 then + error("Test 13 failed") + end + + local checknegpos = ChatCmdBuilder.build(function(cmd) + cmd:sub("checknegpos :pos:pos", function(name, pos) + return pos + end) + end).run + local negpos = checknegpos("checker","checknegpos (-13.3,-4.6,-1234.5)") + if negpos.x ~= -13.3 or negpos.y ~= -4.6 or negpos.z ~= -1234.5 then + error("Test 14 failed") + end + + local checktypes = ChatCmdBuilder.build(function(cmd) + cmd:sub("checktypes :int:int :number:number :pos:pos :word:word :text:text", function(name, int, number, pos, word, text) + return int, number, pos.x, pos.y, pos.z, word, text + end) + end).run + local int, number, posx, posy, posz, word, text + int, number, posx, posy, posz, word, text = checktypes("checker","checktypes -1 -2.4 (-3,-5.3,6.12) some text to finish off with") + --dprint(int, number, posx, posy, posz, word, text) + if int ~= -1 or number ~= -2.4 or posx ~= -3 or posy ~= -5.3 or posz ~= 6.12 or word ~= "some" or text ~= "text to finish off with" then + error("Test 15 failed") + end + dprint("All tests passed") + +end +if not minetest then + run_tests() +end diff --git a/commands.lua b/commands.lua new file mode 100644 index 0000000..441722f --- /dev/null +++ b/commands.lua @@ -0,0 +1,51 @@ +ChatCmdBuilder.new("sumo", function(cmd) + + -- create arena + cmd:sub("create :arena", function(name, arena_name) + arena_lib.create_arena(name, "sumo", arena_name) + end) + + cmd:sub("create :arena :minplayers:int :maxplayers:int", function(name, arena_name, min_players, max_players) + arena_lib.create_arena(name, "sumo", arena_name, min_players, max_players) + end) + + -- remove arena + cmd:sub("remove :arena", function(name, arena_name) + arena_lib.remove_arena(name, "sumo", arena_name) + end) + + -- list of the arenas + cmd:sub("list", function(name) + arena_lib.print_arenas(name, "sumo") + end) + + -- enter editor mode + cmd:sub("edit :arena", function(sender, arena) + arena_lib.enter_editor(sender, "sumo", arena) + end) + + -- enable and disable arenas + cmd:sub("enable :arena", function(name, arena) + arena_lib.enable_arena(name, "sumo", arena) + end) + + cmd:sub("disable :arena", function(name, arena) + arena_lib.disable_arena(name, "sumo", arena) + end) + +end, { + description = [[ + + (/help sumo) + + Use this to configure your arena: + - create [min players] [max players] + - edit + - enable + + Other commands: + - remove + - disable + ]], + privs = { sumo_admin = true } +}) diff --git a/description.txt b/description.txt new file mode 100644 index 0000000..e3f48ce --- /dev/null +++ b/description.txt @@ -0,0 +1 @@ +A minigame for arena_lib where you punch the other players with your stick to knock them off the platform or island. Some advanced uses of the stick include vaulting yourself and throwing otehr players over your shoulder. \ No newline at end of file diff --git a/init.lua b/init.lua new file mode 100644 index 0000000..84a5b54 --- /dev/null +++ b/init.lua @@ -0,0 +1,49 @@ +sumo = {} + +-- local value settings + +local player_speed = 2 -- when in the minigame +local player_jump = 1.3 -- when in the minigame + + + + arena_lib.register_minigame("sumo", { + prefix = "[Sumo] ", + hub_spawn_point = { x = 0, y = 20, z = 0 }, + show_minimap = false, + time_mode = 2, + load_time = 1, + celebration_time = 5, + join_while_in_progress = false, + keep_inventory = false, + in_game_physics = { + speed = player_speed, + jump = player_jump, + sneak = false, + }, + + hotbar = { + slots = 1, + background_image = "sumo_gui_hotbar.png" + }, + + disabled_damage_types = {"punch","fall"}, + + player_properties = { + textures = "", + watching = "", + playing = true, + collisionbox = {}, + }, + }) + + +dofile(minetest.get_modpath("sumo") .. "/commands.lua") +dofile(minetest.get_modpath("sumo") .. "/items.lua") +dofile(minetest.get_modpath("sumo") .. "/minigame_manager.lua") +dofile(minetest.get_modpath("sumo") .. "/nodes.lua") +dofile(minetest.get_modpath("sumo") .. "/privs.lua") + +if not minetest.get_modpath("lib_chatcmdbuilder") then + dofile(minetest.get_modpath("sumo") .. "/chatcmdbuilder.lua") +end diff --git a/items.lua b/items.lua new file mode 100644 index 0000000..78b0a06 --- /dev/null +++ b/items.lua @@ -0,0 +1,92 @@ +local stick_knockback = 30 --multiplier for how hard the stick hits +local stick_vault_reach = 3 -- how close to the pointed node must the player be to vault +local stick_vault_timeout = 1 -- int timer for how long the vault cannot be used after it is used +local allow_swap_distance = 4 -- if an opponent is within this distance, then if the player uses the pushstick with the shift key pressed, the players switch positions. + + +minetest.register_craftitem("sumo:pushstick", { + description = "Push Stick", + inventory_image = "default_stick.png", + stack_max = 1, + wield_scale = {x = 2, y = 2, z = 2}, + on_drop = function() end, + + on_use = function(itemstack, user, pointed_thing) + local sound = 'swish'..math.random(1,4) + minetest.sound_play(sound, { + pos = user:get_pos(), + max_hear_distance = 5, + gain = 10.0, + }) + + if pointed_thing == nil then return end + if pointed_thing.type == 'object' then + if minetest.is_player(pointed_thing.ref) == true then + + local dir = user:get_look_dir() + local keys = user:get_player_control() + local swap = false + local hitted_pos = pointed_thing.ref:get_pos() + local hitter_pos = user:get_pos() + if keys.sneak and vector.distance(hitted_pos,hitter_pos) < allow_swap_distance then + swap = true + user:move_to(hitted_pos, true) + pointed_thing.ref:move_to(hitter_pos, true) + pointed_thing.ref:add_player_velocity(vector.multiply({x = -dir.x, y = dir.y, z= -dir.z}, stick_knockback/2)) + else + pointed_thing.ref:add_player_velocity(vector.multiply(dir, stick_knockback)) + if not swap then + local sound = 'thwack'..math.random(1,3) + minetest.sound_play(sound, { + pos = user:get_pos(), + max_hear_distance = 10, + gain = 10.0, + }) + local sound = 'hurt'..math.random(1,2) + minetest.sound_play(sound, { + to_player = pointed_thing.ref:get_player_name(), + pos = user:get_pos(), + gain = 10.0, + }) + end + end + + end + end + end, + + on_place = function(itemstack, placer, pointed_thing) + if pointed_thing == nil then return end + if pointed_thing.type == 'node' then + + if vector.distance(pointed_thing.under, placer:get_pos()) < stick_vault_reach then + local first_use = false + local imeta = itemstack:get_meta() + local old_time = imeta:get_int('old_time') + local current_time = minetest.get_gametime() + if old_time == 0 or old_time == nil then + first_use = true + end + if first_use or current_time > old_time + stick_vault_timeout then + local lookvect = placer:get_look_dir() + local pushvect = vector.normalize( {x=lookvect.x, z=lookvect.z, y= math.sqrt(1-(lookvect.y*lookvect.y))}) + --gives a unit vector that is 90 deg offset in the vert direction + local force = 10 * vector.length(vector.normalize( {x=lookvect.x, z=lookvect.z, y= 0})) + + placer:add_player_velocity(vector.multiply(pushvect, force)) + --update the staff time for next check + local sound = 'jump'..math.random(1,2) + minetest.sound_play(sound, { + pos = placer:get_pos(), + max_hear_distance = 10, + gain = 10.0, + }) + + imeta:set_int('old_time', current_time) + return itemstack + end + end + end + end, + +}) diff --git a/nodes.lua b/nodes.lua new file mode 100644 index 0000000..d04ed26 --- /dev/null +++ b/nodes.lua @@ -0,0 +1,126 @@ +minetest.register_node("sumo:player_killer_air", { + description = "Sumo Arena Player Killer Air", + drawtype = "airlike", + paramtype = "light", + sunlight_propagates = true, + + walkable = false, + pointable = true, + diggable = true, + buildable_to = false, + drop = "", + damage_per_second = 40, + groups = {oddly_breakable_by_hand = 1}, +}) + + +minetest.register_node("sumo:fullclip", { + description = "Player Blocker (sumo)", + drawtype = "airlike", + paramtype = "light", + sunlight_propagates = true, + + walkable = true, + pointable = true, + diggable = false, + buildable_to = false, + drop = "", + damage_per_second = 40, + groups = {}, +}) + + + +minetest.register_node("sumo:player_killer_water_source", { + description = "Sumo Arena Player Killer Water Source", + drawtype = "liquid", + waving = 3, + tiles = { + { + name = "default_water_source_animated.png", + backface_culling = false, + animation = { + type = "vertical_frames", + aspect_w = 16, + aspect_h = 16, + length = 2.0, + }, + }, + { + name = "default_water_source_animated.png", + backface_culling = true, + animation = { + type = "vertical_frames", + aspect_w = 16, + aspect_h = 16, + length = 2.0, + }, + }, + }, + damage_per_second = 40, + alpha = 191, + paramtype = "light", + walkable = false, + pointable = false, + diggable = false, + buildable_to = true, + is_ground_content = false, + drop = "", + drowning = 1, + liquidtype = "source", + liquid_alternative_flowing = "sumo:player_killer_water_flowing", + liquid_alternative_source = "sumo:player_killer_water_source", + liquid_viscosity = 1, + post_effect_color = {a = 103, r = 30, g = 60, b = 90}, + groups = {water = 3, liquid = 3, cools_lava = 1}, + sounds = default.node_sound_water_defaults(), +}) + + + +minetest.register_node("sumo:player_killer_water_flowing", { + description = "Sumo Arena Player Killer Water Flowing", + drawtype = "flowingliquid", + waving = 3, + tiles = {"default_water.png"}, + special_tiles = { + { + name = "default_water_flowing_animated.png", + backface_culling = false, + animation = { + type = "vertical_frames", + aspect_w = 16, + aspect_h = 16, + length = 0.5, + }, + }, + { + name = "default_water_flowing_animated.png", + backface_culling = true, + animation = { + type = "vertical_frames", + aspect_w = 16, + aspect_h = 16, + length = 0.5, + }, + }, + }, + damage_per_second = 200, + alpha = 191, + paramtype = "light", + walkable = false, + pointable = false, + diggable = false, + buildable_to = true, + is_ground_content = false, + drop = "", + drowning = 1, + liquidtype = "flowing", + liquid_alternative_flowing = "sumo:player_killer_water_flowing", + liquid_alternative_source = "sumo:player_killer_water_source", + liquid_viscosity = 1, + post_effect_color = {a = 103, r = 30, g = 60, b = 90}, + groups = {water = 3, liquid = 3, not_in_creative_inventory = 1, + cools_lava = 1}, + sounds = default.node_sound_water_defaults(), +}) diff --git a/privs.lua b/privs.lua new file mode 100644 index 0000000..8d8d851 --- /dev/null +++ b/privs.lua @@ -0,0 +1,3 @@ +minetest.register_privilege("sumo_admin", { + description = "With this you can use /sumo create, edit" +}) diff --git a/readme.txt b/readme.txt new file mode 100644 index 0000000..04b7282 --- /dev/null +++ b/readme.txt @@ -0,0 +1,31 @@ +Sumo mod by MisterE for use with arena_lib (minigame_lib) You should be familiar with how to set up arena_lib arenas. + +Code snippets from https://gitlab.com/zughy-friends-minetest/arena_lib/-/blob/master/DOCS.md and from Zughy's minigames + + +to set up an arena, create a confined space that floats in the air, with lava or other damage blocks beneath. Set the spawners on the platform. You can use mesecons to make the board more interesting. + +In this game, each player gets a tool that pushes other players... the goal is to knowck all other players off and be the last one standing. Each player gets 3 lives + + +Basic setup: + +1) type /sumo create +2) type /sumo edit +3) use the editor to place a minigame sign, assign it to your minigame. +4) while in the editor, move to where your arena will be. +5) Make your arena. There should be some type of platform, with a lava pool underneath to kill players that fall off. +6) using the editor tools, mark player spawner locations. Protect the arena. +7) exit the editor mode +8) type /minigamesettings sumo +9) change the hub spawnpoint to be next to the signs. + + + + + + + + + + diff --git a/screenshot.png b/screenshot.png new file mode 100644 index 0000000..104672a Binary files /dev/null and b/screenshot.png differ