diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 000000000..2693556b6
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,3 @@
+
+
+- [ ] This PR has been tested locally
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
deleted file mode 100644
index fea960c17..000000000
--- a/.github/workflows/build.yml
+++ /dev/null
@@ -1,16 +0,0 @@
-name: build
-on: [push, pull_request]
-
-jobs:
- luacheck:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v2
- - name: Install Luarocks
- run: |
- sudo apt-get update -qyy
- sudo apt-get install luarocks -qyy
- - name: Install Luacheck
- run: luarocks install --local luacheck
- - name: Run Luacheck
- run: $HOME/.luarocks/bin/luacheck mods
diff --git a/.github/workflows/luacheck.yml b/.github/workflows/luacheck.yml
new file mode 100644
index 000000000..7cebcd9f8
--- /dev/null
+++ b/.github/workflows/luacheck.yml
@@ -0,0 +1,10 @@
+name: luacheck
+on: [push, pull_request]
+jobs:
+ luacheck:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ - name: Luacheck
+ uses: lunarmodules/luacheck@master
diff --git a/.gitignore b/.gitignore
index 02ba1893d..d76a4b932 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,4 +13,6 @@ debug.txt
/mods/ctf/ctf_map/textures/*Back.*
/mods/ctf/ctf_map/textures/*Left.*
/mods/ctf/ctf_map/textures/*Right.*
-/mods/ctf/ctf_map/textures/*_screenshot.png
\ No newline at end of file
+/mods/ctf/ctf_map/textures/*_screenshot.png
+
+*.blend1
diff --git a/README.md b/README.md
index cc0769853..29ac459ff 100644
--- a/README.md
+++ b/README.md
@@ -45,6 +45,7 @@ Note that this version may be slightly behind the git version, but it will be a
* If you use Visual Studio Code we recommend these extensions:
* https://marketplace.visualstudio.com/items?itemName=sumneko.lua
* https://marketplace.visualstudio.com/items?itemName=dwenegar.vscode-luacheck
+ * https://marketplace.visualstudio.com/items?itemName=GreenXenith.minetest-tools
## License
@@ -52,15 +53,14 @@ Created by [rubenwardy](https://rubenwardy.com/).
Developed by [LandarVargan](https://github.com/LoneWolfHT).
Previous Developers: [savilli](https://github.com/savilli).
-Licenses where not specified:
-Code: LGPLv2.1+
-Textures: CC-BY-SA 3.0
-
-### Textures
+Check out [mods/](mods/) to see all the installed mods and their respective licenses.
-* [Header](menu/header.png): CC BY-SA 4.0 by xenonca
-* [Background Image](menu/background.png): CC0 (where applicable) by Apelta (Uses [Minetest Game](https://github.com/minetest/minetest_game) textures, the majority of which are licensed CC-BY-SA 3.0). The player skin used is licensed CC-BY-SA 3.0
+Licenses where not specified:\
+Code: [GNU LGPLv2.1+](https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html)\
+Media: [CC BY-SA 3.0 Unported](https://creativecommons.org/licenses/by-sa/3.0/)
-### Mods
+### Textures
-Check out [mods/](mods/) to see all the installed mods and their respective licenses.
+* [Header](menu/header.png): [CC BY 3.0 Unported](https://creativecommons.org/licenses/by/3.0/) by [SuddenSFD](https://github.com/SuddenSFD)
+* [Background Image](menu/background.png): [CC BY-SA 4.0 International](https://creativecommons.org/licenses/by-sa/4.0/) (where applicable) by [GreenBlob](https://github.com/a-blob) (Uses [Minetest Game](https://github.com/minetest/minetest_game) textures, the majority of which are licensed [CC BY-SA 3.0 Unported](https://creativecommons.org/licenses/by-sa/3.0/)). The player skins used are licensed under [CC BY-SA 3.0 Unported](https://creativecommons.org/licenses/by-sa/3.0/)
+* [Icon](menu/icon.png): [CC BY 3.0](https://creativecommons.org/licenses/by/3.0/) by [SuddenSFD](https://github.com/SuddenSFD)
diff --git a/docs/accurate_statbar.patch b/docs/accurate_statbar.patch
index 8acf78b17..657f73e41 100644
--- a/docs/accurate_statbar.patch
+++ b/docs/accurate_statbar.patch
@@ -6,13 +6,16 @@ index 46c947b6..f4372843 100644
local hud_ids = {}
local function scaleToDefault(player, field)
-+ return player["get_" .. field](player)
-- -- Scale "hp" or "breath" to the default dimensions
-- local current = player["get_" .. field](player)
-- local nominal = core["PLAYER_MAX_".. field:upper() .. "_DEFAULT"]
-- local max_display = math.max(nominal,
-- math.max(player:get_properties()[field .. "_max"], current))
-- return current / max_display * nominal
++ if field == "health" then
++ return player["get_" .. field](player)
++ else
+ -- Scale "hp" or "breath" to the default dimensions
+ local current = player["get_" .. field](player)
+ local nominal = core["PLAYER_MAX_".. field:upper() .. "_DEFAULT"]
+ local max_display = math.max(nominal,
+ math.max(player:get_properties()[field .. "_max"], current))
+ return current / max_display * nominal
++ end
end
local function update_builtin_statbars(player)
diff --git a/docs/ctf-api.md b/docs/ctf-api.md
index 0dc1ed83a..93a8f67d2 100644
--- a/docs/ctf-api.md
+++ b/docs/ctf-api.md
@@ -24,7 +24,7 @@ ctf_settings.register("my_setting", {
on_change = function(player, new_value)
<...>
end
-}
+})
```
### `ctf_settings.set(player, setting, value)`
@@ -39,7 +39,10 @@ ctf_settings.register("my_setting", {
---
# mods/ctf/
-TODO
+TODO, below is a collection of quick notes for later
+
+## ctf_teams
+* https://modern.ircdocs.horse/formatting.html#colors-16-98
---
# mods/mtg/
diff --git a/menu/background.png b/menu/background.png
index 708951b81..c31bc4266 100644
Binary files a/menu/background.png and b/menu/background.png differ
diff --git a/menu/header.png b/menu/header.png
index e7084c066..69635f542 100644
Binary files a/menu/header.png and b/menu/header.png differ
diff --git a/menu/icon.png b/menu/icon.png
index 16f0ce7b8..f741eef5e 100644
Binary files a/menu/icon.png and b/menu/icon.png differ
diff --git a/mods/apis/ctf_gui/dev.lua b/mods/apis/ctf_gui/dev.lua
index 68f105fa5..954058a9c 100644
--- a/mods/apis/ctf_gui/dev.lua
+++ b/mods/apis/ctf_gui/dev.lua
@@ -1,31 +1,48 @@
+local unset_function = "return "
+
function ctf_gui.show_formspec_dev(player, formname, formspec, formcontext)
local filepath = minetest.get_worldpath().."/ctf_gui/"
local filename = filepath.."file_edit.txt"
+ local slower_loop = false
+
+ minetest.chat_send_all("Started formspec editing file at "..filename)
minetest.mkdir(filepath)
local file = assert(io.open(filename, "w"))
-
- file:write(formspec)
-
+ if type(formspec) ~= "function" then
+ file:write(formspec)
+ else
+ file:write(unset_function)
+ end
file:close()
local function interval()
- if formspec:sub(1, 3) == "[f]" then
- local result, form = pcall(loadstring(formspec:sub(4)), formcontext)
- ctf_gui.show_formspec(player, formname, result and form or "")
+ if type(formspec) == "function" then
+ ctf_gui.show_formspec(player, formname, formspec(formcontext))
+ elseif formspec:match("^%s*return") then
+ local result, form = pcall((loadstring(formspec) or function() return function() end end)(), formcontext)
+
+ ctf_gui.show_formspec(player, formname,
+ result and form or "size[10,10]hypertext[0,0;10,10;err;"..minetest.formspec_escape(form or "").."]"
+ )
+
+ slower_loop = not result
else
ctf_gui.show_formspec(player, formname, formspec)
end
- minetest.after(1, function()
+ minetest.after(slower_loop and 3 or 1, function()
local f = assert(io.open(filename, "r"))
+ local new_form = f:read("*a")
- formspec = f:read("*a")
+ if new_form ~= unset_function then
+ formspec = new_form
+ end
f:close()
- if formspec:match("^exit") then
+ if type(formspec) == "function" or not formspec:match("^exit") then
interval()
else
minetest.request_shutdown("Formspec dev requested shutdown", true)
diff --git a/mods/apis/ctf_gui/init.lua b/mods/apis/ctf_gui/init.lua
index 9ca06e89d..2fd23bad6 100644
--- a/mods/apis/ctf_gui/init.lua
+++ b/mods/apis/ctf_gui/init.lua
@@ -39,6 +39,7 @@ function ctf_gui.init()
local action = ctx._on_formspec_input(pname, ctx, fields, ...)
if action == "refresh" then
+ minetest.log("action", "Refreshing formspec "..dump(ctx._formname).." to "..dump(pname))
minetest.show_formspec(pname, ctx._formname, ctx._formspec(ctx))
end
end
@@ -115,6 +116,7 @@ function ctf_gui.show_formspec(player, formname, formspec, formcontext)
context[player]._formname = formname
context[player]._formspec = formspec
+ minetest.log("action", "Showing new_formspec "..dump(formname).." to "..dump(player))
if type(formspec) == "function" then
minetest.show_formspec(player, formname, formspec(formcontext))
else
@@ -144,6 +146,7 @@ do
end
end
+ -- minetest.log("action", "[ctf_gui] unpacking: "..dump(l))
return format(base, unpck(l))
end
@@ -457,9 +460,16 @@ function ctf_gui.old_show_formspec(player, formname, formdef)
formdef._info = formdef
context[player] = formdef
- minetest.show_formspec(player, formname, formspec)
+ if minetest.get_player_by_name(player) then
+ minetest.log("action", "Showing formspec "..dump(formname).." to "..dump(player))
+ minetest.show_formspec(player, formname, formspec)
+ end
end,
formdef, ctf_gui.ELEM_SIZE, ctf_gui.SCROLLBAR_WIDTH)
end
+minetest.register_on_leaveplayer(function(player)
+ context[player:get_player_name()] = nil
+end)
+
dofile(minetest.get_modpath("ctf_gui").."/dev.lua")
diff --git a/mods/apis/ctf_settings/init.lua b/mods/apis/ctf_settings/init.lua
index 2a00f9c3a..b3f5a9178 100644
--- a/mods/apis/ctf_settings/init.lua
+++ b/mods/apis/ctf_settings/init.lua
@@ -3,7 +3,7 @@ ctf_settings = {
settings_list = {},
}
-local FORMSIZE = {x = 8, y = 5}
+local FORMSIZE = {x = 8, y = 9.4}
local SCROLLBAR_W = 0.4
minetest.after(0, function()
@@ -29,16 +29,35 @@ function ctf_settings.register(name, def)
table.insert(ctf_settings.settings_list, name)
end
+---@param player ObjectRef
function ctf_settings.set(player, setting, value)
player:get_meta():set_string("ctf_settings:"..setting, value)
end
+---@param player ObjectRef
---@return string Returns the player's chosen setting value, the default given at registration, or if both are unset: ""
function ctf_settings.get(player, setting)
local value = player:get_meta():get_string("ctf_settings:"..setting)
local info = ctf_settings.settings[setting]
- return value == "" and info.default or value
+ return value == "" and (info and info.default) or value
+end
+
+-- This Function MIT by Rubenwardy
+--- Creates a scrollbaroptions for a scroll_container
+--
+-- @param visible_l the length of the scroll_container and scrollbar
+-- @param total_l length of the scrollable area
+-- @param scroll_factor as passed to scroll_container
+local function make_scrollbaroptions_for_scroll_container(visible_l, total_l, scroll_factor)
+
+ assert(total_l >= visible_l)
+
+ local thumb_size = (visible_l / total_l) * (total_l - visible_l)
+
+ local max = total_l - visible_l
+
+ return ("scrollbaroptions[min=0;max=%f;thumbsize=%f]"):format(max / scroll_factor, thumb_size / scroll_factor)
end
minetest.register_on_mods_loaded(function()
@@ -48,57 +67,97 @@ minetest.register_on_mods_loaded(function()
local setting_list = {}
local lastypos = -0.5
+ if not context then
+ context = {}
+ end
+
+ if not context.setting then
+ context.setting = {}
+ end
+
for k, setting in ipairs(ctf_settings.settings_list) do
local settingdef = ctf_settings.settings[setting]
+ if not context.setting[setting] then
+ context.setting[setting] = ctf_settings.get(player, setting)
+ end
+
if settingdef.type == "bool" then
setting_list[k] = {
"checkbox[0,%f;%s;%s;%s]tooltip[%s;%s]",
lastypos,
setting,
settingdef.label or setting,
- ctf_settings.get(player, setting),
+ context.setting[setting],
setting,
settingdef.description or HumanReadable(setting)
}
lastypos = lastypos + 0.5
elseif settingdef.type == "list" then
+ local max_len = 0
+
+ for _, val in pairs(settingdef.list) do
+ max_len = math.max(val:len(), max_len)
+ end
+
lastypos = lastypos + 0.3
setting_list[k] = {
"dropdown[0,%f;%f;%s;%s;%d]tooltip[0,%f;%f,0.6;%s]",
lastypos,
- FORMSIZE.x/1.7,
+ math.max(FORMSIZE.x / 2, math.min(FORMSIZE.x - SCROLLBAR_W + 2, 0.5 + (max_len * 0.23))),
setting,
settingdef.list,
- ctf_settings.get(player, setting),
+ context.setting[setting],
--label
lastypos,
(FORMSIZE.x/1.7) - 0.3,
settingdef.description or HumanReadable(setting),
}
lastypos = lastypos + 0.6
+ elseif settingdef.type == "bar" then
+ lastypos = lastypos + 0.7
+ setting_list[k] = {
+ "label[0,%f;%s: %d%%]"..
+ "scrollbaroptions[min=%d;max=%d;smallstep=%d]"..
+ "scrollbar[0,%f;%f,0.4;horizontal;%s;%s]",
+ lastypos - 0.5,
+ settingdef.label or HumanReadable(setting),
+ (
+ tonumber(context.setting[setting])
+ /
+ ((settingdef.max - settingdef.min) - tonumber(settingdef.default))
+ ) * 100,
+ settingdef.min or 0,
+ settingdef.max or 10,
+ settingdef.step or 1,
+ lastypos,
+ FORMSIZE.x - SCROLLBAR_W -2,
+ setting,
+ context.setting[setting],
+ }
+ lastypos = lastypos + 0.5
end
end
local form = {
- {"box[-0.1,-0.1;%f,%f;#00000055]", FORMSIZE.x - SCROLLBAR_W, FORMSIZE.y},
+ {"box[-0.1,-0.1;%f,%f;#00000055]", FORMSIZE.x - SCROLLBAR_W, FORMSIZE.y},
{"scroll_container[-0.1,0.3;%f,%f;settings_scrollbar;vertical;0.1]",
- FORMSIZE.x - SCROLLBAR_W,
+ FORMSIZE.x - SCROLLBAR_W + 2,
FORMSIZE.y + 0.7
},
ctf_gui.list_to_formspec_str(setting_list),
"scroll_container_end[]",
- {"scrollbaroptions[max=%d]", math.ceil((lastypos - 3.833) * 11.538)},
+ make_scrollbaroptions_for_scroll_container(FORMSIZE.y + 0.7, math.max(lastypos+1, FORMSIZE.y + 0.7), 0.1),
{"scrollbar[%f,-0.1;%f,%f;vertical;settings_scrollbar;%f]",
FORMSIZE.x - SCROLLBAR_W,
SCROLLBAR_W,
FORMSIZE.y,
- context and context.settings_scrollbar or 0
+ context.settings_scrollbar or 0
},
}
- return sfinv.make_formspec(player, context, ctf_gui.list_to_formspec_str(form), true)
+ return sfinv.make_formspec(player, context, ctf_gui.list_to_formspec_str(form), false)
end,
on_player_receive_fields = function(self, player, context, fields)
local refresh = false
@@ -110,24 +169,43 @@ minetest.register_on_mods_loaded(function()
if setting.type == "bool" then
local newvalue = value == "true" and "true" or "false"
- ctf_settings.set(player, field, newvalue)
+ if context.setting[field] ~= newvalue then
+ context.setting[field] = newvalue
+ ctf_settings.set(player, field, newvalue)
+
+ if setting.on_change then
+ setting.on_change(player, newvalue)
+ end
- if setting.on_change then
- setting.on_change(player, newvalue)
+ refresh = true
end
elseif setting.type == "list" then
local idx = table.indexof(setting.list, value)
- if idx ~= -1 then
+ if idx ~= -1 and context.setting[field] ~= tostring(idx) then
+ context.setting[field] = tostring(idx)
ctf_settings.set(player, field, tostring(idx))
if setting.on_change then
- setting.on_change(player, idx)
+ setting.on_change(player, tostring(idx))
end
+
+ refresh = true
end
- end
+ elseif setting.type == "bar" then
+ local scrollevent = minetest.explode_scrollbar_event(value)
+
+ if scrollevent.value and context.setting[field] ~= tostring(scrollevent.value) then
+ context.setting[field] = tostring(scrollevent.value)
+ ctf_settings.set(player, field, tostring(scrollevent.value))
+
+ if setting.on_change then
+ setting.on_change(player, tostring(scrollevent.value))
+ end
- refresh = true
+ refresh = true
+ end
+ end
end
end
diff --git a/mods/apis/hud_events/init.lua b/mods/apis/hud_events/init.lua
index 1a23a1e8f..66c9e46b0 100644
--- a/mods/apis/hud_events/init.lua
+++ b/mods/apis/hud_events/init.lua
@@ -4,7 +4,7 @@ local hud = mhud.init()
local HUD_SHOW_TIME = 3
local HUD_SHOW_QUICK_TIME = 2
-local HUD_SHOW_NEXT_TIME = 1
+local HUD_SHOW_NEXT_TIME = 0.6
local HUD_COLORS = {
primary = 0x0D6EFD,
@@ -17,15 +17,21 @@ local HUD_COLORS = {
dark = 0x212529,
}
-local hud_queues = {}
+local hud_queues = {
+ -- {}, channel 1
+ -- {}, channel 2
+ -- ...
+}
local quick_event_timer = {}
minetest.register_on_leaveplayer(function(player)
local pname = player:get_player_name()
- if hud_queues[pname] then
- hud_queues[pname].t:cancel()
- hud_queues[pname] = nil
+ for channel=1, #hud_queues do
+ if hud_queues[channel][pname] then
+ hud_queues[channel][pname].t:cancel()
+ hud_queues[channel][pname] = nil
+ end
end
if quick_event_timer[pname] then
@@ -41,7 +47,7 @@ local function show_quick_hud_event(player, huddef)
hud:add(player, "hud_event_quick", {
hud_elem_type = "text",
position = {x = 0.5, y = 0.5},
- offset = {x = 0, y = 45},
+ offset = {x = 0, y = 20},
alignment = {x = "center", y = "down"},
text = huddef.text,
color = huddef.color,
@@ -51,47 +57,58 @@ local function show_quick_hud_event(player, huddef)
end
if quick_event_timer[pname] then
- quick_event_timer[pname].cancel()
+ quick_event_timer[pname]:cancel()
end
quick_event_timer[pname] = minetest.after(HUD_SHOW_QUICK_TIME, function()
if not player:is_player() then return end
- hud:remove(player, "hud_event_quick")
+ if hud:exists(player, "hud_event_quick") then
+ hud:remove(player, "hud_event_quick")
+ end
end)
end
-local function handle_hud_events(player)
+local function handle_hud_events(player, channel)
local pname = player:get_player_name()
- local huddef = table.remove(hud_queues[pname].e, 1)
+ local huddef = table.remove(hud_queues[channel][pname].e, 1)
+ local event_name = "hud_event_"..tostring(channel)
- if not hud:exists(player, "hud_event") then
- hud:add(player, "hud_event", {
+ if not hud:exists(player, event_name) then
+ hud:add(player, event_name, {
hud_elem_type = "text",
position = {x = 0.5, y = 0.5},
- offset = {x = 0, y = 20},
+ offset = {x = 0, y = 45 + (channel - 1) * 25},
alignment = {x = "center", y = "down"},
text = huddef.text,
color = huddef.color,
})
else
- hud:change(player, "hud_event", {
+ hud:change(player, event_name, {
text = huddef.text,
color = huddef.color
})
end
- hud_queues[pname].t = minetest.after(HUD_SHOW_TIME, function()
- hud:change(player, "hud_event", {text = ""})
-
- hud_queues[pname].t = minetest.after(HUD_SHOW_NEXT_TIME, function()
- if #hud_queues[pname].e >= 1 then
- handle_hud_events(player)
- else
- hud:remove(player, "hud_event")
- hud_queues[pname] = nil
- end
- end)
+ hud_queues[channel][pname].t = minetest.after(HUD_SHOW_TIME, function()
+ player = minetest.get_player_by_name(pname)
+
+ if player then
+ hud:change(player, event_name, {text = ""})
+
+ hud_queues[channel][pname].t = minetest.after(HUD_SHOW_NEXT_TIME, function()
+ player = minetest.get_player_by_name(pname)
+
+ if player then
+ if #hud_queues[channel][pname].e >= 1 then
+ handle_hud_events(player, channel)
+ else
+ hud:remove(player, event_name)
+ hud_queues[channel][pname] = nil
+ end
+ end
+ end)
+ end
end)
end
@@ -100,6 +117,7 @@ end
text = "This is a hud event",
color = "info",
quick = true,
+ channel = an integer or nil
})
]]
function hud_events.new(player, def)
@@ -110,6 +128,8 @@ function hud_events.new(player, def)
def = {text = def}
end
+ def.channel = def.channel or 1
+
if def.color then
if type(def.color) == "string" then
def.color = HUD_COLORS[def.color]
@@ -121,13 +141,17 @@ function hud_events.new(player, def)
if not def.quick then
local pname = player:get_player_name()
- if not hud_queues[pname] then
- hud_queues[pname] = {e = {}}
+ while not hud_queues[def.channel] do
+ table.insert(hud_queues, {})
+ end
+
+ if not hud_queues[def.channel][pname] then
+ hud_queues[def.channel][pname] = {e = {}}
end
- table.insert(hud_queues[pname].e, {text = def.text, color = def.color})
+ table.insert(hud_queues[def.channel][pname].e, {text = def.text, color = def.color, channel = def.channel})
- if not hud_queues[pname].t then
- handle_hud_events(player)
+ if not hud_queues[def.channel][pname].t then
+ handle_hud_events(player, def.channel)
end
else
show_quick_hud_event(player, def)
diff --git a/mods/apis/mhud b/mods/apis/mhud
index 1f529c357..056dbfbe7 160000
--- a/mods/apis/mhud
+++ b/mods/apis/mhud
@@ -1 +1 @@
-Subproject commit 1f529c357da5e5e7fdf006a9433becf1ac719e77
+Subproject commit 056dbfbe71e232927410c1b4d5d3cdfbdc8a74a3
diff --git a/mods/apis/physics/init.lua b/mods/apis/physics/init.lua
index 5aa53dae3..80c856275 100644
--- a/mods/apis/physics/init.lua
+++ b/mods/apis/physics/init.lua
@@ -2,9 +2,10 @@ physics = {}
local players = {}
local default_overrides = {
- speed = 1,
- jump = 1,
- gravity = 1
+ speed = false, -- default set in update()
+ speed_crouch = 1.0,
+ jump = false, -- default set in update()
+ gravity = 1.0,
}
minetest.register_on_joinplayer(function(player)
@@ -21,10 +22,18 @@ local function update(name)
for _, layer in pairs(players[name]) do
for attr, val in pairs(layer) do
- override[attr] = override[attr] * val
+ override[attr] = (override[attr] or 1) * val
end
end
+ if (override.jump or -1) < 0 then
+ override.jump = 1.1
+ end
+
+ if (override.speed or -1) < 0 then
+ override.speed = 1.1
+ end
+
player:set_physics_override(override)
end
@@ -37,7 +46,7 @@ function physics.set(name, layer, modifiers)
for attr, val in pairs(modifiers) do
-- Throw error if an unsupported attribute is encountered
- assert(default_overrides[attr], "physics: Unsupported attribute!")
+ assert(default_overrides[attr] ~= nil, "physics: Unsupported attribute!")
-- Remove an attribute if its value is 1
if val == 1 then
diff --git a/mods/ctf/ctf_chat/init.lua b/mods/ctf/ctf_chat/init.lua
index f30082f7c..4e475bffd 100644
--- a/mods/ctf/ctf_chat/init.lua
+++ b/mods/ctf/ctf_chat/init.lua
@@ -80,7 +80,7 @@ minetest.register_chatcommand("t", {
local tcolor = ctf_teams.team[tname].color
for username in pairs(ctf_teams.online_players[tname].players) do
minetest.chat_send_player(username,
- minetest.colorize(tcolor, "<" .. name .. "> ** " .. param .. " **"))
+ minetest.colorize(tcolor, "[TEAM] <" .. name .. "> " .. param ))
end
else
minetest.chat_send_player(name,
diff --git a/mods/ctf/ctf_combat/ctf_combat_mode/init.lua b/mods/ctf/ctf_combat/ctf_combat_mode/init.lua
index fb5a58b78..9c9839cb6 100644
--- a/mods/ctf/ctf_combat/ctf_combat_mode/init.lua
+++ b/mods/ctf/ctf_combat/ctf_combat_mode/init.lua
@@ -13,8 +13,8 @@ local function update(player)
return
end
- local hud_message = "You are in combat [%ds left]"
- hud_message = hud_message:format(combat.time)
+ local hud_message = "You are in combat [%ds left] \n%s"
+ hud_message = hud_message:format(combat.time, combat.suffocation_message)
if hud:exists(player, "combat_indicator") then
hud:change(player, "combat_indicator", {
@@ -36,8 +36,10 @@ local function update(player)
if node.groups.real_suffocation then -- From real_suffocation mod
combat.time = combat.time + 0.5
+ combat.suffocation_message = "You are inside blocks. Move out to stop your combat timer from increasing."
else
combat.time = combat.time - 1
+ combat.suffocation_message = ""
end
combat.timer = minetest.after(1, update, player)
@@ -56,6 +58,7 @@ function ctf_combat_mode.add_hitter(player, hitter, weapon_image, time)
combat.time = time
combat.last_hitter = hitter
combat.weapon_image = weapon_image
+ combat.suffocation_message = ""
if not combat.timer then
update(player)
diff --git a/mods/ctf/ctf_combat/ctf_healing/bandage.lua b/mods/ctf/ctf_combat/ctf_healing/bandage.lua
index 0a8abf942..dfb0348d8 100644
--- a/mods/ctf/ctf_combat/ctf_healing/bandage.lua
+++ b/mods/ctf/ctf_combat/ctf_healing/bandage.lua
@@ -9,70 +9,86 @@ function ctf_healing.register_bandage(name, def)
inventory_overlay = def.inventory_overlay,
wield_image = def.wield_image,
on_use = function(itemstack, player, pointed_thing)
- if pointed_thing.type ~= "object" then return end
-
- local object = pointed_thing.ref
- if not object:is_player() then return end
-
- local pname = object:get_player_name()
- local uname = player:get_player_name()
-
- if pname == uname then return end
-
- if ctf_teams.get(pname) ~= ctf_teams.get(uname) then
- hud_events.new(uname, {
- quick = true,
- text = pname .. " isn't in your team!",
- color = "warning",
- })
- return
- end
-
- local hp = object:get_hp()
- local limit = def.heal_percent * object:get_properties().hp_max
-
- if hp <= 0 then
- hud_events.new(uname, {
- quick = true,
- text = pname .. " is dead!",
- color = "warning",
- })
- return
- end
-
- if hp >= limit then
- hud_events.new(uname, {
- quick = true,
- text = pname .. " already has " .. limit .. " HP!",
- color = "warning",
- })
- return
- end
-
- local hp_add = math.random(def.heal_min or 3, def.heal_max or 4)
-
- if hp + hp_add > limit then
- hp_add = limit - hp
- hp = limit
- else
- hp = hp + hp_add
- end
-
- local result = RunCallbacks(ctf_healing.registered_on_heal, player, object, hp_add)
-
- if not result then
- object:set_hp(hp)
- hud_events.new(pname, {
- quick = true,
- text = uname .. " healed you!",
- color = 0xC1FF44,
- })
- elseif type(result) == "string" then
- hud_events.new(uname, {
- quick = true,
- text = result,
- color = "warning",
- })
+ if pointed_thing.type == "object" then
+ local object = pointed_thing.ref
+ if not object:is_player() then return end
+
+ local pname = object:get_player_name()
+ local uname = player:get_player_name()
+
+ if pname == uname then return end
+
+ if ctf_teams.get(pname) ~= ctf_teams.get(uname) then
+ hud_events.new(uname, {
+ quick = true,
+ text = pname .. " isn't in your team!",
+ color = "warning",
+ })
+ return
+ end
+
+ local hp = object:get_hp()
+ local limit = def.heal_percent * object:get_properties().hp_max
+
+ if hp <= 0 then
+ hud_events.new(uname, {
+ quick = true,
+ text = pname .. " is dead!",
+ color = "warning",
+ })
+ return
+ end
+
+ if hp >= limit then
+ hud_events.new(uname, {
+ quick = true,
+ text = pname .. " already has " .. limit .. " HP!",
+ color = "warning",
+ })
+ return
+ end
+
+ local hp_add = math.random(def.heal_min or 3, def.heal_max or 4)
+
+ if hp + hp_add > limit then
+ hp_add = limit - hp
+ hp = limit
+ else
+ hp = hp + hp_add
+ end
+
+ local result = RunCallbacks(ctf_healing.registered_on_heal, player, object, hp_add)
+
+ if not result then
+ object:set_hp(hp)
+ hud_events.new(pname, {
+ quick = true,
+ text = uname .. " healed you!",
+ color = 0xC1FF44,
+ })
+ hud_events.new(uname, {
+ quick = true,
+ text = "You healed "..pname.."!",
+ color = 0xC1FF44,
+ })
+ elseif type(result) == "string" then
+ hud_events.new(uname, {
+ quick = true,
+ text = result,
+ color = "warning",
+ })
+ end
+ elseif pointed_thing.type == "node" then
+ local node_pointed = minetest.get_node(pointed_thing.under)
+ local node_above = minetest.get_node(pointed_thing.under:offset(0, 1, 0))
+ if node_pointed.name ~= "ctf_modebase:flag_captured_top" then
+ if node_pointed.name:find("ctf_modebase:flag_") then
+ ctf_modebase.flag_on_punch(player, pointed_thing.under, node_pointed)
+ elseif node_above.name:find("ctf_modebase:flag_") and
+ node_above.name ~= "ctf_modebase:flag_captured_top" then
+ ctf_modebase.flag_on_punch(player, pointed_thing.under:offset(0, 1, 0), node_above)
+ end
+ end
end
end,
}
diff --git a/mods/ctf/ctf_combat/ctf_healing/medkit.lua b/mods/ctf/ctf_combat/ctf_healing/medkit.lua
index fde4903f1..6344d6ed2 100644
--- a/mods/ctf/ctf_combat/ctf_healing/medkit.lua
+++ b/mods/ctf/ctf_combat/ctf_healing/medkit.lua
@@ -36,6 +36,23 @@ local function stop_medkit_heal(playername, interrupt_reason)
healing_players[playername] = nil
end
+local prevent_heals_list = {
+ "normal",
+ "glasslike", "glasslike_framed", "glasslike_framed_optional",
+ "allfaces", "allfaces_optional",
+}
+local function prevent_heals(nodedef)
+ if nodedef.walkable then
+ for _, v in pairs(prevent_heals_list) do
+ if nodedef.drawtype == v then
+ return true
+ end
+ end
+ end
+
+ return false
+end
+
local function medkit_heal(playername)
healing_players[playername].after = minetest.after(1, function()
local player = minetest.get_player_by_name(playername)
@@ -51,11 +68,10 @@ local function medkit_heal(playername)
-- In case teammates manage to place blocks inside the player while they're healing
local pos = player:get_pos():offset(0, 0.1, 0)
- local node_0 = minetest.get_node(pos).name
- local node_1 = minetest.get_node(pos:offset(0, 1, 0)).name
+ local node_0 = minetest.registered_nodes[minetest.get_node(pos).name]
+ local node_1 = minetest.registered_nodes[minetest.get_node(pos:offset(0, 1, 0)).name]
- if (not node_0:match("slab") and node_0 ~= "default:snow" and minetest.registered_nodes[node_0].walkable) or
- minetest.registered_nodes[node_1].walkable then
+ if prevent_heals(node_0) or prevent_heals(node_1) then
return stop_medkit_heal(playername, "You can't heal while inside blocks")
end
@@ -97,11 +113,10 @@ local function start_medkit_heal(playername)
-- Prevent players from using medkits while inside nodes
local pos = player:get_pos():offset(0, 0.1, 0)
- local node_0 = minetest.get_node(pos).name
- local node_1 = minetest.get_node(pos:offset(0, 1, 0)).name
+ local node_0 = minetest.registered_nodes[minetest.get_node(pos).name]
+ local node_1 = minetest.registered_nodes[minetest.get_node(pos:offset(0, 1, 0)).name]
- if (not node_0:match("slab") and node_0 ~= "default:snow" and minetest.registered_nodes[node_0].walkable) or
- minetest.registered_nodes[node_1].walkable then
+ if prevent_heals(node_0) or prevent_heals(node_1) then
return hud_events.new(playername, {
text = "You can't heal inside blocks",
color = "danger",
diff --git a/mods/ctf/ctf_combat/ctf_kill_list/init.lua b/mods/ctf/ctf_combat/ctf_kill_list/init.lua
index 18a07dfde..38fedd89c 100644
--- a/mods/ctf/ctf_combat/ctf_kill_list/init.lua
+++ b/mods/ctf/ctf_combat/ctf_kill_list/init.lua
@@ -34,8 +34,17 @@ local HUD_DEFINITIONS = {
local kill_list = {}
+function ctf_kill_list.show_to_player(player)
+ return true
+end
+
local image_scale_map = ctf_settings.settings["ctf_kill_list:tp_size"].image_scale_map
local function update_hud_line(player, idx, new)
+ if not ctf_kill_list.show_to_player(player) then
+ hud:clear(player)
+ return
+ end
+
idx = HUD_LINES - (idx-1)
local image_scale = tonumber(ctf_settings.get(player, "ctf_kill_list:tp_size"))
diff --git a/mods/ctf/ctf_combat/ctf_melee/mod.conf b/mods/ctf/ctf_combat/ctf_melee/mod.conf
index 0318586ef..f1c2d2c8f 100644
--- a/mods/ctf/ctf_combat/ctf_melee/mod.conf
+++ b/mods/ctf/ctf_combat/ctf_melee/mod.conf
@@ -1,2 +1,2 @@
name = ctf_melee
-depends = ctf_core, ctf_player
+depends = ctf_core, ctf_player, default
diff --git a/mods/ctf/ctf_combat/ctf_ranged/init.lua b/mods/ctf/ctf_combat/ctf_ranged/init.lua
index 54aba36e8..6f117c25b 100644
--- a/mods/ctf/ctf_combat/ctf_ranged/init.lua
+++ b/mods/ctf/ctf_combat/ctf_ranged/init.lua
@@ -7,7 +7,6 @@ ctf_ranged = {
local scoped = ctf_ranged.scoped
local scale_const = 6
-local timer = 1
minetest.register_craftitem("ctf_ranged:ammo", {
description = "Ammo\nUsed to reload guns",
@@ -48,6 +47,8 @@ local function process_ray(ray, user, look_dir, def)
collisiondetection = false,
texture = "ctf_ranged_bullethole.png",
})
+
+ minetest.sound_play("ctf_ranged_ricochet", {pos = hitpoint.intersection_point})
elseif nodedef.groups.liquid then
minetest.add_particlespawner({
amount = 10,
@@ -76,8 +77,8 @@ local function process_ray(ray, user, look_dir, def)
end
end
elseif hitpoint.type == "object" then
- hitpoint.ref:punch(user, 1, {
- full_punch_interval = 1,
+ hitpoint.ref:punch(user, def.fire_interval or 0.1, {
+ full_punch_interval = def.fire_interval or 0.1,
damage_groups = {ranged = 1, [def.type] = 1, fleshy = def.damage}
}, look_dir)
end
@@ -89,6 +90,36 @@ function ctf_ranged.can_use_gun(player, name)
return true
end
+--- Play ephemeral sound on the spot of a player.
+-- @param user ObjectRef: The player object.
+-- @param sound_name str: The name of the sound to be played.
+-- @param spec? table: The SimpleSoundSpec of the sound. Some fields are overriden.
+local function play_player_positional_sound(user, sound_name, spec)
+ -- This function handles positional sounds that are
+ -- supposed to be heared equally on both left and right channel
+ -- by the user, while being heared at the position of the player
+ -- by other players.
+ -- Such a mechanism is mainly used on gunshot sounds,
+ -- so the ephemeral flag is set.
+
+ -- The spec table is copied as a base for the SimpleSoundSpec.
+ -- If not supplied, one is created without any customizations.
+
+ local user_name = user:get_player_name()
+
+ -- Two copies of SimpleSoundSpec
+
+ local non_user_spec = spec and table.copy(spec) or {}
+ non_user_spec.pos = user:get_pos()
+ non_user_spec.exclude_player = user_name
+
+ local user_spec = spec and table.copy(spec) or {}
+ user_spec.to_player = user_name
+
+ minetest.sound_play(sound_name, non_user_spec, true)
+ minetest.sound_play(sound_name, user_spec, true)
+end
+
function ctf_ranged.simple_register_gun(name, def)
minetest.register_tool(rawf.also_register_loaded_tool(name, {
description = def.description,
@@ -99,18 +130,21 @@ function ctf_ranged.simple_register_gun(name, def)
groups = {ranged = 1, [def.type] = 1, tier = def.tier or 1, not_in_creative_inventory = 1},
on_use = function(itemstack, user)
if not ctf_ranged.can_use_gun(user, name) then
- minetest.sound_play("ctf_ranged_click", {pos = user:get_pos()}, true)
+ play_player_positional_sound(user, "ctf_ranged_click")
return
end
local result = rawf.load_weapon(itemstack, user:get_inventory())
+ local sound_name
if result:get_name() == itemstack:get_name() then
- minetest.sound_play("ctf_ranged_click", {pos = user:get_pos()}, true)
+ sound_name = "ctf_ranged_click"
else
- minetest.sound_play("ctf_ranged_reload", {pos = user:get_pos()}, true)
+ sound_name = "ctf_ranged_reload"
end
+ play_player_positional_sound(user, sound_name)
+
return result
end,
},
@@ -123,7 +157,7 @@ function ctf_ranged.simple_register_gun(name, def)
loaded_def.on_secondary_use = def.on_secondary_use
loaded_def.on_use = function(itemstack, user)
if not ctf_ranged.can_use_gun(user, name) then
- minetest.sound_play("ctf_ranged_click", {pos = user:get_pos()}, true)
+ play_player_positional_sound(user, "ctf_ranged_click")
return
end
@@ -158,7 +192,7 @@ function ctf_ranged.simple_register_gun(name, def)
rays = rawf.spread_bulletcast(def.bullet, spawnpos, endpos, true, true)
end
- minetest.sound_play(def.fire_sound, {pos = user:get_pos()}, true)
+ play_player_positional_sound(user, def.fire_sound)
for _, ray in pairs(rays) do
process_ray(ray, user, look_dir, def)
@@ -294,7 +328,7 @@ ctf_ranged.simple_register_gun("ctf_ranged:sniper", {
type = "sniper",
description = "Sniper rifle\nDmg: 12 | FR: 2s | Mag: 25",
texture = "ctf_ranged_sniper_rifle.png",
- fire_sound = "ctf_ranged_sniper_shot",
+ fire_sound = "ctf_ranged_sniper",
rounds = 25,
range = 300,
damage = 12,
@@ -304,7 +338,7 @@ ctf_ranged.simple_register_gun("ctf_ranged:sniper", {
if scoped[user:get_player_name()] then
ctf_ranged.hide_scope(user:get_player_name())
else
- local item_name = itemstack:get_name():gsub("_loaded", "")
+ local item_name = itemstack:get_name()
ctf_ranged.show_scope(user:get_player_name(), item_name, 4)
end
end
@@ -314,7 +348,7 @@ ctf_ranged.simple_register_gun("ctf_ranged:sniper_magnum", {
type = "sniper",
description = "Magnum sniper rifle\nDmg: 16 | FR: 2s | Mag: 20",
texture = "ctf_ranged_sniper_rifle_magnum.png",
- fire_sound = "ctf_ranged_sniper_shot",
+ fire_sound = "ctf_ranged_sniper",
rounds = 20,
range = 400,
damage = 16,
@@ -324,7 +358,7 @@ ctf_ranged.simple_register_gun("ctf_ranged:sniper_magnum", {
if scoped[user:get_player_name()] then
ctf_ranged.hide_scope(user:get_player_name())
else
- local item_name = itemstack:get_name():gsub("_loaded", "")
+ local item_name = itemstack:get_name()
ctf_ranged.show_scope(user:get_player_name(), item_name, 8)
end
end
@@ -340,14 +374,14 @@ ctf_ranged.simple_register_gun("ctf_ranged:sniper_magnum", {
local time = 0
minetest.register_globalstep(function(dtime)
time = time + dtime
- if time < timer then
+ if time < 1 then
return
end
time = 0
for name, info in pairs(scoped) do
local player = minetest.get_player_by_name(name)
- local wielded_item = player:get_wielded_item():get_name():gsub("_loaded", "")
+ local wielded_item = player:get_wielded_item():get_name()
if wielded_item ~= info.item_name then
ctf_ranged.hide_scope(name)
end
diff --git a/mods/ctf/ctf_combat/ctf_ranged/sounds/ctf_ranged_ricochet.ogg b/mods/ctf/ctf_combat/ctf_ranged/sounds/ctf_ranged_ricochet.ogg
new file mode 100644
index 000000000..cf00ff321
Binary files /dev/null and b/mods/ctf/ctf_combat/ctf_ranged/sounds/ctf_ranged_ricochet.ogg differ
diff --git a/mods/ctf/ctf_combat/ctf_ranged/sounds/ctf_ranged_sniper.ogg b/mods/ctf/ctf_combat/ctf_ranged/sounds/ctf_ranged_sniper.ogg
new file mode 100644
index 000000000..5ee285734
Binary files /dev/null and b/mods/ctf/ctf_combat/ctf_ranged/sounds/ctf_ranged_sniper.ogg differ
diff --git a/mods/ctf/ctf_combat/ctf_ranged/sounds/license.txt b/mods/ctf/ctf_combat/ctf_ranged/sounds/license.txt
index a5f583a09..d421d7bb8 100644
--- a/mods/ctf/ctf_combat/ctf_ranged/sounds/license.txt
+++ b/mods/ctf/ctf_combat/ctf_ranged/sounds/license.txt
@@ -2,20 +2,23 @@ Sounds were taken from the shooter mod. Relevant section from its license.txt:
License Sounds: freesound.org
- flobert1_20070728.wav by Nonoo - Attribution 3.0 Unported (CC BY 3.0)
+- flobert1_20070728.wav by Nonoo - Attribution 3.0 Unported (CC BY 3.0)
- shot.wav by Sergenious - Attribution 3.0 Unported (CC BY 3.0)
+- shot.wav by Sergenious - Attribution 3.0 Unported (CC BY 3.0)
- GUNSHOT.WAV by erkanozan - CC0 1.0 Universal (CC0 1.0)
+- GUNSHOT.WAV by erkanozan - CC0 1.0 Universal (CC0 1.0)
- winchester-rifle-cock-reload.wav by MentalSanityOff - CC0 1.0 Universal (CC0 1.0)
+- winchester-rifle-cock-reload.wav by MentalSanityOff - CC0 1.0 Universal (CC0 1.0)
- trigger-with-hammer-fall.wav by Nanashi - CC0 1.0 Universal (CC0 1.0)
+- trigger-with-hammer-fall.wav by Nanashi - CC0 1.0 Universal (CC0 1.0)
- woosh.wav by ReadeOnly - CC0 1.0 Universal (CC0 1.0)
+- woosh.wav by ReadeOnly - CC0 1.0 Universal (CC0 1.0)
- AGM-114 Hellfire Rocket Missile Launch.flac by qubodup - CC0 1.0 Universal (CC0 1.0)
+- AGM-114 Hellfire Rocket Missile Launch.flac by qubodup - CC0 1.0 Universal (CC0 1.0)
- Sparkler.aif by Ned Bouhalassa - CC0 1.0 Universal (CC0 1.0)
+- Sparkler.aif by Ned Bouhalassa - CC0 1.0 Universal (CC0 1.0)
- explosion10.wav by V-ktor - CC0 1.0 Universal (CC0 1.0)
\ No newline at end of file
+- explosion10.wav by V-ktor - CC0 1.0 Universal (CC0 1.0)
+
+- `sniper_rifles_rifle.ogg` (`CC0 1.0`)
+ - Converted from [Battle Rifle.wav](https://freesound.org/people/morganpurkis/sounds/391725/) by [morganpurkis](https://freesound.org/people/morganpurkishttps://freesound.org/people/morganpurkis/sounds/391725/).
\ No newline at end of file
diff --git a/mods/ctf/ctf_core/init.lua b/mods/ctf/ctf_core/init.lua
index 4fd416f08..3dbda6bac 100644
--- a/mods/ctf/ctf_core/init.lua
+++ b/mods/ctf/ctf_core/init.lua
@@ -2,6 +2,7 @@ ctf_core = {
settings = {
-- server_mode = minetest.settings:get("ctf_server_mode") or "play",
server_mode = minetest.settings:get_bool("creative_mode", false) and "mapedit" or "play",
+ low_ram_mode = minetest.settings:get("ctf_low_ram_mode") == "true" or false,
}
}
diff --git a/mods/ctf/ctf_cosmetics/init.lua b/mods/ctf/ctf_cosmetics/init.lua
index d666345d5..c7502dc23 100644
--- a/mods/ctf/ctf_cosmetics/init.lua
+++ b/mods/ctf/ctf_cosmetics/init.lua
@@ -2,29 +2,34 @@ ctf_cosmetics = {}
function ctf_cosmetics.get_colored_skin(player, color)
color = color or "white"
- local extras = ""
+ local extras = {}
for clothing, clothcolor in pairs(ctf_cosmetics.get_extra_clothing(player)) do
+ local append = false
+
+ if type(clothcolor) == "table" then
+ append = clothcolor.append
+ clothcolor = clothcolor.color
+ end
+
if clothing:sub(1, 1) ~= "_" then
local texture = ctf_cosmetics.get_clothing_texture(player, clothing)
if texture then
- extras = string.format(
- "%s^(%s^[multiply:%s)", extras,
+ table.insert(extras, append and (#extras + 1) or 1, string.format(
+ "^(%s^[multiply:%s)",
texture,
clothcolor
- )
+ ))
end
end
end
return string.format(
"character.png^(%s^[multiply:%s)^(%s^[multiply:%s)%s",
- ctf_cosmetics.get_clothing_texture(player, "shirt"),
- color,
- ctf_cosmetics.get_clothing_texture(player, "pants"),
- color,
- extras
+ ctf_cosmetics.get_clothing_texture(player, "shirt"), color,
+ ctf_cosmetics.get_clothing_texture(player, "pants"), color,
+ table.concat(extras)
)
end
@@ -71,6 +76,6 @@ function ctf_cosmetics.get_extra_clothing(player)
if meta == "" then
return {_unset = true}
else
- return minetest.deserialize(meta)
+ return minetest.deserialize(meta) or {_unset = true}
end
end
diff --git a/mods/ctf/ctf_map/README.md b/mods/ctf/ctf_map/README.md
index 616775b49..5f36b75b6 100644
--- a/mods/ctf/ctf_map/README.md
+++ b/mods/ctf/ctf_map/README.md
@@ -15,16 +15,16 @@
3. Enable creative mode. This will enable `mapedit` mode.
4. *Optional:* Enable `worldedit`
-### 3. Select an area
+> You can grant yourself the `ctf_map_editor` privilege by running `/grantme ctf_map_editor` or by running `/grantme all` which grants you all the privileges that will be useful for map making.
-1. Decide where you will build your map. The area can be maximum 230x230 blocks in surface area, but it can be lesser.
-2. Grant yourself the `ctf_map_editor` privilege by running `/grantme ctf_map_editor`
-3. Run `/ctf_map editor`
-4. Select "Create New Map"
-5. Select a position to be one corner of the map.
-6. Select a position in the opposite corner. Place it higher/lower than the first one to give the map height.
+### 3. Selecting the Map Area
- **Note:** you can change your area at any time by typing `/ctf_map editor` and pressing the "Corners" button.
+Decide where you will build your map. We recommend you don't make your map larger than 230x230x230 blocks.
+1. Run `ctf_map editor`
+2. Press `Create New Map`
+3. Select a position to be one corner of the map.
+4. Select a position in the opposite corner. Place it higher/lower than the first one to give the map height.
+**Note:** you can change your map area at any time by typing `/ctf_map editor` and pressing the "Corners" button.
### 4. Build
@@ -36,10 +36,13 @@
- Many blocks have an indestructible variant
- Don't forget to add
- Team chests
- - Flags. Place it, then right-click it to choose flag color.
- - "Indestructible Barrier Glass" (`ctf_map:ind_glass`) around the edge of the world.
- - "Indestructible Red Barrier Glass" (`ctf_map:ind_glass_red`) for the build-time wall.
- - "Indestructible Red Barrier Stone" (`ctf_map:ind_stone_red`) for underground build-time wall.
+ - Indestructible blocks under the flag. The minimum is 5x5 blocks, with the flag on top of them in the center.
+ - "Indestructible Barrier Glass" (`ctf_map:ind_glass`) around the sides of the world (You don't need it on the ceiling)
+ - "Indestructible Red Barrier Glass" (`ctf_map:ind_glass_red`) for the build-time wall. This will disappear once the match begins. (More on that later)
+ - "Indestructible Red Barrier Stone" (`ctf_map:ind_stone_red`) for underground build-time wall. This will turn into stone(`default:stone`) once the match begins. (More on that below)
+
+The positions the `Indestructible Red Barrier` and the `Indestructible Red Barrier Stone` are automatically calculated when you save the map.
+If you wish to save your map for later edits, follow the note in the section about exporting the map.
### 5. Fill out information about the map
@@ -47,22 +50,20 @@ Run `/ctf_map editor`.
An explanation of some of the fields is given below.
#### Map Enabled
-Whether or not the map is available for play. You will want to check this.
+Whether or not the map is available for play. You will want to make sure it's enabled.
#### License
-
* Every map must have its own license.
-* If attribution is required (for example if you modify other's map and you have to tell who is author of the original map), that has to be appended to the `license` field.
-If you want to give more information, you can use the `Other info` field.
+* You can append any attribution you need to give to the `license` field (For example: If you modified someone's map or used one of their builds you'd list their name and what map/build of theirs that you modified/used). If you want to give more information, you can use the `Other info` field.
* If you don't know which license to use, [this list of CC licenses](https://creativecommons.org/use-remix/cc-licenses/) can help you.
-* We can only accept Free Software licenses, e.g.`CC BY-SA 4.0`.
-* Please know what you are doing when choosing a certain license. For example, you can read information about various licenses and/or consult a lawyer.
+* We can only accept the free culture licenses like `CC BY-SA 4.0`. Note that not all licenses in the Creative Commons family are free(as in freedom) (e.g `CC BY-ND`).
+* Please make sure you know what you are doing when choosing a license. You could do a little research about various licenses, ask a parent for help if you're young and haven't dealt with licenses before, and/or consult a lawyer.
#### Map Hint
-Does your map have hidden treasures? You can hint about them with the hint field.
+Does your map have hidden treasures? You can hint about them with the "Map Hint" field.
-#### Treasures
-A list of treasures that could end up in treasure chests
+#### Treasures *(optional)*
+A list of treasures that can be added specifically for your map that don't end up in chests by default.
Format:
```
@@ -86,44 +87,70 @@ An example of `initial_stuff` value that registers a stone pickaxe, 30 cobblesto
default:pick_stone,default:cobble 30,default:torch 5,ctf_ranged:pistol_loaded
```
-#### Map start_time
-What time of the day the match begins at. Changing this field will instantly update the time of day in the world you are editing.
+#### Map Constants
+1. `Map Shadow Intensity`: Sets the intensity of the shadows.
+2. `Map Gravity`: Gravitational constant of the map. (default = 1)
+3. `Map Movement Speed`: Regulates the speed at which players move. (default = 1)
+4. `Map Jump Height`: Regulates the height of jumps. (default = 1)
+5. `Map start_time`: Sets time of the day the match begins at. Changing this field will instantly update the time of day in the world you are editing.
+ * `0` is for midnight
+ * `1000` is for 1 AM
+ * `2000` is for 2 AM
+ * etc.
-* `0` is for midnight
-* `1000` is for 1 AM
-* `2000` is for 2 AM
-* etc.
+#### Flag Positions
+Positions where team flags are placed. You can select the teams that you want on the map and place the flags accordingly.
#### Zone Bounds
-How far the players of a certain team may go during build time.
-**Note:** Even standing on the boundary sends the player back. They do not have to go beyond it. Place the boundaries accordingly.
+How far the players of a certain team may go during build time. The zone bounds should overlap at the Red Barrier wall.
+
+> **Note:** Even players standing on the edge of their team zone are sent back to their base. It doesn't just trigger when they go beyond it.
#### Chest Zones
-Areas where chests can be placed. Some maps allow placing chests anywhere, while others only place them in certain locations.
+Areas where treasure chests are placed. Some maps have treasure chests placed throughout the entire map, while others only have them placed in certain small areas/rooms. What you choose for your map is up to you.
-### 6. Export
+### 6. Map Saving
-1. Press "Finish Editing"
-2. Find the exported map
-3. Move to \[Minetest folder]/games/capturetheflag/mods/ctf/ctf_map/maps
+#### Saving your changes
+* Run `/ctf_map editor` and press "Finish Editing" after scrolling to the bottom
+
+#### Moving your map to where CTF can load it
+* Find the exported map located in your map editor save folder`[Minetest folder]/worlds/[Map World]/schems/[Exported Map folder]`
+* Move the exported map folder to the CTF map folder `[Minetest folder]/games/capturetheflag/mods/ctf/ctf_map/maps`.
+
+#### Resuming editing
+Once you've moved your map once (See above) you can make edits to your map without having to move it again by running `/ctf_map editor`, clicking on your map in the list of maps, and then pressing `Resume Editing`, which tells CTF not to use the blocks in the map you copied to the CTF map folder.
+* Any changes you make to the `/ctf_map editor` gui (chest zones, etc) won't apply. You have to copy the map for that.
+* If you accidentally press `Start Editing` instead of `Resume Editing` you need to close the game without saving *copy your map to where CTF can load it* (See above). Otherwise your changes will be lost, because `Start Editing` tells CTF to use the blocks in the map you moved to the CTF map folder, which will be outdated if you've made changes since you last moved your map over.
### 7. Play
1. Create a new world with the `singlenode` mapgen.
-2. Make sure creative mode is disabled
+2. Make sure creative mode is disabled before joining
3. Grant yourself `ctf_admin` by issuing `/grantme ctf_admin`
-4. Type `/maps`
-5. Select your map
-6. Press "Skip to map"
+4. Run `/ctf_next -f ` Using your map's folder (or technical) name instead of ``
### 8. Screenshot
-If you choose to submit your map, include a screenshot of it in the exported map's folder. It should have an aspect ratio of 3:2 (screenshot 600x400px is suggested).
+If you choose to submit your map, include a screenshot of it in the exported map's folder. It should be taken without any texture packs enabled and must have an aspect ratio of 3:2 (screenshot `600px`x`400px` is suggested).
+
You can take a screenshot easily by doing the following:
1. Hide the HUD. By default F1 does that.
2. Hide the chat log. By default F2 does that.
-3. Try to find a good view that shows most of the map.
-4. *(Optional)* Increase your view range if important parts of the map cannot be seen. By default the = (or +) and - keys do that.
-5. Take a screenshot **from Minetest**. By default F12 does that.
-6. You can find the screenshot in `[Minetest folder]/screenshots` unless you have changed the path in settings.
+3. See if your screenshot looks better with/without fog enabled. You can toggle it with F3 by default
+4. Try to find a good view that shows most of the map.
+5. *(Optional)* Increase your view range if important parts of the map cannot be seen. By default the = (or +) and - keys do that.
+6. Take a screenshot **from Minetest**. By default F12 does that.
+7. You can find the screenshot in `[Minetest folder]/screenshots` unless you have changed the path in settings.
+
Crop the screenshot into the aspect ratio mentioned above using a tool of your choice, and put the screenshot inside your exported map's folder. It should be named `screenshot.png`.
+
+### 9. Submission
+
+> Your PR should contain the Map Folder that should include the files `map.conf`, `map.mts`, and `screenshot.png`.
+
+Maps can be submitted to the CTF Maps repository through PRs (Pull Requests). If you don't know how to make them, you could ask someone else to make it for you or make your own GitHub account and make it yourself. A benefit of making it on your own is that you can actively engage in the testing and development of the map PR. If you are new to making PRs and forks, you can learn about them by reading these documentations:
+- [https://docs.github.com/en/get-started/quickstart/fork-a-repo](https://docs.github.com/en/get-started/quickstart/fork-a-repo "https://docs.github.com/en/get-started/quickstart/fork-a-repo")
+- [https://docs.github.com/en/github/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork](https://docs.github.com/en/github/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork "https://docs.github.com/en/github/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork")
+
+If you are creating the PR yourself, it is always better to make each PR on a different branch of your fork of the repo. For example if you want to add a map, then make a separate branch for it, and so on. This will obviate and possible conflicts with any of your already open PRs.
diff --git a/mods/ctf/ctf_map/ctf_traps.lua b/mods/ctf/ctf_map/ctf_traps.lua
index acb580a8c..66bc75b79 100644
--- a/mods/ctf/ctf_map/ctf_traps.lua
+++ b/mods/ctf/ctf_map/ctf_traps.lua
@@ -68,35 +68,40 @@ minetest.register_node("ctf_map:spike", {
})
for _, team in ipairs(ctf_teams.teamlist) do
- local spikecolor = ctf_teams.team[team].color
-
- minetest.register_node("ctf_map:spike_"..team, {
- description = HumanReadable(team).." Team Spike",
- drawtype = "plantlike",
- tiles = {"ctf_map_spike.png^[colorize:"..spikecolor..":150"},
- inventory_image = "ctf_map_spike.png^[colorize:"..spikecolor..":150",
- use_texture_alpha = "clip",
- paramtype = "light",
- paramtype2 = "meshoptions",
- sunlight_propagates = true,
- walkable = false,
- damage_per_second = 7,
- groups = {cracky=1, level=2},
- drop = "ctf_map:spike",
- selection_box = {
- type = "fixed",
- fixed = {-0.5, -0.5, -0.5, 0.5, 0, 0.5},
- },
- on_place = function(itemstack, placer, pointed_thing)
- return minetest.item_place(itemstack, placer, pointed_thing, 34)
- end
- })
+ if not ctf_teams.team[team].not_playing then
+ local spikecolor = ctf_teams.team[team].color
+
+ minetest.register_node("ctf_map:spike_"..team, {
+ description = HumanReadable(team).." Team Spike",
+ drawtype = "plantlike",
+ tiles = {"ctf_map_spike.png^[colorize:"..spikecolor..":150"},
+ inventory_image = "ctf_map_spike.png^[colorize:"..spikecolor..":150",
+ use_texture_alpha = "clip",
+ paramtype = "light",
+ paramtype2 = "meshoptions",
+ sunlight_propagates = true,
+ walkable = false,
+ damage_per_second = 7,
+ groups = {cracky=1, level=2},
+ drop = "ctf_map:spike",
+ selection_box = {
+ type = "fixed",
+ fixed = {-0.5, -0.5, -0.5, 0.5, 0, 0.5},
+ },
+ on_place = function(itemstack, placer, pointed_thing)
+ return minetest.item_place(itemstack, placer, pointed_thing, 34)
+ end
+ })
+ end
end
minetest.register_on_player_hpchange(function(player, hp_change, reason)
if reason.type == "node_damage" then
local team = ctf_teams.get(player)
-
+ local spike_team = string.match(reason.node, "ctf_map:spike_(%S+)")
+ if spike_team and ctf_modebase.flag_captured[spike_team] then
+ return 0, true
+ end
if team and reason.node == string.format("ctf_map:spike_%s", team) then
return 0, true
end
@@ -129,7 +134,7 @@ local function damage_cobble_dig(pos, node, digger)
local placerobj = minetest.get_player_by_name(placer_name)
if placerobj then
- digger:punch(placerobj, 10, {
+ digger:punch(placerobj, 1, {
damage_groups = {
fleshy = 7,
damage_cobble = 1,
@@ -173,6 +178,49 @@ minetest.register_node("ctf_map:reinforced_cobble", {
description = "Reinforced Cobblestone",
tiles = {"ctf_map_reinforced_cobble.png"},
is_ground_content = false,
+ groups = {cracky = 3, stone = 2},
+ sounds = default.node_sound_stone_defaults(),
+ on_punch = function(pos, node, digger)
+ local meta = minetest.get_meta(pos)
+ local placer_team = meta:get_string("placer_team")
+ local digger_team = ctf_teams.get(digger)
+ if placer_team ~= digger_team then
+ minetest.set_node(pos, {name = "ctf_map:reinforced_cobble_hardened"})
+ meta = minetest.get_meta(pos)
+ meta:set_string("placer_team", placer_team)
+ end
+ end,
+ after_place_node = function(pos, placer, itemstack, pointed_thing)
+ local meta = minetest.get_meta(pos)
+ meta:set_string("placer_team", ctf_teams.get(placer))
+ end,
+ on_dig = function(pos, node, digger)
+ local meta = minetest.get_meta(pos)
+ meta:set_string("placer_team", "")
+ minetest.node_dig(pos, node, digger)
+ end
+})
+
+minetest.register_node("ctf_map:reinforced_cobble_hardened", {
+ description = "Reinforced Cobblestone Hardened\nYou're not meant to use this",
+ tiles = {"ctf_map_reinforced_cobble.png"},
+ is_ground_content = false,
groups = {cracky = 1, stone = 2},
sounds = default.node_sound_stone_defaults(),
+ drop = "ctf_map:reinforced_cobble",
+ on_punch = function(pos, node, digger)
+ local meta = minetest.get_meta(pos)
+ local placer_team = meta:get_string("placer_team")
+ local digger_team = ctf_teams.get(digger)
+ if placer_team == digger_team then
+ minetest.set_node(pos, {name = "ctf_map:reinforced_cobble"})
+ meta = minetest.get_meta(pos)
+ meta:set_string("placer_team", placer_team)
+ end
+ end,
+ on_dig = function(pos, node, digger)
+ local meta = minetest.get_meta(pos)
+ meta:set_string("placer_team", "")
+ minetest.node_dig(pos, node, digger)
+ end
})
diff --git a/mods/ctf/ctf_map/init.lua b/mods/ctf/ctf_map/init.lua
index 9ee4cac79..b66f0f2be 100644
--- a/mods/ctf/ctf_map/init.lua
+++ b/mods/ctf/ctf_map/init.lua
@@ -14,9 +14,61 @@ ctf_map = {
maps_dir = minetest.get_modpath("ctf_map").."/maps/",
skyboxes = {"none"},
current_map = false,
- barrier_nodes = {}, -- populated in nodes.lua
+ barrier_nodes = {}, -- populated in nodes.lua,
+ start_time = false,
+ get_duration = function()
+ if not ctf_map.start_time then
+ return "-"
+ end
+
+ local time = os.time() - ctf_map.start_time
+ return string.format("%02d:%02d:%02d",
+ math.floor(time / 3600), -- hours
+ math.floor((time % 3600) / 60), -- minutes
+ math.floor(time % 60)) -- seconds
+ end,
+
+ -- List of registered map folder names. Use `ctf_map.map_path` to get the path
+ registered_maps = {},
+
+ -- Table of map paths. Indexed by map's folder name
+ -- Doesn't include trailing '/'
+ map_path = {},
}
+function ctf_map.register_map(dirname, path_to_map)
+ if path_to_map:sub(-1) ~= "/" then
+ path_to_map = path_to_map .. "/"
+ end
+
+ assert(table.indexof(ctf_map.registered_maps, dirname) == -1, "Duplicate map detected: "..path_to_map)
+
+ table.insert(ctf_map.registered_maps, dirname)
+ ctf_map.map_path[dirname] = path_to_map .. dirname
+end
+
+function ctf_map.register_maps_dir(path_to_folder)
+ if path_to_folder:sub(-1) ~= "/" then
+ path_to_folder = path_to_folder .. "/"
+ end
+
+ for _, mapdir in pairs(minetest.get_dir_list(path_to_folder, true)) do
+ ctf_map.register_map(mapdir, path_to_folder)
+ end
+end
+
+ctf_map.register_maps_dir(ctf_map.maps_dir)
+
+ctf_api.register_on_match_start(function()
+ ctf_map.start_time = os.time()
+end)
+
+ctf_api.register_on_match_end(function()
+ minetest.after(0, function()
+ ctf_map.start_time = nil
+ end)
+end)
+
for _, s in ipairs(skybox.get_skies()) do
table.insert(ctf_map.skyboxes, s[1])
end
@@ -72,11 +124,11 @@ ctf_core.include_files(
local directory = minetest.get_modpath(minetest.get_current_modname()) .. "/maps/"
for _, entry in ipairs(minetest.get_dir_list(directory, true)) do
- for _, filename in ipairs(minetest.get_dir_list(directory .. "/" .. entry .. "/", false)) do
- if filename == "init.lua" then
- dofile(directory .. "/" .. entry .. "/"..filename)
- end
- end
+ for _, filename in ipairs(minetest.get_dir_list(directory .. "/" .. entry .. "/", false)) do
+ if filename == "init.lua" then
+ dofile(directory .. "/" .. entry .. "/"..filename)
+ end
+ end
end
@@ -120,6 +172,118 @@ minetest.register_chatcommand("ctf_map", {
end
})
+minetest.register_chatcommand("map", {
+ description = "Prints the current map name and map author",
+ func = function()
+ local map = ctf_map.current_map
+
+ if not map then
+ return false, "There is no map currently in play"
+ end
+
+ local mapName = map.name or "Unknown"
+ local mapAuthor = map.author or "Unknown Author"
+ local mapDuration = ctf_map.get_duration()
+
+ return true, string.format("The current map is %s by %s. Map duration: %s", mapName, mapAuthor, mapDuration)
+ end
+})
+
+minetest.register_chatcommand("ctf_barrier", {
+ description = "Place or remove map barriers\n" ..
+ "place_buildtime: Within the selected area, replace certain nodes with the " ..
+ "corresponding build-time barrier\nremove_buildtime: Remove build-time " ..
+ "barriers within the selected area\nplace_outer: Surrounds the selected area " ..
+ "with an indestructible glass/stone barrier",
+ privs = {ctf_map_editor = true},
+ params = "[place_buildtime] | [remove_buildtime] | [place_outer]",
+ func = function(name, params)
+ if not params or params == "" then
+ return false, "See /help ctf_barrier for usage instructions"
+ end
+
+ if ctf_core.settings.server_mode ~= "mapedit" then
+ return false, minetest.colorize("red", "You have to be in mapedit mode to run this")
+ end
+
+ if params ~= "place_buildtime" and params ~= "remove_buildtime" and params ~= "place_outer" then
+ return false
+ end
+
+ if params == "place_outer" then
+ minetest.chat_send_player(name,
+ minetest.colorize("yellow", "Warning: this action can't be undone"))
+ end
+
+ ctf_map.get_pos_from_player(name, 2, function(p, positions)
+ local pos1, pos2 = vector.sort(positions[1], positions[2])
+
+ if params == "place_buildtime" and pos1.x ~= pos2.x and pos1.z ~= pos2.z then
+ minetest.chat_send_player(name, minetest.colorize("yellow",
+ "Warning: your build-time barrier is more than 1 node thick, " ..
+ "use /ctf_barrier remove_buildtime to remove unwanted parts"))
+ end
+
+ local vm = minetest.get_voxel_manip()
+ local emin, emax = vm:read_from_map(pos1, pos2)
+ local a = VoxelArea:new{
+ MinEdge = emin,
+ MaxEdge = emax
+ }
+ local data = vm:get_data()
+
+ for x = pos1.x, pos2.x do
+ for y = pos1.y, pos2.y do
+ for z = pos1.z, pos2.z do
+ local vi = a:index(x, y, z)
+ if params == "place_buildtime" then
+ if data[vi] == minetest.get_content_id("air") then
+ data[vi] = minetest.get_content_id("ctf_map:ind_glass_red")
+ elseif data[vi] == minetest.get_content_id("default:stone") then
+ data[vi] = minetest.get_content_id("ctf_map:ind_stone_red")
+ elseif data[vi] == minetest.get_content_id("default:water_source") then
+ data[vi] = minetest.get_content_id("ctf_map:ind_water")
+ elseif data[vi] == minetest.get_content_id("default:lava_source") then
+ data[vi] = minetest.get_content_id("ctf_map:ind_lava")
+ end
+ elseif params == "remove_buildtime" then
+ if data[vi] == minetest.get_content_id("ctf_map:ind_glass_red") then
+ data[vi] = minetest.get_content_id("air")
+ elseif data[vi] == minetest.get_content_id("ctf_map:ind_stone_red") then
+ data[vi] = minetest.get_content_id("default:stone")
+ elseif data[vi] == minetest.get_content_id("ctf_map:ind_water") then
+ data[vi] = minetest.get_content_id("default:water_source")
+ elseif data[vi] == minetest.get_content_id("ctf_map:ind_lava") then
+ data[vi] = minetest.get_content_id("default:lava_source")
+ end
+ elseif params == "place_outer" then
+ if x == pos1.x or x == pos2.x or y == pos1.y
+ or z == pos1.z or z == pos2.z then
+ if data[vi] == minetest.get_content_id("air") or
+ data[vi] == minetest.get_content_id("ignore") or
+ data[vi] == minetest.get_content_id("ctf_map:ignore") then
+ data[vi] = minetest.get_content_id("ctf_map:ind_glass")
+ else
+ data[vi] = minetest.get_content_id("ctf_map:stone")
+ end
+ end
+ end
+ end
+ end
+ end
+
+ vm:set_data(data)
+ vm:write_to_map(true)
+
+ local message =
+ (params == "place_buildtime" and "Build-time barrier placed") or
+ (params == "remove_buildtime" and "Build-time barrier removed") or
+ (params == "place_outer" and "Outer barrier placed")
+ minetest.chat_send_player(name, message)
+ end)
+ end
+})
+
-- Attempt to restore user's time speed after server close
local TIME_SPEED = minetest.settings:get("time_speed")
diff --git a/mods/ctf/ctf_map/map_functions.lua b/mods/ctf/ctf_map/map_functions.lua
index 540973554..496da71df 100644
--- a/mods/ctf/ctf_map/map_functions.lua
+++ b/mods/ctf/ctf_map/map_functions.lua
@@ -1,7 +1,7 @@
function ctf_map.announce_map(map)
local msg = (minetest.colorize("#fcdb05", "Map: ") .. minetest.colorize("#f49200", map.name) ..
minetest.colorize("#fcdb05", " by ") .. minetest.colorize("#f49200", map.author))
- if map.hint then
+ if map.hint and map.hint ~= "" then
msg = msg .. "\n" .. minetest.colorize("#f49200", map.hint)
end
minetest.chat_send_all(msg)
@@ -9,22 +9,25 @@ end
function ctf_map.place_map(mapmeta, callback)
local dirname = mapmeta.dirname
- local schempath = ctf_map.maps_dir .. dirname .. "/map.mts"
+ local schempath = ctf_map.map_path[dirname] .. "/map.mts"
+
+ local barrier_data = mapmeta.barriers and mapmeta.barriers()
ctf_map.emerge_with_callbacks(nil, mapmeta.pos1, mapmeta.pos2, function(ctx)
local rotation = (mapmeta.rotation and mapmeta.rotation ~= "z") and "90" or "0"
- local res = minetest.place_schematic(mapmeta.pos1, schempath, rotation)
+ local res = minetest.place_schematic(mapmeta.pos1, schempath, rotation, {["ctf_map:chest"] = "air"})
minetest.log("action", string.format(
- "Placed map %s in %.2fs", dirname, (minetest.get_us_time() - ctx.start_time) / 1000000
+ "Placed map %s in %.2fs", dirname, (minetest.get_us_time() - ctx.start_time) / 1e6
))
for name, def in pairs(mapmeta.teams) do
local p = def.flag_pos
+ minetest.load_area(p)
local node = minetest.get_node(p)
- if node.name ~= "ctf_modebase:flag" then
+ if node.name ~= "ignore" and node.name ~= "ctf_modebase:flag" then
minetest.log("error", name.."'s flag was set incorrectly, or there is no flag node placed")
else
minetest.set_node(vector.offset(p, 0, 1, 0), {name="ctf_modebase:flag_top_"..name, param2 = node.param2})
@@ -40,12 +43,16 @@ function ctf_map.place_map(mapmeta, callback)
end
end
- minetest.after(0, minetest.fix_light, mapmeta.pos1, mapmeta.pos2)
+ minetest.fix_light(mapmeta.pos1, mapmeta.pos2)
assert(res, "Unable to place schematic, does the MTS file exist? Path: " .. schempath)
ctf_map.current_map = mapmeta
+ if barrier_data then
+ ctf_map.current_map.barrier_data = barrier_data
+ end
+
callback()
end)
end
@@ -54,61 +61,95 @@ end
--- VOXELMANIP FUNCTIONS
--
--- Takes [mapmeta] or [pos1, pos2] arguments
-function ctf_map.remove_barrier(mapmeta, pos2)
- local pos1 = mapmeta
+local ID_IGNORE = minetest.CONTENT_IGNORE
+local ID_AIR = minetest.CONTENT_AIR
+local ID_WATER = minetest.get_content_id("default:water_source")
- if not pos2 then
- pos1, pos2 = mapmeta.barrier_area.pos1, mapmeta.barrier_area.pos2
- end
+---@param mapmeta table Map meta table
+---@param callback function
+function ctf_map.remove_barrier(mapmeta, callback)
+ if not mapmeta.barrier_data then
+ minetest.log("action", "Clearing barriers using mapmeta.barrier_area")
- local vm = VoxelManip()
- pos1, pos2 = vm:read_from_map(pos1, pos2)
+ local pos1, pos2 = mapmeta.barrier_area.pos1, mapmeta.barrier_area.pos2
- local data = vm:get_data()
+ local vm = VoxelManip(pos1, pos2)
+ local data = vm:get_data()
- -- Shave off ~0.1 seconds from the main loop
- minetest.handle_async(function(d, p1, p2, barrier_nodes, t)
- local Nx = p2.x - p1.x + 1
- local Ny = p2.y - p1.y + 1
- local ID_IGNORE = minetest.CONTENT_IGNORE
- local ID_AIR = minetest.CONTENT_AIR
-
- for z = p1.z, p2.z do
- for y = p1.y, p2.y do
- for x = p1.x, p2.x do
- local vi = (z - p1.z) * Ny * Nx + (y - p1.y) * Nx + (x - p1.x) + 1
- local done = false
-
- for barriernode_id, replacement_id in pairs(barrier_nodes) do
- if d[vi] == barriernode_id then
- d[vi] = replacement_id
- done = true
- break
- end
- end
+ for i, id in pairs(data) do
+ local done = false
- -- Liquid updates fail if I turn everything but changes into ignore
- if not done and d[vi] == ID_AIR then
- d[vi] = ID_IGNORE
- end
+ for barriernode_id, replacement_id in pairs(ctf_map.barrier_nodes) do
+ if id == barriernode_id then
+
+ data[i] = replacement_id
+ done = true
+ break
end
end
+
+ if not done then
+ data[i] = ID_IGNORE
+ end
end
- return d
- end, function(d)
- vm:set_data(d)
- vm:update_liquids()
+ vm:set_data(data)
vm:write_to_map(false)
- end, data, pos1, pos2, ctf_map.barrier_nodes)
+
+ minetest.after(0.1, function()
+ local vm2 = VoxelManip(pos1, pos2)
+ vm2:update_liquids()
+
+ callback()
+ end)
+ else
+ minetest.log("action", "Clearing barriers using barriers.data")
+
+ local i = 0
+ for _, barrier_area in pairs(mapmeta.barrier_data) do
+ minetest.after(i, function()
+ if mapmeta.barrier_data then
+ local vm = VoxelManip()
+ vm:read_from_map(barrier_area.pos1, barrier_area.pos2)
+
+ local data = vm:get_data()
+
+ if #data ~= barrier_area.max then
+ -- minetest.log(dump(mapmeta.barrier_data)) -- Used for debugging issues
+ minetest.log("error", "Potential issue with barriers.data. Aborting... | " ..
+ "Debug: "..dump(#data)..", "..dump(barrier_area.max))
+
+ mapmeta.barrier_data = nil
+ ctf_map.remove_barrier(mapmeta, callback)
+ end
+
+ for idx in pairs(data) do
+ data[idx] = barrier_area.reps[idx] or ID_IGNORE
+ end
+
+ vm:set_data(data)
+ vm:write_to_map(false)
+ end
+ end)
+
+ i = i + 0.05
+ end
+
+ minetest.after(i - 0.05, function()
+ if mapmeta.barrier_data then
+ local vm = VoxelManip(mapmeta.pos1, mapmeta.pos2)
+ vm:update_liquids()
+
+ mapmeta.barrier_data = nil -- Contains a large amount of data, free it up now that it's not needed
+
+ callback()
+ end
+ end)
+ end
end
-local ID_AIR = minetest.CONTENT_AIR
-local ID_IGNORE = minetest.CONTENT_IGNORE
-local ID_CHEST = minetest.get_content_id("ctf_map:chest")
-local ID_WATER = minetest.get_content_id("default:water_source")
+local ID_CHEST = minetest.get_content_id("ctf_map:chest")
local function get_place_positions(a, data, pos1, pos2)
if a.amount <= 0 then return {} end
@@ -174,8 +215,10 @@ local function prepare_nodes(pos1, pos2, data, team_chest_items, blacklisted_nod
end
for _, team in ipairs(ctf_teams.teamlist) do
- local node = "ctf_teams:chest_" .. team
- nodes[minetest.get_content_id(node)] = minetest.registered_nodes[node]
+ if not ctf_teams.team[team].not_playing then
+ local node = "ctf_teams:chest_" .. team
+ nodes[minetest.get_content_id(node)] = minetest.registered_nodes[node]
+ end
end
for i, v in ipairs(data) do
@@ -219,7 +262,11 @@ local function place_treasure_chests(mapmeta, pos1, pos2, data, param2_data, tre
if #place_positions < a.amount then
minetest.log("error",
- string.format("[MAP] Couldn't place %d from %d chests from pos %d", a.amount - #place_positions, a.amount, i)
+ string.format("[MAP] Couldn't place %d of the %d chests needed to place in zone %d",
+ a.amount - #place_positions,
+ a.amount,
+ i
+ )
)
end
end
diff --git a/mods/ctf/ctf_map/map_meta.lua b/mods/ctf/ctf_map/map_meta.lua
index 1539ad701..4aac16868 100644
--- a/mods/ctf/ctf_map/map_meta.lua
+++ b/mods/ctf/ctf_map/map_meta.lua
@@ -1,5 +1,7 @@
-local CURRENT_MAP_VERSION = "2"
-local modname = minetest.get_current_modname();
+local CURRENT_MAP_VERSION = "3"
+local BARRIER_Y_SIZE = 16
+
+local modname = minetest.get_current_modname()
function ctf_map.skybox_exists(subdir)
local list = minetest.get_dir_list(subdir, true)
@@ -24,19 +26,58 @@ local function calc_flag_center(map)
return flag_center
end
+local function connect_barriers_file(map_name, offset, barriers_filepath)
+ return function()
+ local f, err = io.open(barriers_filepath, "rb")
+
+ if (ctf_core.settings.server_mode ~= "mapedit" and assert(f, err)) or f then
+ local barriers = f:read("*all")
+
+ f:close()
+
+ assert(barriers and barriers ~= "")
+
+ barriers = minetest.deserialize(minetest.decompress(barriers, "deflate"))
+
+ if barriers then
+ for _, barrier_area in pairs(barriers) do
+ barrier_area.pos1 = vector.add(barrier_area.pos1, offset)
+ barrier_area.pos2 = vector.add(barrier_area.pos2, offset)
+
+ for i = 1, barrier_area.max do
+ if not barrier_area.reps[i] then
+ barrier_area.reps[i] = minetest.CONTENT_IGNORE
+ else
+ barrier_area.reps[i] = minetest.get_content_id(barrier_area.reps[i])
+ end
+ end
+ end
+
+ return barriers
+ else
+ minetest.log("error", "Map "..map_name.." has a corrupted barriers file. Re-save map to fix")
+ end
+ else
+ minetest.log("error", "Map "..map_name.." is missing its barriers file. Re-save map to fix")
+ end
+ end
+end
+
function ctf_map.load_map_meta(idx, dirname)
- local meta = Settings(ctf_map.maps_dir .. dirname .. "/map.conf")
+ assert(ctf_map.map_path[dirname], "Map "..dirname.." not found")
+
+ local meta = Settings(ctf_map.map_path[dirname] .. "/map.conf")
if not meta then error("Map '"..dump(dirname).."' not found") end
minetest.log("info", "load_map_meta: Loading map meta from '" .. dirname .. "/map.conf'")
local map
- local offset = vector.new(600 * idx, 0, 0)
+ local offset = vector.new(608 * idx, 0, 0) -- 608 is a multiple of 16, the size of a mapblock
if not meta:get("map_version") then
if not meta:get("r") then
- error("Map was not properly configured: " .. ctf_map.maps_dir .. dirname .. "/map.conf")
+ error("Map was not properly configured: " .. ctf_map.map_path[dirname] .. "/map.conf")
end
local mapr = meta:get("r")
@@ -76,7 +117,8 @@ function ctf_map.load_map_meta(idx, dirname)
phys_gravity = tonumber(meta:get("phys_gravity")),
chests = {},
teams = {},
- barrier_area = {pos1 = pos1, pos2 = pos2}
+ barrier_area = {pos1 = pos1, pos2 = pos2},
+ barriers = false,
}
-- Read teams from config
@@ -123,7 +165,7 @@ function ctf_map.load_map_meta(idx, dirname)
amount = ctf_map.DEFAULT_CHEST_AMOUNT,
}
end
- elseif meta:get("map_version") == CURRENT_MAP_VERSION then
+ else
-- If new items are added also remember to change the table in mapedit_gui.lua
-- The version number should be updated if you change an item
local size = minetest.deserialize(meta:get("size"))
@@ -131,7 +173,7 @@ function ctf_map.load_map_meta(idx, dirname)
offset.y = -size.y/2
map = {
- map_version = CURRENT_MAP_VERSION,
+ map_version = tonumber(meta:get("map_version") or "0"),
pos1 = offset,
pos2 = vector.add(offset, size),
offset = offset,
@@ -157,6 +199,9 @@ function ctf_map.load_map_meta(idx, dirname)
game_modes = minetest.deserialize(meta:get("game_modes")),
enable_shadows = tonumber(meta:get("enable_shadows") or "0.26"),
}
+ if tonumber(meta:get("map_version")) >= 3 and not ctf_core.settings.low_ram_mode then
+ map.barriers = connect_barriers_file(dirname, offset, ctf_map.map_path[dirname] .. "/barriers.data")
+ end
for id, def in pairs(map.chests) do
map.chests[id].pos1 = vector.add(offset, def.pos1)
@@ -180,7 +225,7 @@ function ctf_map.load_map_meta(idx, dirname)
map.flag_center = calc_flag_center(map)
- if ctf_map.skybox_exists(ctf_map.maps_dir .. dirname) then
+ if ctf_map.skybox_exists(ctf_map.map_path[dirname]) then
skybox.add({dirname, "#ffffff", [5] = "png"})
map.skybox = dirname
@@ -217,11 +262,12 @@ function ctf_map.save_map(mapmeta)
if not def.enabled then
mapmeta.teams[id] = nil
else
+ minetest.load_area(def.flag_pos)
local flagpos = minetest.find_node_near(def.flag_pos, 3, {"group:flag_bottom"}, true)
if not flagpos then
flagpos = def.flag_pos
- minetest.chat_send_all(minetest.colorize("red",
+ minetest.chat_send_all(minetest.colorize((minetest.get_node(flagpos).name == "ignore") and "orange" or "red",
"Failed to find flag for team " .. id ..
". Node at given position: " .. dump(minetest.get_node(flagpos).name)
))
@@ -237,8 +283,63 @@ function ctf_map.save_map(mapmeta)
end
end
- mapmeta.barrier_area.pos1 = vector.subtract(mapmeta.barrier_area.pos1, mapmeta.offset)
- mapmeta.barrier_area.pos2 = vector.subtract(mapmeta.barrier_area.pos2, mapmeta.offset)
+ -- Calculate where barriers are
+ local barriers = {}
+ local pos1, pos2 = mapmeta.pos1:copy(), mapmeta.pos2:copy()
+ local barrier_area = {pos1 = pos1:subtract(mapmeta.offset), pos2 = pos2:subtract(mapmeta.offset)}
+
+ if pos1.y > pos2.y then
+ local t = pos2
+ pos2 = pos1
+ pos1 = t
+ end
+
+ if pos1.y + BARRIER_Y_SIZE < pos2.y then
+ pos2.y = pos1.y + BARRIER_Y_SIZE
+ end
+
+ local queue_break = false
+ while true do
+ local tmp = {
+ -- pos1 = pos1
+ -- pos2 = pos2
+ -- max = #data
+ reps = {}
+ }
+ local vm = VoxelManip()
+ pos1, pos2 = vm:read_from_map(pos1, pos2)
+ tmp.pos1, tmp.pos2 = pos1:subtract(mapmeta.offset), pos2:subtract(mapmeta.offset)
+
+ local data = vm:get_data()
+ local barrier_found = false
+ for i, v in ipairs(data) do
+ for b, rep in pairs(ctf_map.barrier_nodes) do
+ if v == b then
+ barrier_found = true
+ tmp.reps[i] = minetest.get_name_from_content_id(rep)
+ end
+ end
+ end
+
+ tmp.max = #data
+
+ if barrier_found then
+ table.insert(barriers, tmp)
+ end
+
+ if queue_break then
+ break
+ end
+
+ if pos2.y + BARRIER_Y_SIZE < mapmeta.pos2.y then
+ pos1.y = pos2.y + 1
+ pos2.y = pos2.y + BARRIER_Y_SIZE
+ else
+ pos1.y = pos2.y + 1
+ pos2.y = mapmeta.pos2.y
+ queue_break = true
+ end
+ end
meta:set("map_version" , CURRENT_MAP_VERSION)
meta:set("size" , minetest.serialize(vector.subtract(mapmeta.pos2, mapmeta.pos1)))
@@ -258,21 +359,23 @@ function ctf_map.save_map(mapmeta)
meta:set("phys_gravity" , mapmeta.phys_gravity)
meta:set("chests" , minetest.serialize(mapmeta.chests))
meta:set("teams" , minetest.serialize(mapmeta.teams))
- meta:set("barrier_area" , minetest.serialize(mapmeta.barrier_area))
+ meta:set("barrier_area" , minetest.serialize(barrier_area))
meta:set("game_modes" , minetest.serialize(mapmeta.game_modes))
meta:set("enable_shadows", mapmeta.enable_shadows)
meta:write()
- minetest.after(0.1, function()
- local filepath = path .. "map.mts"
- if minetest.create_schematic(mapmeta.pos1, mapmeta.pos2, nil, filepath) then
- minetest.chat_send_all(minetest.colorize(ctf_map.CHAT_COLOR, "Saved Map '" .. mapmeta.name .. "' to " .. path))
- minetest.chat_send_all(minetest.colorize(ctf_map.CHAT_COLOR,
- "To play, move it to \""..minetest.get_modpath(modname).."/maps/"..mapmeta.dirname..", "..
- "start a normal ctf game, and run \"/ctf_next "..mapmeta.dirname.."\" then \"/ctf_skip\""));
- else
- minetest.chat_send_all(minetest.colorize(ctf_map.CHAT_COLOR, "Map Saving Failed!"))
- end
- end)
+ local filepath = path .. "map.mts"
+ if minetest.create_schematic(mapmeta.pos1, mapmeta.pos2, nil, filepath) then
+ minetest.chat_send_all(minetest.colorize(ctf_map.CHAT_COLOR, "Saved Map '" .. mapmeta.name .. "' to " .. path))
+ minetest.chat_send_all(minetest.colorize(ctf_map.CHAT_COLOR,
+ "To play, move it to \""..minetest.get_modpath(modname).."/maps/"..mapmeta.dirname..", "..
+ "start a normal ctf game, and run \"/ctf_next -f "..mapmeta.dirname.."\""));
+ else
+ minetest.chat_send_all(minetest.colorize(ctf_map.CHAT_COLOR, "Map Saving Failed!"))
+ end
+
+ local f = assert(io.open(path .. "barriers.data", "wb"))
+ f:write(minetest.compress(minetest.serialize(barriers), "deflate"))
+ f:close()
end
diff --git a/mods/ctf/ctf_map/mapedit_gui.lua b/mods/ctf/ctf_map/mapedit_gui.lua
index 9c736f01a..be4e6ed14 100644
--- a/mods/ctf/ctf_map/mapedit_gui.lua
+++ b/mods/ctf/ctf_map/mapedit_gui.lua
@@ -11,8 +11,8 @@ local function greet_player(player)
minetest.chat_send_player(
player:get_player_name(),
minetest.colorize(ctf_map.CHAT_COLOR,
- "To start, grant yourself \"ctf_map_editor\""..
- "using \"/grantme ctf_map_editor\" Then run \"/ctf_map editor\"")
+ "To start, grant yourself \"ctf_map_editor\" "..
+ "using \"/grantme ctf_map_editor\". Then run \"/ctf_map editor\"")
)
else
minetest.chat_send_player(
@@ -63,15 +63,63 @@ function ctf_map.set_flag_location(pname, teamname, pos)
context[pname].teams[teamname].flag_pos = pos
end
+local function dothenext(time, dir, func)
+ minetest.after(time, function()
+ local next = function(dir2)
+ dothenext(time, dir2, func)
+ end
+
+ func(next, dir)
+ end)
+end
+
+ctf_map.register_map_command("resave_all", function(name, params)
+ local maplist = table.copy(ctf_map.registered_maps)
+
+ dothenext(1, 1, function(next, dir)
+ if not maplist[dir] then
+ minetest.chat_send_player(
+ name,
+ minetest.colorize("green", "\nMap resaving done.\n")
+ )
+ return
+ end
+
+ local map = ctf_map.load_map_meta(dir, maplist[dir])
+
+
+ if map.enabled then
+ ctf_map.place_map(map, function()
+ edit_map(name, map)
+
+ if context[name].initial_stuff[1] == "none" then
+ table.remove(context[name].initial_stuff, 1)
+ end
+
+ if context[name].treasures == "none" then
+ context[name].treasures = nil
+ end
+
+ ctf_map.save_map(context[name])
+ context[name] = nil
+
+ next(dir + 1)
+ end)
+ else
+ next(dir + 1)
+ end
+ end)
+end)
+
function ctf_map.show_map_editor(player)
if context[player] then
ctf_map.show_map_save_form(player)
return
end
- local dirlist = minetest.get_dir_list(ctf_map.maps_dir, true)
- local dirlist_sorted = dirlist
- table.sort(dirlist_sorted)
+ local maplist = table.copy(ctf_map.registered_maps)
+ local maplist_sorted = maplist
+ table.sort(maplist_sorted)
local selected_map = 1
ctf_gui.old_show_formspec(player, "ctf_map:start", {
@@ -114,7 +162,6 @@ function ctf_map.show_map_editor(player)
--
chests = {},
teams = {},
- barrier_area = {pos1 = pos1, pos2 = pos2},
--
game_modes = {},
}
@@ -128,7 +175,7 @@ function ctf_map.show_map_editor(player)
type = "textlist",
pos = {"center", 1.7},
size = {6, 6},
- items = dirlist_sorted,
+ items = maplist_sorted,
func = function(pname, fields)
local event = minetest.explode_textlist_event(fields.currentmaps)
@@ -145,13 +192,13 @@ function ctf_map.show_map_editor(player)
ctf_gui.old_show_formspec(pname, "ctf_map:loading", {
size = {x = 6, y = 4},
title = "Capture The Flag Map Editor",
- description = "Placing map '"..dirlist_sorted[selected_map].."'. This will take a few seconds..."
+ description = "Placing map '"..maplist_sorted[selected_map].."'. This will take a few seconds..."
})
end)
minetest.after(0.5, function()
- local idx = table.indexof(dirlist, dirlist_sorted[selected_map])
- local map = ctf_map.load_map_meta(idx, dirlist_sorted[selected_map])
+ local idx = table.indexof(maplist, maplist_sorted[selected_map])
+ local map = ctf_map.load_map_meta(idx, maplist_sorted[selected_map])
ctf_map.place_map(map, function()
minetest.after(2, edit_map, pname, map)
@@ -167,14 +214,14 @@ function ctf_map.show_map_editor(player)
ctf_gui.old_show_formspec(pname, "ctf_map:loading", {
size = {x = 6, y = 4},
title = "Capture The Flag Map Editor",
- description = "Resuming map '"..dirlist_sorted[selected_map]..
+ description = "Resuming map '"..maplist_sorted[selected_map]..
"'.\n(Remember that this doesn't recall setting changes)"
})
end)
minetest.after(0.5, function()
- local idx = table.indexof(dirlist, dirlist_sorted[selected_map])
- local map = ctf_map.load_map_meta(idx, dirlist_sorted[selected_map])
+ local idx = table.indexof(maplist, maplist_sorted[selected_map])
+ local map = ctf_map.load_map_meta(idx, maplist_sorted[selected_map])
minetest.after(2, edit_map, pname, map)
end)
@@ -597,24 +644,6 @@ function ctf_map.show_map_save_form(player, scroll_pos)
idx = idx + 1
end
end
- idx = idx + 1
-
- elements.barrier_area = {
- type = "button", exit = true,
- label = "Barrier Area - " .. minetest.pos_to_string(context[player].barrier_area.pos1, 0) ..
- " - " .. minetest.pos_to_string(context[player].barrier_area.pos2, 0),
- pos = {0, idx},
- size = {9 - (ctf_gui.SCROLLBAR_WIDTH + 0.1), ctf_gui.ELEM_SIZE.y},
- func = function(pname, fields)
- ctf_map.get_pos_from_player(pname, 2, function(p, positions)
- context[pname].barrier_area.pos1 = positions[1]
- context[pname].barrier_area.pos2 = positions[2]
-
- minetest.after(0.1, ctf_map.show_map_save_form, pname,
- minetest.explode_scrollbar_event(fields.formcontent).value)
- end)
- end,
- }
idx = idx + 1.5
-- FINISH EDITING
diff --git a/mods/ctf/ctf_map/maps b/mods/ctf/ctf_map/maps
index 0765001c6..e831d074c 160000
--- a/mods/ctf/ctf_map/maps
+++ b/mods/ctf/ctf_map/maps
@@ -1 +1 @@
-Subproject commit 0765001c648e83a1012c8e73e541c4d75edac57e
+Subproject commit e831d074cfd388d06f961227706c00023e8ba2ff
diff --git a/mods/ctf/ctf_map/mod.conf b/mods/ctf/ctf_map/mod.conf
index fde0f366a..6f2e2c4c1 100644
--- a/mods/ctf/ctf_map/mod.conf
+++ b/mods/ctf/ctf_map/mod.conf
@@ -1,3 +1,3 @@
name = ctf_map
-depends = ctf_core, ctf_teams, ctf_gui, default, ctf_changes, skybox
+depends = ctf_core, ctf_teams, ctf_gui, default, ctf_changes, skybox, ctf_api
optional_depends = worldedit
diff --git a/mods/ctf/ctf_map/nodes.lua b/mods/ctf/ctf_map/nodes.lua
index 967c9b9b4..d04ceaa5c 100644
--- a/mods/ctf/ctf_map/nodes.lua
+++ b/mods/ctf/ctf_map/nodes.lua
@@ -50,6 +50,32 @@ minetest.register_node("ctf_map:ind_glass_red", {
})
ctf_map.barrier_nodes[minetest.get_content_id("ctf_map:ind_glass_red")] = minetest.CONTENT_AIR
+minetest.register_node("ctf_map:ind_water", {
+ description = "Indestructible Water Barrier Glass",
+ drawtype = "glasslike",
+ tiles = {"ctf_map_ind_water.png"},
+ inventory_image = minetest.inventorycube("ctf_map_ind_water.png"),
+ paramtype = "light",
+ sunlight_propagates = true,
+ is_ground_content = false,
+ walkable = true,
+ buildable_to = false,
+ use_texture_alpha = false,
+ alpha = 0,
+ pointable = ctf_core.settings.server_mode == "mapedit",
+ groups = {immortal = 1},
+ sounds = default.node_sound_glass_defaults()
+})
+ctf_map.barrier_nodes[minetest.get_content_id("ctf_map:ind_water")] = minetest.get_content_id("default:water_source")
+
+minetest.register_node("ctf_map:ind_lava", {
+ description = "Indestructible Lava Barrier Glass",
+ groups = {immortal = 1},
+ tiles = {"ctf_map_ind_lava.png"},
+ is_ground_content = false
+})
+ctf_map.barrier_nodes[minetest.get_content_id("ctf_map:ind_lava")] = minetest.get_content_id("default:lava_source")
+
minetest.register_node("ctf_map:ind_stone_red", {
description = "Indestructible Red Barrier Stone",
groups = {immortal = 1},
@@ -76,6 +102,7 @@ local mod_prefixes = {
default = "";
stairs = "";
wool = "wool_";
+ walls = "walls_";
}
-- See Lua API, section "Node-only groups"
@@ -83,9 +110,12 @@ local preserved_groups = {
bouncy = true;
fence = true;
connect_to_raillike = true;
+ wall = true;
disable_jump = true;
fall_damage_add_percent = true;
slippery = true;
+ tree = true;
+ wood = true;
}
local function make_immortal(def)
@@ -98,6 +128,26 @@ local function make_immortal(def)
def.description = def.description and ("Indestructible " .. def.description)
end
+minetest.register_on_player_hpchange(function(player, hp_change, reason)
+ local pos = player:get_pos()
+ local def = minetest.registered_nodes[reason.node]
+
+ if reason.type == 'node_damage' and def.groups.immortal and def.drawtype == "normal" and def.walkable ~= false then
+ for _, flagteam in ipairs(ctf_teams.current_team_list) do
+ if flagteam ~= ctf_teams.get(player) and ctf_map.current_map.teams[flagteam] then
+ local fdist = vector.distance(pos, ctf_map.current_map.teams[flagteam].flag_pos)
+ if fdist <= 6 then
+ return hp_change
+ end
+ end
+ end
+
+ return 0
+ end
+
+ return hp_change
+end, true)
+
local queue = {}
for name, def in pairs(minetest.registered_nodes) do
local mod, nodename = name:match"(..-):(.+)"
@@ -188,14 +238,14 @@ local chest_def = {
local inv = minetest.get_inventory({type = "node", pos = pos})
if not inv or inv:is_empty("main") then
- minetest.set_node(pos, {name = "air"})
- minetest.show_formspec(player:get_player_name(), "", player:get_inventory_formspec())
+ minetest.close_formspec(player:get_player_name(), "")
+ minetest.after(0, function()
+ minetest.set_node(pos, {name = "air"})
+ end)
end
- end,
- on_rightclick = function(pos, node, clicker, itemstack, pointed_thing)
minetest.swap_node(pos, {name = "ctf_map:chest_opened"})
minetest.get_meta(pos):set_string("infotext", chestv)
- end
+ end,
}
local ochest_def = table.copy(chest_def)
@@ -206,6 +256,20 @@ ochest_def.tiles[6] = "default_chest_inside.png"
ochest_def.mesh = "chest_open.obj"
ochest_def.light_source = 1
ochest_def.on_rightclick = nil
+ochest_def.on_metadata_inventory_take = function(pos, listname, index, stack, player)
+ minetest.log("action", string.format("%s takes %s from treasure chest at %s",
+ player:get_player_name(),
+ stack:to_string(),
+ minetest.pos_to_string(pos)
+ ))
+ local inv = minetest.get_inventory({type = "node", pos = pos})
+ if not inv or inv:is_empty("main") then
+ minetest.close_formspec(player:get_player_name(), "")
+ minetest.after(0, function()
+ minetest.set_node(pos, {name = "air"})
+ end)
+ end
+end
minetest.register_node("ctf_map:chest_opened", ochest_def)
minetest.register_node("ctf_map:chest", chest_def)
diff --git a/mods/ctf/ctf_map/textures/ctf_map_ind_lava.png b/mods/ctf/ctf_map/textures/ctf_map_ind_lava.png
new file mode 100644
index 000000000..fcc1b26f7
Binary files /dev/null and b/mods/ctf/ctf_map/textures/ctf_map_ind_lava.png differ
diff --git a/mods/ctf/ctf_map/textures/ctf_map_ind_water.png b/mods/ctf/ctf_map/textures/ctf_map_ind_water.png
new file mode 100644
index 000000000..088af19ab
Binary files /dev/null and b/mods/ctf/ctf_map/textures/ctf_map_ind_water.png differ
diff --git a/mods/ctf/ctf_map/textures/default_chest_front_blue.png b/mods/ctf/ctf_map/textures/default_chest_front_blue.png
deleted file mode 100644
index 3dabef4c1..000000000
Binary files a/mods/ctf/ctf_map/textures/default_chest_front_blue.png and /dev/null differ
diff --git a/mods/ctf/ctf_map/textures/default_chest_front_red.png b/mods/ctf/ctf_map/textures/default_chest_front_red.png
deleted file mode 100644
index 02305af6c..000000000
Binary files a/mods/ctf/ctf_map/textures/default_chest_front_red.png and /dev/null differ
diff --git a/mods/ctf/ctf_map/textures/default_chest_side_blue.png b/mods/ctf/ctf_map/textures/default_chest_side_blue.png
deleted file mode 100644
index 584b81b99..000000000
Binary files a/mods/ctf/ctf_map/textures/default_chest_side_blue.png and /dev/null differ
diff --git a/mods/ctf/ctf_map/textures/default_chest_side_red.png b/mods/ctf/ctf_map/textures/default_chest_side_red.png
deleted file mode 100644
index 066aa80e3..000000000
Binary files a/mods/ctf/ctf_map/textures/default_chest_side_red.png and /dev/null differ
diff --git a/mods/ctf/ctf_map/textures/default_chest_top_blue.png b/mods/ctf/ctf_map/textures/default_chest_top_blue.png
deleted file mode 100644
index 75ce5eb0d..000000000
Binary files a/mods/ctf/ctf_map/textures/default_chest_top_blue.png and /dev/null differ
diff --git a/mods/ctf/ctf_map/textures/default_chest_top_red.png b/mods/ctf/ctf_map/textures/default_chest_top_red.png
deleted file mode 100644
index 5bc699b57..000000000
Binary files a/mods/ctf/ctf_map/textures/default_chest_top_red.png and /dev/null differ
diff --git a/mods/ctf/ctf_modebase/bounties.lua b/mods/ctf/ctf_modebase/bounties.lua
index 8bffca3c1..1ebae4f4e 100644
--- a/mods/ctf/ctf_modebase/bounties.lua
+++ b/mods/ctf/ctf_modebase/bounties.lua
@@ -48,9 +48,9 @@ function ctf_modebase.bounties.claim(player, killer)
end
local rewards = bounties[pteam].rewards
- minetest.chat_send_all(minetest.colorize(CHAT_COLOR,
- string.format("[Bounty] %s killed %s and got %s", killer, player, get_reward_str(rewards))
- ))
+ local bounty_kill_text = string.format("[Bounty] %s killed %s and got %s", killer, player, get_reward_str(rewards))
+ minetest.chat_send_all(minetest.colorize(CHAT_COLOR, bounty_kill_text))
+ ctf_modebase.announce(bounty_kill_text)
bounties[pteam] = nil
return rewards
@@ -156,7 +156,7 @@ ctf_core.register_chatcommand_alias("list_bounties", "lb", {
)
table.insert(output, label)
- local model = "model[%d,1;4,6;player;character.b3d;%s;{0,160};;;]"
+ local model = "model[%d,1;4,6;player;character.b3d;%s,blank.png;{0,160};;;]"
model = string.format(
model,
x,
@@ -183,17 +183,28 @@ ctf_core.register_chatcommand_alias("put_bounty", "pb", {
privs = { ctf_admin = true },
func = function(name, param)
local player, amount = string.match(param, "(.*) (.*)")
- local pteam = ctf_teams.get(player)
- local team_colour = ctf_teams.team[pteam].color
- if not (player and pteam and amount) then
+
+ if not (player and amount) then
return false, "Incorrect parameters"
end
+
+ local pteam = ctf_teams.get(player)
+ if not pteam then
+ return false, "You can only put a bounty on a player in a team!"
+ end
+
+ local team_colour = ctf_teams.team[pteam].color
+
amount = ctf_core.to_number(amount)
- set(
- player,
- pteam,
- { bounty_kills=1, score=amount }
- )
- return true, "Successfully placed a bounty of " .. amount .. " on " .. minetest.colorize(team_colour, player) .. "!"
+ if amount then
+ set(
+ player,
+ pteam,
+ { bounty_kills=1, score=amount }
+ )
+ return true, "Successfully placed a bounty of " .. amount .. " on " .. minetest.colorize(team_colour, player) .. "!"
+ else
+ return false, "Invalid Amount"
+ end
end,
})
diff --git a/mods/ctf/ctf_modebase/bounty_algo.lua b/mods/ctf/ctf_modebase/bounty_algo.lua
index 43915bec8..f2bffcc76 100644
--- a/mods/ctf/ctf_modebase/bounty_algo.lua
+++ b/mods/ctf/ctf_modebase/bounty_algo.lua
@@ -6,13 +6,16 @@ function ctf_modebase.bounty_algo.kd.get_next_bounty(team_members)
local recent = ctf_modebase:get_current_mode().recent_rankings.players()
for _, pname in ipairs(team_members) do
- local kd = 0.1
- if recent[pname] then
- kd = math.max(kd, (recent[pname].kills or 0) / (recent[pname].deaths or 1))
+ local team = ctf_teams.get(pname)
+ if team and not ctf_teams.team[team].not_playing then
+ local kd = 0.1
+ if recent[pname] then
+ kd = math.max(kd, (recent[pname].kills or 0) / (recent[pname].deaths or 1))
+ end
+
+ table.insert(kd_list, kd)
+ sum = sum + kd
end
-
- table.insert(kd_list, kd)
- sum = sum + kd
end
local random = math.random() * sum
diff --git a/mods/ctf/ctf_modebase/build_timer.lua b/mods/ctf/ctf_modebase/build_timer.lua
index 600d5ba24..b5745c375 100644
--- a/mods/ctf/ctf_modebase/build_timer.lua
+++ b/mods/ctf/ctf_modebase/build_timer.lua
@@ -7,13 +7,12 @@ local timer = nil
ctf_modebase.build_timer = {}
local function timer_func(time_left)
- if time_left <= 0 then
- ctf_modebase.build_timer.finish()
- return
- end
-
for _, player in pairs(minetest.get_connected_players()) do
- local time_str = string.format("%dm %ds until match begins!", math.floor(time_left / 60), math.floor(time_left % 60))
+ local time_str = "Removing Barrier..."
+
+ if time_left > 0 then
+ time_str = string.format("%dm %ds until match begins!", math.floor(time_left / 60), math.floor(time_left % 60))
+ end
if not hud:exists(player, "build_timer") then
hud:add(player, "build_timer", {
@@ -30,7 +29,8 @@ local function timer_func(time_left)
end
local pteam = ctf_teams.get(player)
- if pteam and not ctf_core.pos_inside(player:get_pos(), ctf_teams.get_team_territory(pteam)) then
+ local tpos1, tpos2 = ctf_teams.get_team_territory(pteam)
+ if pteam and tpos1 and not ctf_core.pos_inside(player:get_pos(), tpos1, tpos2) then
hud_events.new(player, {
quick = true,
text = "You can't cross the barrier until build time is over!",
@@ -40,32 +40,54 @@ local function timer_func(time_left)
end
end
+ if time_left <= 0 then
+ ctf_modebase.build_timer.finish()
+ return
+ end
+
timer = minetest.after(1, timer_func, time_left - 1)
end
+function ctf_modebase.build_timer.start(build_time)
+ local time = build_time or ctf_modebase:get_current_mode().build_timer or DEFAULT_BUILD_TIME
+
+ if time > 0 then
+ if timer then timer:cancel() end
+ timer = timer_func(time)
+ end
+end
function ctf_modebase.build_timer.finish()
if timer == nil then return end
- timer:cancel()
- timer = nil
- hud:remove_all()
if ctf_map.current_map then
- minetest.sound_play("ctf_modebase_build_time_over", {
- gain = 1.0,
- pitch = 1.0,
- }, true)
+ ctf_map.remove_barrier(ctf_map.current_map, function()
+ if timer then
+ timer:cancel()
+ timer = nil
+ end
+
+ hud:remove_all()
+ local text = "Build time is over!"
+ minetest.chat_send_all(text)
+ ctf_modebase.announce(text)
+
+ ctf_modebase.on_match_start()
+
+ minetest.sound_play("ctf_modebase_build_time_over", {
+ gain = 1.0,
+ pitch = 1.0,
+ }, true)
+ end)
+ else
+ timer:cancel()
+ timer = nil
+ hud:remove_all()
- ctf_map.remove_barrier(ctf_map.current_map)
+ ctf_modebase.on_match_start()
end
-
- ctf_modebase.on_match_start()
end
-ctf_api.register_on_new_match(function()
- timer = minetest.after(1, timer_func, ctf_modebase:get_current_mode().build_timer or DEFAULT_BUILD_TIME)
-end)
-
ctf_api.register_on_match_end(function()
if timer == nil then return end
timer:cancel()
@@ -81,7 +103,9 @@ minetest.is_protected = function(pos, pname, ...)
local pteam = ctf_teams.get(pname)
- if pteam and not ctf_core.pos_inside(pos, ctf_teams.get_team_territory(pteam)) then
+ if pteam and ctf_teams.get_team_territory(pteam) and
+ not ctf_core.pos_inside(pos, ctf_teams.get_team_territory(pteam))
+ then
hud_events.new(pname, {
quick = true,
text = "You can't interact outside of your team territory during build time!",
diff --git a/mods/ctf/ctf_modebase/features.lua b/mods/ctf/ctf_modebase/features.lua
index f5dd62093..e309070c4 100644
--- a/mods/ctf/ctf_modebase/features.lua
+++ b/mods/ctf/ctf_modebase/features.lua
@@ -1,3 +1,172 @@
+local mapload_huds = mhud.init()
+local LOADING_SCREEN_TARGET_TIME = 7
+local loading_screen_time
+
+local function supports_observers(x)
+ if x then
+ if x.object then x = x.object end
+
+ if x.get_observers and x:get_pos() then
+ return true
+ end
+ end
+
+ return false
+end
+
+local function update_playertag(player, t, nametag, team_nametag, symbol_nametag)
+ if not supports_observers(nametag.object) or
+ not supports_observers(team_nametag.object) or
+ not supports_observers(symbol_nametag.object)
+ then
+ return
+ end
+
+ local entity_players = {}
+ local nametag_players = ctf_modebase.get_allowed_nametag_observers(player)
+ local symbol_players = {}
+ nametag_players[player:get_player_name()] = nil
+
+ for n, extra in pairs(table.copy(nametag_players)) do
+ local setting = extra
+
+ if setting == true then
+ setting = ctf_settings.get(minetest.get_player_by_name(n), "teammate_nametag_style")
+ end
+
+ if setting == "3" then
+ nametag_players[n] = nil
+ elseif setting == "2" then
+ symbol_players[n] = true
+ nametag_players[n] = nil
+ end
+ end
+
+ for k, v in ipairs(minetest.get_connected_players()) do
+ local n = v:get_player_name()
+ if not nametag_players[n] then
+ entity_players[n] = true
+ end
+ end
+
+ nametag.object:set_observers(entity_players )
+ team_nametag.object:set_observers(nametag_players)
+ symbol_nametag.object:set_observers(symbol_players )
+end
+
+local tags_hidden = false
+local update_timer = false
+function ctf_modebase.update_playertags(time)
+ if not update_timer and not tags_hidden then
+ update_timer = true
+ minetest.after(time or 1.2, function()
+ update_timer = false
+ for _, p in pairs(minetest.get_connected_players()) do
+ local t = ctf_teams.get(p)
+ local playertag = playertag.get(p)
+
+ if playertag then
+ local team_nametag = playertag.nametag_entity
+ local nametag = playertag.entity
+ local symbol_entity = playertag.symbol_entity
+
+ if t and nametag and team_nametag and symbol_entity then
+ update_playertag(p, t, nametag, team_nametag, symbol_entity)
+ end
+ end
+ end
+ end)
+ end
+end
+
+local PLAYERTAGS_OFF = false
+local PLAYERTAGS_ON = true
+local function set_playertags_state(state)
+ if state == PLAYERTAGS_ON and tags_hidden then
+ tags_hidden = false
+
+ ctf_modebase.update_playertags(0)
+ elseif state == PLAYERTAGS_OFF and not tags_hidden then
+ tags_hidden = true
+
+ for _, p in pairs(minetest.get_connected_players()) do
+ local playertag = playertag.get(p)
+
+ if ctf_teams.get(p) and playertag then
+ local team_nametag = playertag.nametag_entity
+ local nametag = playertag.entity
+ local symbol_entity = playertag.symbol_entity
+
+ if supports_observers(nametag) and supports_observers(team_nametag) and supports_observers(symbol_entity) then
+ team_nametag.object:set_observers({})
+ symbol_entity.object:set_observers({})
+ nametag.object:set_observers({})
+ end
+ end
+ end
+ end
+end
+
+local old_announce = ctf_modebase.map_chosen
+function ctf_modebase.map_chosen(map, ...)
+ set_playertags_state(PLAYERTAGS_OFF)
+
+ mapload_huds:clear_all()
+
+ for _, p in pairs(minetest.get_connected_players()) do
+ if ctf_teams.get(p) then
+ mapload_huds:add(p, "loading_screen", {
+ hud_elem_type = "image",
+ position = {x = 0.5, y = 0.5},
+ image_scale = -100,
+ z_index = 1000,
+ texture = "[combine:1x1^[invert:rgba^[opacity:1^[colorize:#141523:255"
+ })
+
+ mapload_huds:add(p, "map_image", {
+ hud_elem_type = "image",
+ position = {x = 0.5, y = 0.5},
+ image_scale = -100,
+ z_index = 1001,
+ texture = map.dirname.."_screenshot.png^[opacity:30",
+ })
+
+ mapload_huds:add(p, "loading_text", {
+ hud_elem_type = "text",
+ position = {x = 0.5, y = 0.5},
+ alignment = {x = "center", y = "up"},
+ text_scale = 2,
+ text = "Loading Map: " .. map.name .. "...",
+ color = 0x7ec5ff,
+ z_index = 1002,
+ })
+ mapload_huds:add(p, {
+ hud_elem_type = "text",
+ position = {x = 0.5, y = 0.75},
+ alignment = {x = "center", y = "center"},
+ text = random_messages.get_random_message(),
+ color = 0xffffff,
+ z_index = 1002,
+ })
+ end
+ end
+
+ loading_screen_time = minetest.get_us_time()
+
+ return old_announce(map, ...)
+end
+
+ctf_settings.register("teammate_nametag_style", {
+ type = "list",
+ description = "Controls what style of nametag to use for teammates.",
+ list = {"Minetest Nametag: Full", "Minetest Nametag: Symbol", "Entity Nametag"},
+ default = "1",
+ on_change = function(player, new_value)
+ minetest.log("action", "Player "..player:get_player_name().." changed their nametag setting")
+ ctf_modebase.update_playertags()
+ end
+})
+
ctf_modebase.features = function(rankings, recent_rankings)
local FLAG_MESSAGE_COLOR = "#d9b72a"
@@ -9,8 +178,27 @@ local teams_left
local function calculate_killscore(player)
local match_rank = recent_rankings.players()[player] or {}
local kd = (match_rank.kills or 1) / (match_rank.deaths or 1)
+ local flag_multiplier = 1
+ for tname, carrier in pairs(ctf_modebase.flag_taken) do
+ if carrier.p == player then
+ flag_multiplier = flag_multiplier * 2
+ end
+ end
- return math.max(1, math.round(kd * 7))
+ minetest.log("ACTION", string.format(
+ "[KILLDEBUG] { og = %f, kills = %d, assists = %f, deaths = %d, score = %f, hp_healed = %f, attempts = %d, " ..
+ "reward_given_to_enemy = %f },",
+ math.max(1, math.round(kd * 7 * flag_multiplier)),
+ match_rank.kills or 1,
+ match_rank.kill_assists or 0,
+ match_rank.deaths or 1,
+ match_rank.score or 0,
+ match_rank.hp_healed or 0,
+ match_rank.flag_attempts or 0,
+ match_rank.reward_given_to_enemy or 0
+ ))
+
+ return math.max(1, math.round(kd * 7 * flag_multiplier))
end
local damage_group_textures = {
@@ -70,14 +258,22 @@ local function tp_player_near_flag(player)
local tname = ctf_teams.get(player)
if not tname then return end
- local pos = vector.offset(ctf_map.current_map.teams[tname].flag_pos,
- math.random(-1, 1),
- 0.5,
- math.random(-1, 1)
- )
- local rotation_y = vector.dir_to_rotation(
- vector.direction(pos, ctf_map.current_map.teams[tname].look_pos or ctf_map.current_map.flag_center)
- ).y
+ local rotation_y
+ local pos
+
+ if ctf_map.current_map.teams[tname] then
+ pos = vector.offset(ctf_map.current_map.teams[tname].flag_pos,
+ math.random(-1, 1),
+ 0.5,
+ math.random(-1, 1)
+ )
+ rotation_y = vector.dir_to_rotation(
+ vector.direction(pos, ctf_map.current_map.teams[tname].look_pos or ctf_map.current_map.flag_center)
+ ).y
+ else
+ pos = vector.add(ctf_map.current_map.pos1, vector.divide(ctf_map.current_map.size, 2))
+ rotation_y = player:get_look_horizontal()
+ end
local function apply()
player:set_pos(pos)
@@ -99,19 +295,22 @@ local function celebrate_team(teamname)
for _, player in ipairs(minetest.get_connected_players()) do
local pname = player:get_player_name()
local pteam = ctf_teams.get(pname)
+ local volume = (tonumber(ctf_settings.get(player, "flag_sound_volume")) or 10.0) / 10
- if pteam == teamname then
- minetest.sound_play("ctf_modebase_trumpet_positive", {
- to_player = pname,
- gain = 1.0,
- pitch = 1.0,
- }, true)
- else
- minetest.sound_play("ctf_modebase_trumpet_negative", {
- to_player = pname,
- gain = 1.0,
- pitch = 1.0,
- }, true)
+ if volume > 0 then
+ if pteam == teamname then
+ minetest.sound_play("ctf_modebase_trumpet_positive", {
+ to_player = pname,
+ gain = volume,
+ pitch = 1.0,
+ }, true)
+ else
+ minetest.sound_play("ctf_modebase_trumpet_negative", {
+ to_player = pname,
+ gain = volume,
+ pitch = 1.0,
+ }, true)
+ end
end
end
end
@@ -120,18 +319,19 @@ local function drop_flag(teamname)
for _, player in ipairs(minetest.get_connected_players()) do
local pname = player:get_player_name()
local pteam = ctf_teams.get(pname)
+ local drop_volume = (tonumber(ctf_settings.get(player, "flag_sound_volume")) or 10.0) / 10
- if pteam then
+ if pteam and drop_volume > 0 then
if pteam == teamname then
minetest.sound_play("ctf_modebase_drop_flag_negative", {
to_player = pname,
- gain = 0.4,
+ gain = math.max(0.1, drop_volume - 0.5),
pitch = 1.0,
}, true)
else
minetest.sound_play("ctf_modebase_drop_flag_positive", {
to_player = pname,
- gain = 0.4,
+ gain = math.max(0.1, drop_volume - 0.5),
pitch = 1.0,
}, true)
end
@@ -142,60 +342,71 @@ end
local function end_combat_mode(player, reason, killer, weapon_image)
local comment = nil
- if reason == "combatlog" then
- killer, weapon_image = ctf_combat_mode.get_last_hitter(player)
- if killer then
- comment = " (Combat Log)"
- recent_rankings.add(player, {deaths = 1}, true)
- end
- else
- if reason ~= "punch" or killer == player then
- if ctf_teams.get(player) then
+ if ctf_teams.get(player) then
+ if reason == "combatlog" then
+ killer, weapon_image = ctf_combat_mode.get_last_hitter(player)
+ if killer then
+ comment = " (Combat Log)"
+ recent_rankings.add(player, {deaths = 1}, true)
+ end
+ else
+ if reason ~= "punch" or killer == player then
if reason == "punch" then
ctf_kill_list.add(player, player, weapon_image)
else
ctf_kill_list.add("", player, get_suicide_image(reason))
end
- end
- killer, weapon_image = ctf_combat_mode.get_last_hitter(player)
- comment = " (Suicide)"
+ killer, weapon_image = ctf_combat_mode.get_last_hitter(player)
+ comment = " (Suicide)"
+ end
+ recent_rankings.add(player, {deaths = 1}, true)
end
- recent_rankings.add(player, {deaths = 1}, true)
- end
- if killer then
- local killscore = calculate_killscore(player)
+ if killer then
+ local killscore = calculate_killscore(player)
+ local total_enemy_reward = 0
- local rewards = {kills = 1, score = killscore}
- local bounty = ctf_modebase.bounties.claim(player, killer)
+ local rewards = {kills = 1, score = killscore}
+ local bounty = ctf_modebase.bounties.claim(player, killer)
- if bounty then
- for name, amount in pairs(bounty) do
- rewards[name] = (rewards[name] or 0) + amount
+ if bounty then
+ for name, amount in pairs(bounty) do
+ rewards[name] = (rewards[name] or 0) + amount
+ end
end
- end
- recent_rankings.add(killer, rewards)
+ recent_rankings.add(killer, rewards)
+ total_enemy_reward = total_enemy_reward + rewards.score
- if ctf_teams.get(killer) then
- ctf_kill_list.add(killer, player, weapon_image, comment)
- end
+ if ctf_teams.get(killer) then
+ ctf_kill_list.add(killer, player, weapon_image, comment)
+ hud_events.new(player, {
+ quick = false,
+ text = killer.." killed you for ".. rewards.score .." points!",
+ color = "warning",
+ })
+ end
- -- share kill score with other hitters
- local hitters = ctf_combat_mode.get_other_hitters(player, killer)
- for _, pname in ipairs(hitters) do
- recent_rankings.add(pname, {kill_assists = 1, score = math.ceil(killscore / #hitters)})
- end
+ -- share kill score with other hitters
+ local hitters = ctf_combat_mode.get_other_hitters(player, killer)
+ for _, pname in ipairs(hitters) do
+ recent_rankings.add(pname, {kill_assists = 1, score = math.ceil(killscore / #hitters)})
+ total_enemy_reward = total_enemy_reward + math.ceil(killscore / #hitters)
+ end
- -- share kill score with healers
- local healers = ctf_combat_mode.get_healers(killer)
- for _, pname in ipairs(healers) do
- recent_rankings.add(pname, {score = math.ceil(killscore / #healers)})
- end
+ -- share kill score with healers
+ local healers = ctf_combat_mode.get_healers(killer)
+ for _, pname in ipairs(healers) do
+ recent_rankings.add(pname, {score = math.ceil(killscore / #healers)})
+ total_enemy_reward = total_enemy_reward + math.ceil(killscore / #healers)
+ end
+
+ recent_rankings.add(player, {reward_given_to_enemy = total_enemy_reward}, true)
- if ctf_combat_mode.is_only_hitter(killer, player) then
- ctf_combat_mode.set_kill_time(killer, 5)
+ if ctf_combat_mode.is_only_hitter(killer, player) then
+ ctf_combat_mode.set_kill_time(killer, 5)
+ end
end
end
@@ -239,8 +450,11 @@ local item_levels = {
}
local delete_queue = {}
+local team_switch_after_capture = false
return {
+ tp_player_near_flag = tp_player_near_flag,
+
on_new_match = function()
team_list = {}
for tname in pairs(ctf_map.current_map.teams) do
@@ -255,7 +469,7 @@ return {
map_treasures[k] = v
end
- if #delete_queue > 0 then
+ if #delete_queue > 0 and delete_queue._map ~= ctf_map.current_map.dirname then
local p1, p2 = unpack(delete_queue)
for _, object_drop in pairs(minetest.get_objects_in_area(p1, p2)) do
@@ -279,15 +493,33 @@ return {
ctf_modebase:get_current_mode().team_chest_items or {},
ctf_modebase:get_current_mode().blacklisted_nodes or {}
)
+
+ if loading_screen_time then
+ local total_time = (minetest.get_us_time() - loading_screen_time) / 1e6
+
+ minetest.after(math.max(0, LOADING_SCREEN_TARGET_TIME - total_time), function()
+ mapload_huds:clear_all()
+ set_playertags_state(PLAYERTAGS_ON)
+
+ ctf_modebase.build_timer.start()
+ end)
+ end
end,
on_match_end = function()
recent_rankings.on_match_end()
if ctf_map.current_map then
+ minetest.log("action",
+ "matchend: Match ended for map "..ctf_map.current_map.name..
+ " in mode "..(ctf_modebase.current_mode or "")..
+ ". Duration: "..ctf_map.get_duration()
+ )
-- Queue deletion for after the players have left
- delete_queue = {ctf_map.current_map.pos1, ctf_map.current_map.pos2}
+ delete_queue = {ctf_map.current_map.pos1, ctf_map.current_map.pos2, _map = ctf_map.current_map.dirname}
end
end,
+ -- If you set this in a mode def it will replace the call to ctf_teams.allocate_teams() in match.lua
+ -- allocate_teams = function()
team_allocator = function(player)
player = PlayerName(player)
@@ -297,21 +529,47 @@ return {
local worst_kd = nil
local best_players = nil
local worst_players = nil
+ local total_players = 0
for _, team in ipairs(team_list) do
local players_count = ctf_teams.online_players[team].count
+ local players = ctf_teams.online_players[team].players
+
+ local bk = 0
+ local bd = 1
+
+ for name in pairs(players) do
+ local rank = rankings:get(name)
+
+ if rank then
+ if bk <= (rank.kills or 0) then
+ bk = rank.kills or 0
+ bd = rank.deaths or 0
+ end
+ end
+ end
+
+ total_players = total_players + players_count
- local kd = 0.1
+ local kd = bk / bd
+ local match_kd = 0
+ local tk = 0
if team_scores[team] then
- kd = math.max(kd, (team_scores[team].kills or 0) / (team_scores[team].deaths or 1))
+ if (team_scores[team].score or 0) >= 50 then
+ tk = team_scores[team].kills or 0
+
+ kd = math.max(kd, (team_scores[team].kills or bk) / (team_scores[team].deaths or bd))
+ end
+
+ match_kd = (team_scores[team].kills or 0) / (team_scores[team].deaths or 1)
end
- if not best_kd or kd > best_kd.s then
- best_kd = {s = kd, t = team}
+ if not best_kd or match_kd > best_kd.a then
+ best_kd = {s = kd, a = match_kd, t = team, kills = tk}
end
- if not worst_kd or kd < worst_kd.s then
- worst_kd = {s = kd, t = team}
+ if not worst_kd or match_kd < worst_kd.a then
+ worst_kd = {s = kd, a = match_kd, t = team, kills = tk}
end
if not best_players or players_count > best_players.s then
@@ -323,21 +581,49 @@ return {
end
end
+ if worst_players.s == 0 then
+ return worst_players.t
+ end
+
local kd_diff = best_kd.s - worst_kd.s
+ local actual_kd_diff = best_kd.a - worst_kd.a
local players_diff = best_players.s - worst_players.s
- local remembered_team = ctf_teams.get(player)
+ local rem_team = ctf_teams.get(player)
+ local player_rankings = recent_rankings.get(player) --[pteam.."_score"]
- if worst_players.s == 0 then
- return worst_players.t
+ if not rem_team or
+ math.max(player_rankings[rem_team.."_kills"] or 0, player_rankings[rem_team.."_deaths"] or 0) <= 6 then
+ player_rankings = rankings:get(player) or {}
+ else
+ player_rankings.kills = player_rankings[rem_team.."_kills"] or 0
+ player_rankings.deaths = player_rankings[rem_team.."_deaths"] or 1
end
- -- Allocate player to remembered team unless they're desperately needed in the other
- if remembered_team and not ctf_modebase.flag_captured[remembered_team] and kd_diff <= 0.6 and players_diff < 5 then
- return remembered_team
+ local one_third = math.ceil(0.34 * total_players)
+
+ -- Allocate player to remembered team unless teams are imbalanced
+ if rem_team and not ctf_modebase.flag_captured[rem_team] and
+ (worst_kd.kills <= total_players or actual_kd_diff <= 0.8) and players_diff <= one_third then
+ return rem_team
end
- if players_diff == 0 or (kd_diff > 0.4 and players_diff < 2) then
+ local pkd = (player_rankings.kills or 0) / (player_rankings.deaths or 1)
+
+ local one_fourth = math.ceil(0.25 * total_players)
+ local avg = (kd_diff + actual_kd_diff) / 2
+ local pcount_diff_limit = (
+ (players_diff <= math.min(one_fourth, 1)) or
+ (pkd >= 1.8 and players_diff <= math.min(one_third, 2))
+ )
+ if best_kd.kills + worst_kd.kills >= 30 then
+ avg = actual_kd_diff
+ end
+
+ local result = pcount_diff_limit and ((best_kd.kills + worst_kd.kills >= 30 and best_kd.t == best_players.t) or
+ (pkd >= math.min(1, kd_diff/2) and avg >= 0.4))
+
+ if players_diff == 0 or result then
return worst_kd.t
else
return worst_players.t
@@ -371,7 +657,10 @@ return {
celebrate_team(ctf_teams.get(pname))
- recent_rankings.add(pname, {score = 30, flag_attempts = 1})
+ recent_rankings.add(pname, {
+ score = ctf_teams.online_players[teamname].count * 3 * #ctf_modebase.taken_flags[pname],
+ flag_attempts = 1
+ })
ctf_modebase.flag_huds.track_capturer(pname, FLAG_CAPTURE_TIMER)
end,
@@ -393,6 +682,11 @@ return {
ctf_modebase.flag_huds.untrack_capturer(pname)
playertag.set(player, playertag.TYPE_ENTITY)
+
+ if player.set_observers then
+ ctf_modebase.update_playertags()
+ end
+
drop_flag(pteam)
end,
on_flag_capture = function(player, teamnames)
@@ -401,26 +695,81 @@ return {
local tcolor = ctf_teams.team[pteam].color
playertag.set(player, playertag.TYPE_ENTITY)
- celebrate_team(pteam)
- local text = " has captured the flag"
- if many_teams then
- text = " has captured the flag of team(s) " .. HumanReadable(teamnames)
- minetest.chat_send_all(
- minetest.colorize(tcolor, pname) ..
- minetest.colorize(FLAG_MESSAGE_COLOR, text)
- )
+ if player.set_observers then
+ ctf_modebase.update_playertags()
end
- ctf_modebase.announce(string.format("Player %s (team %s)%s", pname, pteam, text))
+
+ celebrate_team(pteam)
ctf_modebase.flag_huds.untrack_capturer(pname)
local team_scores = recent_rankings.teams()
+ local player_scores = recent_rankings.players()
local capture_reward = 0
for _, lost_team in ipairs(teamnames) do
- local score = ((team_scores[lost_team] or {}).score or 0) / 4
- score = math.max(75, math.min(500, score))
- capture_reward = capture_reward + score
+ local team_score = 0
+
+ for n in pairs(ctf_teams.online_players[lost_team].players) do
+ team_score = team_score + (player_scores[n].score or 0)
+ end
+
+ local score = team_score / math.max(1, ctf_teams.online_players[lost_team].count)
+ score = math.max(
+ 8 * ((player_scores[pname].flag_attempts or 0) + math.min(
+ (os.time() - ctf_map.start_time) / 60,
+ 10
+ )),
+ score * (2.4 + ctf_teams.online_players[lost_team].count/30)
+ )
+
+ score = math.max(score, #ctf_teams.get_connected_players() * 1.4)
+
+ capture_reward = capture_reward + math.min(score, 800)
+
+ minetest.log("action", string.format(
+ "[CAPDEBUG] div: %.1f {team_score = %d, capture_score = %d, connected_players = %d, lost_team_count = %d, "..
+ "player_attempts = %d, time = %d, winteam_score = %d, \"%s\"},",
+ team_score / score,
+ team_score,
+ score,
+ #ctf_teams.get_connected_players(),
+ ctf_teams.online_players[lost_team].count,
+ player_scores[pname].flag_attempts or 0,
+ os.time() - ctf_map.start_time,
+ team_scores[pteam].score or 0,
+ many_teams and "many teams" or "2 teams"
+ ))
+ end
+
+ local text = string.format(" has captured the flag and got %d points", capture_reward)
+ if many_teams then
+ text = string.format(
+ " has captured the flag of team(s) %s and got %d points",
+ HumanReadable(teamnames),
+ capture_reward
+ )
+ end
+
+ minetest.chat_send_all(
+ minetest.colorize(tcolor, pname) .. minetest.colorize(FLAG_MESSAGE_COLOR, text)
+ )
+
+ ctf_modebase.announce(string.format("Player %s (team %s)%s", pname, pteam, text))
+
+ local team_score = team_scores[pteam].score
+ local healers = ctf_combat_mode.get_healers(pname)
+ for teammate in pairs(ctf_teams.online_players[pteam].players) do
+ if teammate ~= pname then
+ local teammate_value = (recent_rankings.get(teammate)[pteam.."_score"] or 0) / (team_score or 1)
+
+ if table.indexof(healers, teammate) ~= -1 then
+ teammate_value = teammate_value + ((#ctf_teams.get_connected_players() / 10) / #healers)
+ end
+
+ local victory_bonus = math.max(5, math.min(capture_reward / 2, capture_reward * teammate_value))
+ recent_rankings.add(teammate, {score = victory_bonus}, true)
+ end
end
recent_rankings.add(pname, {score = capture_reward, flag_captures = #teamnames})
@@ -428,12 +777,12 @@ return {
teams_left = teams_left - #teamnames
if teams_left <= 1 then
- local capture_text = "Player %s captured"
+ local capture_text = "Player %s captured and got %d points"
if many_teams then
- capture_text = "Player %s captured the last flag"
+ capture_text = "Player %s captured the last flag and got %d points"
end
- ctf_modebase.summary.set_winner(string.format(capture_text, minetest.colorize(tcolor, pname)))
+ ctf_modebase.summary.set_winner(string.format(capture_text, minetest.colorize(tcolor, pname), capture_reward))
local win_text = HumanReadable(pteam) .. " Team Wins!"
@@ -452,7 +801,9 @@ return {
table.remove(team_list, table.indexof(team_list, lost_team))
for lost_player in pairs(ctf_teams.online_players[lost_team].players) do
+ team_switch_after_capture = true
ctf_teams.allocate_player(lost_player)
+ team_switch_after_capture = false
end
end
end
@@ -460,21 +811,27 @@ return {
on_allocplayer = function(player, new_team)
player:set_hp(player:get_properties().hp_max)
- ctf_modebase.update_wear.cancel_player_updates(player)
+ if not team_switch_after_capture then
+ ctf_modebase.update_wear.cancel_player_updates(player)
- ctf_modebase.player.remove_bound_items(player)
- ctf_modebase.player.give_initial_stuff(player)
+ ctf_modebase.player.remove_bound_items(player)
+ ctf_modebase.player.give_initial_stuff(player)
+ end
local tcolor = ctf_teams.team[new_team].color
player:hud_set_hotbar_image("gui_hotbar.png^[colorize:" .. tcolor .. ":128")
player:hud_set_hotbar_selected_image("gui_hotbar_selected.png^[multiply:" .. tcolor)
- player:set_properties({textures = {ctf_cosmetics.get_skin(player)}})
+ player_api.set_texture(player, 1, ctf_cosmetics.get_skin(player))
recent_rankings.set_team(player, new_team)
playertag.set(player, playertag.TYPE_ENTITY)
+ if player.set_observers then
+ ctf_modebase.update_playertags()
+ end
+
tp_player_near_flag(player)
end,
on_leaveplayer = function(player)
@@ -507,12 +864,27 @@ return {
end,
get_chest_access = function(pname)
local rank = rankings:get(pname)
+ local player = minetest.get_player_by_name(pname)
+ local pro_chest = player and player:get_meta():get_int("ctf_rankings:pro_chest:"..
+ (ctf_modebase.current_mode or "")) >= 1
local deny_pro = "You need to have more than 1.4 kills per death, "..
- "5 captures, and at least 8,000 score to access the pro section"
+ "5 captures, and at least 8,000 score to access the pro section."
+ if rank then
+ local captures_needed = math.max(0, 5 - (rank.flag_captures or 0))
+ local score_needed = math.floor(math.max(0, 8000 - (rank.score or 0)))
+ local current_kd = math.floor((rank.kills or 0) / (rank.deaths or 1) * 10)
+ current_kd = current_kd / 10
+ deny_pro = deny_pro .. " You still need " .. captures_needed
+ .. " captures, " .. score_needed ..
+ " score, and your kills per death is " ..
+ current_kd .. "."
+ end
-- Remember to update /make_pro in ranking_commands.lua if you change anything here
- if rank then
+ if pro_chest or rank then
if
+ pro_chest
+ or
(rank.score or 0) >= 8000 and
(rank.kills or 0) / (rank.deaths or 1) >= 1.4 and
(rank.flag_captures or 0) >= 5
@@ -584,7 +956,7 @@ return {
end
end,
sword = function(item)
- local mod, match = item:get_name():match("(%a+):sword_(%a+)")
+ local mod, match = item:get_name():match("([^:]+):sword_(%a+)")
if mod and (mod == "default" or mod == "ctf_melee") and match then
return table.indexof(item_levels, match)
diff --git a/mods/ctf/ctf_modebase/flags/huds.lua b/mods/ctf/ctf_modebase/flags/huds.lua
index 396b57cd8..6f3b50707 100644
--- a/mods/ctf/ctf_modebase/flags/huds.lua
+++ b/mods/ctf/ctf_modebase/flags/huds.lua
@@ -1,12 +1,12 @@
local hud = mhud.init()
-local FLAG_SAFE = {color = 0xFFFFFF, text = "Punch the enemy flags! Protect your flag!" }
-local FLAG_STOLEN = {color = 0xFF0000, text = "Kill %s, they've got your flag!" }
-local FLAG_STOLEN_YOU = {color = 0xFF0000, text = "You've got a flag! Run back and punch your flag!" }
-local FLAG_STOLEN_TEAMMATE = {color = 0x22BB22, text = "Protect teammates %s! They have the enemy flag!" }
-local BOTH_FLAGS_STOLEN = {color = 0xFF0000, text = "Kill %s to allow teammates %s to capture the flag!" }
-local BOTH_FLAGS_STOLEN_YOU = {color = 0xFF0000, text = "You can't capture that flag until %s is killed!" }
-local OTHER_FLAG_STOLEN = {color = 0xAA00FF, text = "Kill %s, they've got some flags!" }
+local FLAG_SAFE = {color = 0xFFFFFF, text = "Punch the enemy flags! Protect your flag!" }
+local FLAG_STOLEN = {color = 0xFF0000, text = "Kill %s, they've got your flag!" }
+local FLAG_STOLEN_YOU = {color = 0x22BB22, text = "You've got a flag! Run back and punch your flag!" }
+local FLAG_STOLEN_TEAMMATE = {color = 0x22BB22, text = "Protect teammates %s! They have the enemy flag!" }
+local BOTH_FLAGS_STOLEN = {color = 0xFF0000, text = "Kill %s to allow teammates %s to capture the flag!"}
+local BOTH_FLAGS_STOLEN_YOU = {color = 0xFF0000, text = "You can't capture that flag until %s is killed!" }
+local OTHER_FLAG_STOLEN = {color = 0xAA00FF, text = "Kill %s, they've got some flags!" }
ctf_modebase.flag_huds = {}
@@ -77,9 +77,24 @@ local function get_flag_status(you)
end
function ctf_modebase.flag_huds.update_player(player)
+ local team = ctf_teams.get(player)
+
+ if not team or (ctf_teams.team[team] and ctf_teams.team[team].not_playing) then
+ hud:clear(player)
+ return
+ end
+
local flag_status = get_flag_status(player:get_player_name())
if hud:exists(player, "flag_status") then
+ if hud:get(player, "flag_status").def.text ~= flag_status.text then
+ hud_events.new(player, {
+ text = flag_status.text,
+ color = flag_status.color,
+ channel = 2,
+ })
+ end
+
hud:change(player, "flag_status", flag_status)
else
hud:add(player, "flag_status", {
@@ -87,8 +102,15 @@ function ctf_modebase.flag_huds.update_player(player)
position = {x = 1, y = 0},
offset = {x = -6, y = 6},
alignment = {x = "left", y = "down"},
+ text = flag_status.text,
+ text_scale = 1,
+ color = flag_status.color,
+ })
+
+ hud_events.new(player, {
text = flag_status.text,
color = flag_status.color,
+ channel = 2
})
end
@@ -129,6 +151,7 @@ local function update_timer(pname)
if timeleft <= 1 then
ctf_modebase.drop_flags(minetest.get_player_by_name(pname))
+ ctf_modebase:get_current_mode().recent_rankings.add(pname, {score = 30})
else
player_timers[pname] = timeleft - 1
diff --git a/mods/ctf/ctf_modebase/flags/nodes.lua b/mods/ctf/ctf_modebase/flags/nodes.lua
index 0965b7eb5..33c31b0f0 100644
--- a/mods/ctf/ctf_modebase/flags/nodes.lua
+++ b/mods/ctf/ctf_modebase/flags/nodes.lua
@@ -21,7 +21,7 @@ minetest.register_node("ctf_modebase:flag", {
paramtype = "light",
paramtype2 = "facedir",
walkable = false,
- light_source = 5,
+ light_source = 6,
tiles = {
"default_wood.png",
"default_wood.png",
@@ -33,7 +33,7 @@ minetest.register_node("ctf_modebase:flag", {
node_box = {
type = "fixed",
fixed = {
- {0.250000,-0.500000,0.000000,0.312500,0.500000,0.062500}
+ {0.25,-0.5000,0.0000,0.3625,0.5000,0.1125}
}
},
groups = {immortal=1,is_flag=1,flag_bottom=1,not_in_creative_inventory=1},
@@ -65,7 +65,7 @@ for name, def in pairs(ctf_teams.team) do
paramtype2 = "facedir",
walkable = false,
buildable_to = false,
- light_source = 5,
+ light_source = 6,
tiles = {
"default_wood.png",
"default_wood.png",
@@ -77,8 +77,8 @@ for name, def in pairs(ctf_teams.team) do
node_box = {
type = "fixed",
fixed = {
- {0.250000,-0.500000,0.000000,0.312500,0.500000,0.062500},
- {-0.5,0,0.000000,0.250000,0.500000,0.062500}
+ {0.25,-0.5000,0.0000,0.3625,0.5000,0.1125},
+ {-0.5,0,0.0500,0.2500,0.5000,0.0625}
}
},
groups = {immortal=1,is_flag=1,flag_top=1,not_in_creative_inventory=1,[name]=1},
@@ -115,7 +115,7 @@ minetest.register_node("ctf_modebase:flag_captured_top",{
node_box = {
type = "fixed",
fixed = {
- {0.250000,-0.500000,0.000000,0.312500,0.500000,0.062500}
+ {0.25,-0.5000,0.0000,0.3625,0.5000,0.1125}
}
},
groups = {immortal=1,is_flag=1,flag_top=1,not_in_creative_inventory=1},
diff --git a/mods/ctf/ctf_modebase/flags/taking.lua b/mods/ctf/ctf_modebase/flags/taking.lua
index 01408b0fb..c427e6f5b 100644
--- a/mods/ctf/ctf_modebase/flags/taking.lua
+++ b/mods/ctf/ctf_modebase/flags/taking.lua
@@ -21,6 +21,10 @@ local function drop_flags(player, pteam)
end
end
+ if player_api.players[pname] then
+ player_api.set_texture(player, 2, "blank.png")
+ end
+
ctf_modebase.taken_flags[pname] = nil
ctf_modebase.skip_vote.on_flag_drop(#flagteams)
@@ -74,6 +78,10 @@ function ctf_modebase.flag_on_punch(puncher, nodepos, node)
table.insert(ctf_modebase.taken_flags[pname], target_team)
ctf_modebase.flag_taken[target_team] = {p=pname, t=pteam}
+ player_api.set_texture(puncher, 2,
+ "default_wood.png^([combine:16x16:4,0=wool_white.png^[colorize:"..ctf_teams.team[target_team].color..":200)"
+ )
+
ctf_modebase.skip_vote.on_flag_take()
ctf_modebase:get_current_mode().on_flag_take(puncher, target_team)
@@ -96,6 +104,8 @@ function ctf_modebase.flag_on_punch(puncher, nodepos, node)
ctf_modebase.flag_captured[flagteam] = true
end
+ player_api.set_texture(puncher, 2, "blank.png")
+
ctf_modebase.on_flag_capture(puncher, flagteams)
ctf_modebase.skip_vote.on_flag_capture(#flagteams)
@@ -105,6 +115,10 @@ function ctf_modebase.flag_on_punch(puncher, nodepos, node)
end
ctf_api.register_on_match_end(function()
+ for pname in pairs(ctf_modebase.taken_flags) do
+ player_api.set_texture(minetest.get_player_by_name(pname), 2, "blank.png")
+ end
+
ctf_modebase.taken_flags = {}
ctf_modebase.flag_taken = {}
ctf_modebase.flag_captured = {}
diff --git a/mods/ctf/ctf_modebase/immunity.lua b/mods/ctf/ctf_modebase/immunity.lua
index c20ef1348..3d3eea949 100644
--- a/mods/ctf/ctf_modebase/immunity.lua
+++ b/mods/ctf/ctf_modebase/immunity.lua
@@ -64,7 +64,8 @@ function ctf_modebase.give_immunity(player, respawn_timer)
})
if old == nil then
- player:set_properties({pointable = false, textures = {ctf_cosmetics.get_skin(player)}})
+ player_api.set_texture(player, 1, ctf_cosmetics.get_skin(player))
+ player:set_properties({pointable = false})
player:set_armor_groups({fleshy = 0})
end
end
@@ -85,7 +86,11 @@ function ctf_modebase.remove_immunity(player)
immune_players[pname] = nil
- player:set_properties({pointable = true, textures = {ctf_cosmetics.get_skin(player)}})
+ if player_api.players[pname] then
+ player_api.set_texture(player, 1, ctf_cosmetics.get_skin(player))
+ end
+
+ player:set_properties({pointable = true})
player:set_armor_groups({fleshy = 100})
end
@@ -103,7 +108,11 @@ function ctf_modebase.remove_respawn_immunity(player)
minetest.delete_particlespawner(old.particles, pname)
- player:set_properties({pointable = true, textures = {ctf_cosmetics.get_skin(player)}})
+ if player_api.players[pname] then
+ player_api.set_texture(player, 1, ctf_cosmetics.get_skin(player))
+ end
+
+ player:set_properties({pointable = true})
player:set_armor_groups({fleshy = 100})
return true
diff --git a/mods/ctf/ctf_modebase/init.lua b/mods/ctf/ctf_modebase/init.lua
index a50d3960e..359f28dfc 100644
--- a/mods/ctf/ctf_modebase/init.lua
+++ b/mods/ctf/ctf_modebase/init.lua
@@ -2,7 +2,9 @@ ctf_modebase = {
-- Table containing all registered modes and their definitions
modes = {}, ---@type table
- -- Same as ctf_modebase.modes but in list form
+ -- Same as ctf_modebase.modes but in list form.
+ --
+ -- Exception: Disabled modes that show up in ctf_modebase.modes won't show up in the modelist
modelist = {}, ---@type list
-- Name of the mode currently being played. On server start this will be false
@@ -36,11 +38,24 @@ ctf_modebase = {
--flag_captured[Team name] = true if captured, otherwise nil
flag_captured = {},
+
+ -- Choose who can see a player's nametag, defaults to their teammates
+ --
+ -- return {playername = , playername2 = , ...}
+ --
+ -- x: `"1"` for full nametag, `"2"` for symbol, or `true` to use the player setting for it
+ get_allowed_nametag_observers = function(player)
+ local pteam = ctf_teams.get(player)
+
+ return table.copy(ctf_teams.online_players[pteam].players)
+ end
}
ctf_gui.old_init()
+-- Can be added to by other mods, like irc
function ctf_modebase.announce(msg)
+ minetest.log("action", msg)
end
ctf_core.include_files(
@@ -72,21 +87,75 @@ ctf_core.include_files(
minetest.register_on_mods_loaded(function()
table.sort(ctf_modebase.modelist)
- if ctf_core.settings.server_mode == "play" then
- minetest.after(1, ctf_modebase.start_new_match)
+
+ if ctf_rankings.do_reset then
+ minetest.register_on_joinplayer(function(player)
+ skybox.clear(player)
+
+ player:set_moon({
+ visible = false
+ })
+ player:set_sun({
+ visible = false
+ })
+ player:set_clouds({
+ density = 0
+ })
+
+ player:hud_set_flags({
+ crosshair = false,
+ wielditem = false,
+ })
+
+ player:override_day_night_ratio(0)
+
+ player:set_hp(20)
+ player:set_nametag_attributes({color = {a = 0, r = 255, g = 255, b = 255}, text = ""})
+ end)
+ elseif ctf_core.settings.server_mode == "play" then
+ minetest.chat_send_all("[CTF] Sorting rankings...")
+ local function check()
+ if not ctf_rankings:rankings_sorted() then
+ return minetest.after(1, check)
+ end
+
+ minetest.chat_send_all("[CTF] Rank sorting done. Starting new match...")
+ ctf_modebase.start_new_match()
+ end
+
+ check()
end
for _, name in pairs(ctf_modebase.modelist) do
- ctf_settings.register("ctf_modebase:default_vote_"..name, {
- type = "list",
- description = "Match count vote for the mode '"..HumanReadable(name).."'",
- list = {HumanReadable(name).." - Ask", "0", "1", "2", "3", "4", "5"},
- _list_map = {"ask", 0, 1, 2, 3, 4, 5},
- default = "1", -- "Ask"
- })
+ if not ctf_modebase.modes[name].rounds then
+ ctf_settings.register("ctf_modebase:default_vote_"..name, {
+ type = "list",
+ description = "Match count vote for the mode '"..HumanReadable(name).."'",
+ list = {HumanReadable(name).." - Ask", "0", "1", "2", "3", "4", "5"},
+ _list_map = {"ask", 0, 1, 2, 3, 4, 5},
+ default = "1", -- "Ask"
+ })
+ end
end
end)
minetest.override_chatcommand("pulverize", {
privs = {creative = true},
})
+
+minetest.register_chatcommand("mode", {
+ description = "Prints the current mode and matches played",
+ func = function()
+ local mode = ctf_modebase.current_mode
+
+ if not mode then
+ return false, "The game isn't running"
+ end
+
+ return true, string.format("The current mode is %s. Matches finished: %d/%d",
+ HumanReadable(ctf_modebase.current_mode),
+ ctf_modebase.current_mode_matches_played-1,
+ ctf_modebase.current_mode_matches
+ )
+ end
+})
diff --git a/mods/ctf/ctf_modebase/map_catalog.lua b/mods/ctf/ctf_modebase/map_catalog.lua
index 3d198e8bb..02c80bc40 100644
--- a/mods/ctf/ctf_modebase/map_catalog.lua
+++ b/mods/ctf/ctf_modebase/map_catalog.lua
@@ -11,10 +11,9 @@ local used_maps_idx = 1
local map_repeat_interval
local function init()
- local maps = minetest.get_dir_list(ctf_map.maps_dir, true)
- table.sort(maps)
+ table.sort(ctf_map.registered_maps)
- for i, dirname in ipairs(maps) do
+ for i, dirname in ipairs(ctf_map.registered_maps) do
local map = ctf_map.load_map_meta(i, dirname)
if map.map_version and map.enabled then
table.insert(ctf_modebase.map_catalog.maps, map)
@@ -30,30 +29,43 @@ local function init()
map_repeat_interval = math.floor(#ctf_modebase.map_catalog.maps / 2)
end
-init()
-assert(#ctf_modebase.map_catalog.maps > 0 or ctf_core.settings.server_mode == "mapedit")
+minetest.register_on_mods_loaded(function()
+ init()
+ assert(#ctf_modebase.map_catalog.maps > 0 or ctf_core.settings.server_mode == "mapedit")
+end)
-function ctf_modebase.map_catalog.select_map(filter)
+function ctf_modebase.map_catalog.select_map(filter, full_pool)
local maps = {}
- for idx, map in ipairs(maps_pool) do
- if not filter or filter(ctf_modebase.map_catalog.maps[map]) then
- table.insert(maps, idx)
+ for _, pool in pairs({maps_pool, full_pool and used_maps}) do
+ for idx, map in ipairs(pool) do
+ if not filter or filter(ctf_modebase.map_catalog.maps[map]) then
+ table.insert(maps, full_pool and map or idx)
+ end
end
end
local selected = maps[math.random(1, #maps)]
- ctf_modebase.map_catalog.current_map = maps_pool[selected]
- if map_repeat_interval > 0 then
- if #used_maps < map_repeat_interval then
- table.insert(used_maps, maps_pool[selected])
- maps_pool[selected] = maps_pool[#maps_pool]
- maps_pool[#maps_pool] = nil
- else
- used_maps[used_maps_idx], maps_pool[selected] = maps_pool[selected], used_maps[used_maps_idx]
- used_maps_idx = used_maps_idx + 1
- if used_maps_idx > #used_maps then
- used_maps_idx = 1
+ if not selected then
+ selected = ctf_modebase.map_catalog.map_dirnames["plains"]
+ end
+
+ if full_pool then
+ ctf_modebase.map_catalog.current_map = selected
+ else
+ ctf_modebase.map_catalog.current_map = maps_pool[selected]
+
+ if map_repeat_interval > 0 then
+ if #used_maps < map_repeat_interval then
+ table.insert(used_maps, maps_pool[selected])
+ maps_pool[selected] = maps_pool[#maps_pool]
+ maps_pool[#maps_pool] = nil
+ else
+ used_maps[used_maps_idx], maps_pool[selected] = maps_pool[selected], used_maps[used_maps_idx]
+ used_maps_idx = used_maps_idx + 1
+ if used_maps_idx > #used_maps then
+ used_maps_idx = 1
+ end
end
end
end
diff --git a/mods/ctf/ctf_modebase/map_catalog_show.lua b/mods/ctf/ctf_modebase/map_catalog_show.lua
index 5f4d2387a..4186fd1af 100644
--- a/mods/ctf/ctf_modebase/map_catalog_show.lua
+++ b/mods/ctf/ctf_modebase/map_catalog_show.lua
@@ -31,7 +31,7 @@ local function show_catalog(pname, current_map)
}
}
- local y = 1
+ local y = 0.7
if current_map_meta.author and current_map_meta.author ~= "" then
formspec.elements.author = {
@@ -105,7 +105,8 @@ local function show_catalog(pname, current_map)
formspec.elements.previous = {
type = "button",
label = "<<",
- pos = {1, ctf_gui.FORM_SIZE.y - ctf_gui.ELEM_SIZE.y - 2.5},
+ pos = {1, ctf_gui.FORM_SIZE.y - ctf_gui.ELEM_SIZE.y - 11.8},
+ size = {5, 0.5},
func = function()
show_catalog(pname, current_map - 1)
end,
@@ -116,7 +117,8 @@ local function show_catalog(pname, current_map)
formspec.elements.next = {
type = "button",
label = ">>",
- pos = {5, ctf_gui.FORM_SIZE.y - ctf_gui.ELEM_SIZE.y - 2.5},
+ pos = {1, ctf_gui.FORM_SIZE.y - ctf_gui.ELEM_SIZE.y - 3.3},
+ size = {5, 0.5},
func = function()
show_catalog(pname, current_map + 1)
end,
@@ -128,7 +130,8 @@ local function show_catalog(pname, current_map)
type = "button",
exit = true,
label = "Skip to map",
- pos = {9, ctf_gui.FORM_SIZE.y - ctf_gui.ELEM_SIZE.y - 2.5},
+ pos = {1, ctf_gui.FORM_SIZE.y - ctf_gui.ELEM_SIZE.y - 2.2},
+ size = {2.5, 1},
func = function()
local mapname = ctf_modebase.map_catalog.maps[current_map].dirname
minetest.log("action", string.format("[ctf_admin] %s skipped to new map %s", pname, mapname))
@@ -143,7 +146,8 @@ local function show_catalog(pname, current_map)
formspec.elements.set_as_next_map = {
type = "button",
label = "Set as next map",
- pos = {13, ctf_gui.FORM_SIZE.y - ctf_gui.ELEM_SIZE.y - 2.5},
+ pos = {3.5, ctf_gui.FORM_SIZE.y - ctf_gui.ELEM_SIZE.y - 2.2},
+ size = {2.5, 1},
func = function()
local mapname = ctf_modebase.map_catalog.maps[current_map].dirname
minetest.log("action", string.format("[ctf_admin] %s set new map %s", pname, mapname))
diff --git a/mods/ctf/ctf_modebase/markers.lua b/mods/ctf/ctf_modebase/markers.lua
index 5652f7086..0217f4815 100644
--- a/mods/ctf/ctf_modebase/markers.lua
+++ b/mods/ctf/ctf_modebase/markers.lua
@@ -13,7 +13,7 @@ ctf_settings.register("prevent_marker_placement", {
type = "bool",
label = "Prevent automatic marker placement while sniping",
description = "Prevent placement of markers while holding ranged weapons,\nthis exludes the shotgun and pistol.",
- default = true
+ default = "true"
})
local hud = mhud.init()
@@ -60,6 +60,31 @@ local function add_marker(pname, pteam, message, pos, owner)
end
end
+local function check_pointed_entity(pointed, message)
+ local concat
+ local obj = pointed.ref
+ local entity = obj:get_luaentity()
+ -- If object is a player, append player name to display text
+ -- Else if obj is item entity, append item description and count to str.
+ if obj:is_player() then
+ concat = obj:get_player_name()
+ elseif entity then
+ if entity.name == "__builtin:item" then
+ local stack = ItemStack(entity.itemstring)
+ local itemdef = minetest.registered_items[stack:get_name()]
+ -- Fallback to itemstring if description doesn't exist
+ -- Only use first line of itemstring
+ concat = string.match(itemdef.description or entity.itemstring, "^([^\n]+)")
+ concat = concat .. " " .. stack:get_count()
+ end
+ end
+ local pos = obj:get_pos()
+ if concat then
+ message = message .. " <" .. concat .. ">"
+ end
+ return message, pos
+end
+
function ctf_modebase.markers.remove(pname, no_notify)
if markers[pname] then
markers[pname].timer:cancel()
@@ -82,7 +107,7 @@ function ctf_modebase.markers.add(pname, msg, pos, no_notify, specific_player)
if not ctf_modebase.in_game then return end
local pteam = ctf_teams.get(pname)
- if not pteam then return end
+ if not pteam or ctf_teams.team[pteam].not_playing then return end
if markers[pname] then
markers[pname].timer:cancel()
@@ -134,7 +159,7 @@ ctf_api.register_on_match_end(function()
hud:remove_all()
end)
-local function marker_func(name, param, specific_player)
+local function marker_func(name, param, specific_player, hpmarker)
local pteam = ctf_teams.get(name)
if marker_cooldown:get(name) then
@@ -146,10 +171,13 @@ local function marker_func(name, param, specific_player)
end
local player = minetest.get_player_by_name(name)
+ local message
+ local pos
local pos1 = vector.offset(player:get_pos(), 0, player:get_properties().eye_height, 0)
-
if param == "" then
param = "Look here!"
+ elseif string.len(param) > 40 then
+ param = string.sub(param, 1, 40)
end
local ray = minetest.raycast(
@@ -162,49 +190,74 @@ local function marker_func(name, param, specific_player)
pointed = ray:next()
end
- if not pointed then
- return false, "Can't find anything to mark, too far away!"
+ if pointed and vector.distance(
+ pointed.under or pointed.ref:get_pos(),
+ player:get_pos()
+ ) <= 2 then
+ hpmarker = true
end
- local message = string.format("m [%s]: %s", name, param)
- local pos
-
- if pointed.type == "object" then
- local concat
- local obj = pointed.ref
- local entity = obj:get_luaentity()
-
- -- If object is a player, append player name to display text
- -- Else if obj is item entity, append item description and count to str.
- if obj:is_player() then
- concat = obj:get_player_name()
- elseif entity then
- if entity.name == "__builtin:item" then
- local stack = ItemStack(entity.itemstring)
- local itemdef = minetest.registered_items[stack:get_name()]
-
- -- Fallback to itemstring if description doesn't exist
- -- Only use first line of itemstring
- concat = string.match(itemdef.description or entity.itemstring, "^([^\n]+)")
- concat = concat .. " " .. stack:get_count()
+ if pointed and hpmarker == true then
+ local player_hpr = string.format("HP: %i/%i", player:get_hp(),
+ player:get_properties().hp_max)
+ message = string.format("m [%s]: ", name) .. player_hpr
+ if vector.distance(
+ pointed.under or pointed.ref:get_pos(),
+ player:get_pos()
+ ) <= 2 then
+ pos = pointed.under or pointed.ref:get_pos()
+ else
+ pos = player:get_pos()
+ end
+ if pointed then
+ if pointed.type == "object" then
+ message, pos = check_pointed_entity(pointed, message, pos)
end
end
+ if param ~= "Look here!" then
+ message = string.format("[HP: %i/%i] %s", player:get_hp(),
+ player:get_properties().hp_max, param)
+ end
- pos = obj:get_pos()
- if concat then
- message = message .. " <" .. concat .. ">"
+ -- If the player places a marker upon death, it will resort to the below
+ if player:get_hp() == 0 then
+ message = string.format("m <%s> died here", name)
+ if param ~= "Look here!" then message = string.format(
+ "m [%s]: %s", name, param
+ )
+ end
end
else
- pos = pointed.under
+ if not pointed then
+ return false, "Can't find anything to mark, too far away!"
+ end
+ message = string.format("m [%s]: %s", name, param)
+ if pointed.type == "object" then
+ message, pos = check_pointed_entity(pointed, message)
+ else
+ pos = pointed.under
+ end
end
ctf_modebase.markers.add(name, message, pos, nil, specific_player)
-
marker_cooldown:set(name, MARKER_PLACE_INTERVAL)
-
- return true, "Marker is placed!"
+ if hpmarker then
+ return true, "HP marker is placed!"
+ else
+ return true, "Marker is placed!"
+ end
end
+
+minetest.register_chatcommand("mhp", {
+ description = "Place a HP marker in your look direction",
+ params = "",
+ privs = {interact = true, shout = true},
+ func = function(name, param)
+ return marker_func(name, param, nil, true)
+ end
+})
+
minetest.register_chatcommand("m", {
description = "Place a marker in your look direction",
params = "[message]",
diff --git a/mods/ctf/ctf_modebase/match.lua b/mods/ctf/ctf_modebase/match.lua
index b98da5f73..258f400c9 100644
--- a/mods/ctf/ctf_modebase/match.lua
+++ b/mods/ctf/ctf_modebase/match.lua
@@ -1,7 +1,13 @@
+--- @type false | string
local restart_on_next_match = false
+
ctf_modebase.map_on_next_match = nil
ctf_modebase.mode_on_next_match = nil
+-- Overridable
+function ctf_modebase.map_chosen(map)
+end
+
function ctf_modebase.start_match_after_vote()
local old_mode = ctf_modebase.current_mode
@@ -20,20 +26,44 @@ function ctf_modebase.start_match_after_vote()
end
local map = ctf_modebase.map_catalog.maps[ctf_modebase.map_catalog.current_map]
+
+ ctf_modebase.map_chosen(map)
+
ctf_map.place_map(map, function()
-- Set time and time_speed
minetest.set_timeofday(map.start_time/24000)
minetest.settings:set("time_speed", map.time_speed * 72)
ctf_map.announce_map(map)
- ctf_modebase.announce(string.format("New match: %s map, %s mode", map.name, HumanReadable(ctf_modebase.current_mode)))
+ ctf_modebase.announce(string.format("New match: %s map by %s, %s mode",
+ map.name,
+ map.author,
+ HumanReadable(ctf_modebase.current_mode))
+ )
ctf_modebase.on_new_match()
ctf_modebase.in_game = true
- ctf_teams.allocate_teams(ctf_map.current_map.teams)
+ local team_alloc = ctf_modebase:get_current_mode().allocate_teams
+
+ if team_alloc then
+ team_alloc(ctf_map.current_map.teams)
+ else
+ ctf_teams.allocate_teams(ctf_map.current_map.teams)
+ end
ctf_modebase.current_mode_matches_played = ctf_modebase.current_mode_matches_played + 1
+
+ local current_map = ctf_map.current_map
+ local current_mode = ctf_modebase.current_mode
+
+ if table.indexof(current_map.game_modes, current_mode) == -1 then
+ local concat = "The current mode is not in the list of modes supported by the current map."
+ local cmd_text = string.format("/ctf_next -f [mode:technical modename] %s", current_map.dirname)
+ minetest.chat_send_all(minetest.colorize(
+ "red", string.format("%s\nSupported mode(s): %s. To switch to a mode set for the map, do %s",
+ concat, table.concat(current_map.game_modes, ", "), cmd_text)))
+ end
end)
end
@@ -41,14 +71,19 @@ local function start_new_match()
local path = minetest.get_worldpath() .. "/queue_restart.txt"
if ctf_core.file_exists(path) then
os.remove(path)
- restart_on_next_match = true
+ restart_on_next_match = ""
end
- ctf_modebase.in_game = false
- ctf_modebase.on_match_end()
+ if ctf_modebase.in_game then
+ ctf_modebase.in_game = false
+ ctf_modebase.on_match_end()
+ end
if restart_on_next_match then
- minetest.chat_send_all(minetest.colorize("red", "[NOTICE] Server restarting in 5 seconds..."))
+ minetest.chat_send_all(minetest.colorize("red",
+ "[NOTICE] Server restarting in 5 seconds"..restart_on_next_match.."..."
+ ))
+
minetest.request_shutdown(
"Restarting server at imperator request.\n\nTip: Count to 15 before clicking reconnect",
true, 5
@@ -149,11 +184,14 @@ minetest.register_chatcommand("queue_restart", {
description = "Queue server restart",
privs = {server = true},
func = function(name, param)
- if not param then param = "" end
+ if not param or param == "" then param = false end
+
+ restart_on_next_match = param and (" ("..param..")") or ""
- restart_on_next_match = true
- minetest.log("action", string.format("[ctf_admin] %s queued a restart", name))
- minetest.chat_send_all(minetest.colorize("red", "[NOTICE] Server will restart after this match is over. " .. param))
+ minetest.log("action", string.format("[ctf_admin] %s queued a restart. Reason: %s", name, restart_on_next_match))
+ minetest.chat_send_all(minetest.colorize("red",
+ "[NOTICE] Server will restart after this match is over. " .. restart_on_next_match
+ ))
return true, "Restart is queued."
end
})
diff --git a/mods/ctf/ctf_modebase/mod.conf b/mods/ctf/ctf_modebase/mod.conf
index f585cb49f..4158de296 100644
--- a/mods/ctf/ctf_modebase/mod.conf
+++ b/mods/ctf/ctf_modebase/mod.conf
@@ -1,2 +1,2 @@
name = ctf_modebase
-depends = ctf_api, ctf_core, ctf_teams, ctf_gui, ctf_map, ctf_healing, crafting, mhud, default, binoculars
+depends = ctf_api, ctf_core, ctf_teams, ctf_gui, ctf_map, ctf_healing, crafting, mhud, default, binoculars, ctf_player, player_api, playertag, random_messages, ctf_ranged, ctf_cosmetics, ctf_settings
diff --git a/mods/ctf/ctf_modebase/mode_vote.lua b/mods/ctf/ctf_modebase/mode_vote.lua
index da93935c9..60c5dd0f9 100644
--- a/mods/ctf/ctf_modebase/mode_vote.lua
+++ b/mods/ctf/ctf_modebase/mode_vote.lua
@@ -11,6 +11,8 @@ local new_mode
ctf_modebase.mode_vote = {}
local function player_vote(name, length)
+ if not voted then return end
+
if not voted[name] then
voters_count = voters_count - 1
end
@@ -24,14 +26,21 @@ local function player_vote(name, length)
end
local function show_modechoose_form(player)
- local vote_setting = ctf_settings.get(minetest.get_player_by_name(player), "ctf_modebase:default_vote_"..new_mode)
+ local vote_setting = "ask"
+
+ if ctf_settings.settings["ctf_modebase:default_vote_"..new_mode] then
+ vote_setting = ctf_settings.get(minetest.get_player_by_name(player), "ctf_modebase:default_vote_"..new_mode)
- vote_setting = ctf_settings.settings["ctf_modebase:default_vote_"..new_mode]._list_map[tonumber(vote_setting)]
+ vote_setting = ctf_settings.settings["ctf_modebase:default_vote_"..new_mode]._list_map[tonumber(vote_setting)]
+ end
if vote_setting ~= "ask" then
minetest.after(0, function()
if not minetest.get_player_by_name(player) then return end
+ minetest.chat_send_player(player,
+ string.format("Voting for " .. new_mode .. ". Automatic vote: " .. vote_setting .. "\n" ..
+ "To change the automatic vote settings, go to the \"Settings\" tab of your inventory."))
player_vote(player, vote_setting)
end)
@@ -104,19 +113,25 @@ function ctf_modebase.mode_vote.start_vote()
new_mode = ctf_modebase.modelist[mode_index + 1]
end
- for _, player in pairs(minetest.get_connected_players()) do
- if ctf_teams.get(player) ~= nil or not ctf_modebase.current_mode then
- local pname = player:get_player_name()
+ local mode_defined_rounds = ctf_modebase.modes[new_mode].rounds
+ if not mode_defined_rounds then
+ for _, player in pairs(minetest.get_connected_players()) do
+ if ctf_teams.get(player) ~= nil or not ctf_modebase.current_mode then
+ local pname = player:get_player_name()
- show_modechoose_form(pname)
+ show_modechoose_form(pname)
- voted[pname] = false
- voters_count = voters_count + 1
+ voted[pname] = false
+ voters_count = voters_count + 1
+ end
end
- end
- timer = minetest.after(VOTING_TIME, ctf_modebase.mode_vote.end_vote)
- formspec_send_timer = minetest.after(2, send_formspec)
+ timer = minetest.after(VOTING_TIME, ctf_modebase.mode_vote.end_vote)
+ formspec_send_timer = minetest.after(2, send_formspec)
+ else
+ votes = {mode_defined_rounds}
+ ctf_modebase.mode_vote.end_vote()
+ end
end
function ctf_modebase.mode_vote.end_vote()
@@ -200,11 +215,14 @@ minetest.register_on_leaveplayer(function(player)
local pname = player:get_player_name()
if votes and not voted[pname] then
- voted[pname] = nil
voters_count = voters_count - 1
if voters_count == 0 then
ctf_modebase.mode_vote.end_vote()
end
end
+
+ if voted then
+ voted[pname] = nil
+ end
end)
diff --git a/mods/ctf/ctf_modebase/player.lua b/mods/ctf/ctf_modebase/player.lua
index d6e43ab1b..626595842 100644
--- a/mods/ctf/ctf_modebase/player.lua
+++ b/mods/ctf/ctf_modebase/player.lua
@@ -1,5 +1,123 @@
ctf_modebase.player = {}
+ctf_settings.register("auto_trash_stone_swords", {
+ type = "bool",
+ label = "Auto-trash stone swords when you pick up a better sword",
+ description = "Only triggers when picking up swords from the ground",
+ default = "false"
+})
+
+ctf_settings.register("auto_trash_stone_tools", {
+ type = "bool",
+ label = "Auto-trash stone tools when you pick up a better one",
+ description = "Only triggers when picking up tools from the ground",
+ default = "false"
+})
+
+ctf_settings.register("flag_sound_volume", {
+ type = "bar",
+ label = "Flag Sound Volume",
+ default = "10",
+ min = 0,
+ max = 20,
+ step = 1,
+})
+
+local simplify_for_saved_stuff = function(iname)
+ if not iname or iname == "" then return iname end
+
+ local match
+
+ match = iname:match("default:pick_(%S+)")
+ if match then
+ return "pick", match
+ end
+
+ match = iname:match("default:axe_(%S+)")
+ if match then
+ return "axe", match
+ end
+
+ match = iname:match("default:shovel_(%S+)")
+ if match then
+ return "shovel", match
+ end
+
+ match = iname:match("ctf_mode_nade_fight:(%S+)")
+ if match then
+ return "nade_fight_grenade", match
+ end
+
+ if
+ iname == "ctf_mode_classes:knight_sword" or
+ iname == "ctf_mode_classes:support_bandage" or
+ iname == "ctf_mode_classes:ranged_rifle_loaded"
+ then
+ return "class_primary"
+ end
+
+ local mod
+ mod, match = iname:match("(%S+):sword_(%S+)")
+
+ if mod and (mod == "default" or mod == "ctf_melee") and match then
+ return "sword", match
+ end
+
+ return iname
+end
+
+local function is_initial_stuff(player, i)
+ local mode = ctf_modebase:get_current_mode()
+ if mode and mode.stuff_provider then
+ for _, item in ipairs(mode.stuff_provider(player)) do
+ if ItemStack(item):get_name() == i then
+ return true
+ end
+ end
+ end
+
+ if ctf_map.current_map and ctf_map.current_map.initial_stuff then
+ for _, item in ipairs(ctf_map.current_map.initial_stuff) do
+ if ItemStack(item):get_name() == i then
+ return true
+ end
+ end
+ end
+end
+
+function ctf_modebase.player.save_initial_stuff_positions(player, soft)
+ if not ctf_modebase.current_mode then return end
+
+ local inv = player:get_inventory()
+ local meta = player:get_meta()
+ local ssp = meta:get_string("ctf_modebase:player:initial_stuff_positions:"..ctf_modebase.current_mode)
+
+ if ssp == "" then
+ ssp = {}
+ else
+ ssp = minetest.deserialize(ssp)
+ end
+
+ local done = {}
+ for i, s in pairs(inv:get_list("main")) do
+ local n = s:get_name()
+
+ if n ~= "" and is_initial_stuff(player, n) then
+ local k = simplify_for_saved_stuff(n:match("[^%s]*"))
+
+ if not soft or not ssp[k] then
+ if not done[k] or (i < ssp[k]) then
+ ssp[k] = i
+ done[k] = true
+ end
+ end
+ end
+ end
+
+ meta:set_string("ctf_modebase:player:initial_stuff_positions:"..ctf_modebase.current_mode, minetest.serialize(ssp))
+end
+
+-- Changes made to this function should also be made to is_initial_stuff() above
local function get_initial_stuff(player, f)
local mode = ctf_modebase:get_current_mode()
if mode and mode.stuff_provider then
@@ -19,6 +137,8 @@ function ctf_modebase.player.give_initial_stuff(player)
minetest.log("action", "Giving initial stuff to player " .. player:get_player_name())
local inv = player:get_inventory()
+ local meta = player:get_meta()
+
local item_level = {}
get_initial_stuff(player, function(item)
local mode = ctf_modebase:get_current_mode()
@@ -57,8 +177,148 @@ function ctf_modebase.player.give_initial_stuff(player)
inv:remove_item("main", item)
inv:add_item("main", item)
end)
+
+ -- Check for new items not yet in the order list
+ ctf_modebase.player.save_initial_stuff_positions(player, true)
+
+ local saved_stuff_positions = meta:get_string(
+ "ctf_modebase:player:initial_stuff_positions:"..ctf_modebase.current_mode
+ )
+
+ if saved_stuff_positions == "" then
+ saved_stuff_positions = {}
+ else
+ saved_stuff_positions = minetest.deserialize(saved_stuff_positions)
+ end
+
+ local new = {}
+ local tmp = {}
+ local current = inv:get_list("main")
+ for search, idx in pairs(saved_stuff_positions) do
+ for sidx, stack in ipairs(current) do
+ if stack then
+ local sname = simplify_for_saved_stuff(stack:get_name())
+
+ if sname ~= "" and sname:match(search) then
+ tmp[stack] = idx
+ current[sidx] = false
+ end
+ end
+ end
+ end
+
+ for stack, idx in pairs(tmp) do
+ if not new[idx] then
+ new[idx] = stack
+ end
+ end
+
+ for stack, idx in pairs(tmp) do
+ if new[idx] ~= stack then
+ table.insert(new, stack)
+ end
+ end
+
+ for _, stack in ipairs(current) do
+ if stack then
+ table.insert(new, stack)
+ end
+ end
+
+ inv:set_list("main", new)
+end
+
+if minetest.register_on_item_pickup then
+ minetest.register_on_item_pickup(function(itemstack, picker)
+ if ctf_modebase.current_mode and ctf_teams.get(picker) then
+ local mode = ctf_modebase:get_current_mode()
+ for name, func in pairs(mode.initial_stuff_item_levels) do
+ local priority = func(itemstack)
+
+ if priority then
+ local inv = picker:get_inventory()
+ for i=1, 8 do -- loop through the top row of the player's inv
+ local compare = inv:get_stack("main", i)
+
+ if not mode.is_bound_item or not mode.is_bound_item(picker, compare:get_name()) then
+ local cprio = func(compare)
+
+ if cprio and cprio < priority then
+ local item, typ = simplify_for_saved_stuff(compare:get_name())
+ --minetest.log(dump(item)..dump(typ))
+ inv:set_stack("main", i, itemstack)
+
+ if item == "sword" and typ == "stone" and
+ ctf_settings.get(picker, "auto_trash_stone_swords") == "true" then
+ return ItemStack("")
+ end
+
+ if item ~= "sword" and typ == "stone" and
+ ctf_settings.get(picker, "auto_trash_stone_tools") == "true" then
+ return ItemStack("")
+ else
+ local result = inv:add_item("main", compare):get_count()
+
+ if result == 0 then
+ return ItemStack("")
+ else
+ compare:set_count(result)
+ return compare
+ end
+ end
+ end
+ end
+ end
+ break -- We already found a place for it, don't check for one held by a different item type
+ end
+ end
+ end
+ end)
+else
+ minetest.log("error", "You aren't using the latest version of Minetest, auto-trashing and auto-sort won't work")
end
+minetest.register_on_player_inventory_action(function(player, action, inv, inv_info)
+ if action == "put" and inv_info.listname == "main" then
+ if ctf_modebase.current_mode and ctf_teams.get(player) then
+ local mode = ctf_modebase:get_current_mode()
+ for name, func in pairs(mode.initial_stuff_item_levels) do
+ local priority = func(inv_info.stack)
+
+ if priority then
+ for i=1, 8 do -- loop through the top row of the player's inv
+ local compare = inv:get_stack("main", i)
+
+ local cprio = func(compare)
+
+ if cprio and cprio < priority then
+ local item, typ = simplify_for_saved_stuff(compare:get_name())
+ --minetest.log(dump(item)..dump(typ))
+ inv:set_stack("main", i, inv_info.stack)
+
+ if item == "sword" and typ == "stone" and
+ ctf_settings.get(player, "auto_trash_stone_swords") == "true" then
+ inv:set_stack("main", inv_info.index, ItemStack(""))
+ break
+ end
+
+ if item ~= "sword" and typ == "stone" and
+ ctf_settings.get(player, "auto_trash_stone_tools") == "true" then
+ inv:set_stack("main", inv_info.index, ItemStack(""))
+ break
+ end
+
+ inv:set_stack("main", inv_info.index, compare)
+ break
+ end
+ end
+ break -- We already found a place for it, don't check for one held by a different item type
+ end
+ end
+ end
+ end
+end)
+
function ctf_modebase.player.empty_inv(player)
player:get_inventory():set_list("main", {})
end
@@ -85,6 +345,14 @@ function ctf_modebase.player.remove_initial_stuff(player)
end)
end
+local function nil_to_default(x, default)
+ if x == nil then
+ return default
+ else
+ return x
+ end
+end
+
function ctf_modebase.player.update(player)
-- Set skyboxes, shadows and physics
@@ -104,8 +372,8 @@ function ctf_modebase.player.update(player)
if mode.physics then
player:set_physics_override({
- sneak_glitch = mode.physics.sneak_glitch or false,
- new_move = mode.physics.new_move or true
+ sneak_glitch = nil_to_default(mode.physics.sneak_glitch, false),
+ new_move = nil_to_default(mode.physics.new_move, true),
})
end
end
@@ -137,7 +405,11 @@ minetest.register_on_joinplayer(function(player)
player:set_hp(player:get_properties().hp_max)
local inv = player:get_inventory()
- inv:set_list("main", {})
+
+ if ctf_core.settings.server_mode == "play" then
+ inv:set_list("main", {})
+ end
+
inv:set_list("craft", {})
inv:set_size("craft", 1)
diff --git a/mods/ctf/ctf_modebase/ranking_commands.lua b/mods/ctf/ctf_modebase/ranking_commands.lua
index 430ca1389..44b5aae13 100644
--- a/mods/ctf/ctf_modebase/ranking_commands.lua
+++ b/mods/ctf/ctf_modebase/ranking_commands.lua
@@ -3,7 +3,9 @@ local function get_gamemode(param)
if mode_param then
local mode = ctf_modebase.modes[mode_param]
- if not mode then
+ if mode_param == "all" then
+ return "all", nil, opt_param
+ elseif not mode then
return false, "No such game mode: " .. mode_param
end
@@ -18,54 +20,72 @@ local function get_gamemode(param)
end
end
-ctf_core.register_chatcommand_alias("rank", "r", {
- description = "Get the rank of yourself or a player",
- params = "[mode:technical modename] ",
- func = function(name, param)
- local mode_name, mode_data, pname = get_gamemode(param)
- if not mode_name then
- return false, mode_data
- end
-
- if not pname then
- pname = name
- end
- local prank = mode_data.rankings:get(pname) -- [p]layer [rank]
+local function rank(name, mode_name, mode_data, pname)
+ if not mode_name then
+ return false, mode_data
+ end
- if not prank then
- return false, string.format("Player %s has no rankings in mode %s!", pname, mode_name)
- end
+ if not pname then
+ pname = name
+ end
+ local prank = mode_data.rankings:get(pname) -- [p]layer [rank]
- local return_str = string.format(
- "Rankings for player %s in mode %s:\n\t", minetest.colorize("#ffea00", pname), mode_name
- )
+ if not prank then
+ return false, string.format("Player %s has no rankings in mode %s\n", pname, mode_name)
+ end
- for _, rank in ipairs(mode_data.summary_ranks) do
- return_str = string.format("%s%s: %s,\n\t",
- return_str,
- minetest.colorize("#63d437", HumanReadable(rank)),
- minetest.colorize("#ffea00", math.round(prank[rank] or 0))
- )
- end
+ local return_str = string.format(
+ "\tRankings for player %s in mode %s:\n\t", minetest.colorize("#ffea00", pname), mode_name
+ )
- for _, pair in pairs({{"kills", "deaths"}, {"score", "kills"}}) do
- return_str = string.format("%s%s: %s,\n\t",
- return_str,
- minetest.colorize("#63d437", HumanReadable(pair[1].."/"..pair[2])),
- minetest.colorize("#ffea00", 0.1 * math.round(10 * (
- (prank[pair[1]] or 0 ) /
- math.max(prank[pair[2]] or 0, 1)
- )))
- )
- end
+ for _, irank in ipairs(mode_data.summary_ranks) do
+ return_str = string.format("%s%s: %s,\n\t",
+ return_str,
+ minetest.colorize("#63d437", HumanReadable(irank)),
+ minetest.colorize("#ffea00", math.round(prank[irank] or 0))
+ )
+ end
- return_str = string.format("%s%s: %s",
+ for _, pair in pairs({{"kills", "deaths"}, {"score", "kills"}}) do
+ return_str = string.format("%s%s: %s,\n\t",
return_str,
- minetest.colorize("#63d437", "Place"),
- minetest.colorize("#ffea00", mode_data.rankings.top:get_place(pname))
+ minetest.colorize("#63d437", HumanReadable(pair[1].."/"..pair[2])),
+ minetest.colorize("#ffea00", 0.1 * math.round(10 * (
+ (prank[pair[1]] or 0 ) /
+ math.max(prank[pair[2]] or 0, 1)
+ )))
)
+ end
+
+ return_str = string.format("%s%s: %s\n",
+ return_str,
+ minetest.colorize("#63d437", "Place"),
+ minetest.colorize("#ffea00", mode_data.rankings.top:get_place(pname))
+ )
+
+ return true, return_str
+end
- return true, return_str
+ctf_core.register_chatcommand_alias("rank", "r", {
+ description = "Get the rank of yourself or a player",
+ params = "[ mode:all | mode:technical modename] ",
+ func = function(name, param)
+ local mode_name, mode_data, pname = get_gamemode(param)
+ if mode_name == "all" then
+ local return_str = string.format(
+ "Rankings for player %s in all modes:\n",
+ minetest.colorize("#ffea00", pname or name),
+ mode_name
+ )
+
+ for _, mode in ipairs(ctf_modebase.modelist) do
+ mode_data = ctf_modebase.modes[mode]
+ return_str = return_str .. select(2, rank(name, mode, mode_data, pname))
+ end
+ return true, return_str
+ else
+ return rank(name, mode_name, mode_data, pname)
+ end
end
})
@@ -75,18 +95,56 @@ ctf_api.register_on_match_end(function()
donate_timer = {}
end)
-minetest.register_chatcommand("donate", {
- description = "Donate your match score to your teammate\nCan be used only once in 10 minutes",
- params = "",
+ctf_core.register_chatcommand_alias("donate", "d", {
+ description = "Donate your match score to your teammate\nCan be used only once in 2.5 minutes"..
+ "\nReplace with :max or any negative number to donate the maximum amount",
+ params = " [message]",
func = function(name, param)
local current_mode = ctf_modebase:get_current_mode()
if not current_mode or not ctf_modebase.match_started then
return false, "The match hasn't started yet!"
end
- local pname, score = string.match(param, "^(.*) (.*)$")
+ local pnames, score, dmessage = {}, 0, ""
- if not pname then
+ local pcount, ismessage = 0, false
+
+ for p in string.gmatch(param, "%S+") do
+ if ismessage then
+ dmessage = dmessage .. " " .. p
+ elseif ctf_core.to_number(p) and score == 0 then
+ score = p
+ elseif p == ":max" and score == 0 then
+ score = -1
+ else
+ local team = ctf_teams.get(p)
+ if not team and pcount > 0 then
+ dmessage = dmessage .. p
+ ismessage = true
+ else
+ if pnames[p] then
+ return false, "You cannot donate more than once to the same person."
+ end
+
+ if p == name then
+ return false, 'You cannot donate to yourself!'
+ end
+
+ if not minetest.get_player_by_name(p) then
+ return false, string.format("Player %s is not online!", p)
+ end
+
+ if team ~= ctf_teams.get(name) then
+ return false, string.format("Player %s is not on your team!", p)
+ end
+
+ pnames[p] = team
+ pcount = pcount + 1
+ end
+ end
+ end
+
+ if pcount == 0 then
return false, "You should provide the player name!"
end
@@ -94,54 +152,58 @@ minetest.register_chatcommand("donate", {
if not score then
return false, "You should provide score amount!"
end
-
score = math.floor(score)
- if score < 5 then
- return false, "You should donate at least 5 score!"
- end
+ local cur_score = math.min(
+ current_mode.recent_rankings.get(name).score or 0,
+ (current_mode.rankings:get(name) or {}).score or 0
+ )
- if score > 100 then
- return false, "You can donate no more than 100 score!"
+ if score < 0 then
+ score = math.floor(cur_score / 2 / pcount)
end
- if pname == name then
- return false, 'You cannot donate to yourself!'
+ if score < 5 then
+ return false, "You should donate at least 5 score!"
end
- local team = ctf_teams.get(pname)
+ local scoretotal = score * pcount
- if not team then
- return false, string.format("Player %s is not online!", pname)
+ if scoretotal > cur_score / 2 then
+ return false, "You can donate only half of your match score!"
end
- if team ~= ctf_teams.get(name) then
- return false, string.format("Player %s is not on your team!", pname)
+ if donate_timer[name] and donate_timer[name] + 150 > os.time() then
+ local time_diff = donate_timer[name] + 150 - os.time()
+ return false, string.format(
+ "You can donate only once in 2.5 minutes! You can donate again in %dm %ds.",
+ math.floor(time_diff / 60),
+ time_diff % 60)
end
- local cur_score = math.min(
- current_mode.recent_rankings.get(name).score or 0,
- (current_mode.rankings:get(name) or {}).score or 0
- )
- if score > cur_score / 2 then
- return false, "You can donate only half of your match score!"
- end
+ dmessage = (dmessage and dmessage ~= "") and (": " .. dmessage) or ""
- if donate_timer[name] and donate_timer[name] + 600 > os.time() then
- return false, "You can donate only once in 10 minutes!"
- end
- current_mode.recent_rankings.add(pname, {score=score}, true)
- current_mode.recent_rankings.add(name, {score=-score}, true)
+ current_mode.recent_rankings.add(name, {score=-scoretotal}, true)
+ local names = ""
+ for pname, team in pairs(pnames) do
+ current_mode.recent_rankings.add(pname, {score=score}, true)
+ minetest.log("action", string.format(
+ "Player '%s' donated %s score to player '%s'", name, score, pname
+ ))
+ names = names .. pname .. ", "
+ end
+ names = names:sub(1, -3)
+ if pcount > 2 then
+ names = string.gsub(names, ", (%S+)$", ", and %1")
+ elseif pcount > 1 then
+ names = string.gsub(names, ", (%S+)$", " and %1")
+ end
donate_timer[name] = os.time()
-
- minetest.chat_send_all(minetest.colorize("#00EEFF",
- string.format("%s donated %s score to %s for their hard work", name, score, pname)
- ))
- minetest.log("action", string.format(
- "Player '%s' donated %s score to player '%s'", name, score, pname
- ))
+ local donate_text = string.format("%s donated %s score to %s%s", name, score, names, dmessage)
+ minetest.chat_send_all(minetest.colorize("#00EEFF", donate_text))
+ ctf_modebase.announce(donate_text)
return true
end
})
@@ -152,7 +214,7 @@ minetest.register_chatcommand("reset_rankings", {
params = "[mode:technical modename] ",
func = function(name, param)
local mode_name, mode_data, pname = get_gamemode(param)
- if not mode_name then
+ if not mode_name or not mode_data then
return false, mode_data
end
@@ -176,10 +238,14 @@ minetest.register_chatcommand("reset_rankings", {
allow_reset[key] = nil
end)
- return true, "This will reset your stats and rankings for " .. mode_name .." mode completely."
- .. " You will lose access to any special privileges such as the"
- .. " team chest or userlimit skip. This is irreversable. If you're"
- .. " sure, re-type /reset_rankings within 30 seconds to reset."
+ return true, minetest.colorize("red", "This will reset your (") ..
+ minetest.colorize("cyan", name) ..
+ minetest.colorize("red", ") stats and rankings for ") ..
+ minetest.colorize("cyan", mode_name) ..
+ minetest.colorize("red", " mode completely.\n") ..
+ "You will lose access to any special privileges such as the " ..
+ "team chest or userlimit skip. This is irreversable. If you're " ..
+ "sure, re-type /reset_rankings within 30 seconds to reset."
end
mode_data.rankings:set(name, {}, true)
allow_reset[key] = nil
@@ -197,7 +263,7 @@ minetest.register_chatcommand("top50", {
params = "[mode:technical modename]",
func = function(name, param)
local mode_name, mode_data = get_gamemode(param)
- if not mode_name then
+ if not mode_name or not mode_data then
return false, mode_data
end
@@ -217,21 +283,22 @@ minetest.register_chatcommand("top50", {
table.insert(top50, t)
end
+ mode_data.summary_ranks._sort = "score"
ctf_modebase.summary.show_gui_sorted(name, top50, {}, mode_data.summary_ranks, {
title = "Top 50 Players",
gamemode = mode_name,
- disable_nonuser_colors = true,
+ -- disable_nonuser_colors = true,
})
end,
})
minetest.register_chatcommand("make_pro", {
- description = "Make yourself or another player a pro",
+ description = "Make yourself or another player a pro (Will break target player's ranks)",
params = "[mode:technical modename] ",
privs = {ctf_admin = true},
func = function(name, param)
local mode_name, mode_data, pname = get_gamemode(param)
- if not mode_name then
+ if not mode_name or not mode_data then
return false, mode_data
end
@@ -240,16 +307,17 @@ minetest.register_chatcommand("make_pro", {
end
local old_ranks = mode_data.rankings:get(pname)
+ local note = ""
if not old_ranks then
- return false, string.format("Player '%s' has no rankings!", pname)
+ note = string.format(" Note: Player '%s' had no rankings before that.", pname)
end
- mode_data.rankings:add(pname, {score = 8000, kills = 7, deaths = 5, flag_captures = 5})
+ mode_data.rankings:set(pname, {score = 8000, kills = 7, deaths = 5, flag_captures = 5})
minetest.log("action", string.format(
"[ctf_admin] %s made player '%s' a pro in mode %s: %s", name, pname, mode_name, dump(old_ranks)
))
- return true, string.format("Player '%s' is now a pro!", pname)
+ return true, string.format("Player '%s' is now a pro.%s", pname, note)
end
})
@@ -275,17 +343,17 @@ minetest.register_chatcommand("add_score", {
end
local old_ranks = mode_data.rankings:get(pname)
+ local note = ""
if not old_ranks then
- return false, string.format("Player '%s' has no rankings!", pname)
+ note = string.format(" Note: Player '%s' had no rankings before that.", pname)
end
- local old_score = old_ranks.score or 0
- mode_data.rankings:set(pname, {score = old_score + score})
+ mode_data.rankings:add(pname, {score = score})
minetest.log("action", string.format(
"[ctf_admin] %s added %s score to player '%s' in mode %s", name, score, pname, mode_name
))
- return true, string.format("Added %s score to player '%s'", score, pname)
+ return true, string.format("Added %s score to player '%s'.%s", score, pname, note)
end
})
@@ -322,11 +390,12 @@ minetest.register_chatcommand("transfer_rankings", {
end
end
+ local note = ""
if not src_exists then
return false, string.format("Source player '%s' has no rankings!", src)
end
if not dst_exists then
- return false, string.format("Destination player '%s' has no rankings!", dst)
+ note = string.format(" Note: Destination player '%s' had no rankings.", dst)
end
if src == dst then
@@ -334,7 +403,7 @@ minetest.register_chatcommand("transfer_rankings", {
end
for mode_name, mode in pairs(ctf_modebase.modes) do
- mode.rankings:add(dst, src_rankings[mode_name])
+ mode.rankings:set(dst, src_rankings[mode_name], true)
end
for _, mode in pairs(ctf_modebase.modes) do
@@ -342,9 +411,9 @@ minetest.register_chatcommand("transfer_rankings", {
end
minetest.log("action", string.format(
- "[ctf_admin] %s transferred rankings from '%s' to '%s': %s -> %s",
- name, src, dst, dump(src_rankings), dump(dst_rankings)
+ "[ctf_admin] %s transferred rankings from '%s' to '%s': %s -> %s | %s",
+ name, src, dst, dump(src_rankings), dump(dst_rankings), note
))
- return true, string.format("Rankings of '%s' have been transferred to '%s'", src, dst)
+ return true, string.format("Rankings of '%s' have been transferred to '%s'.%s", src, dst, note)
end
})
diff --git a/mods/ctf/ctf_modebase/recent_rankings.lua b/mods/ctf/ctf_modebase/recent_rankings.lua
index 990de103c..42ea7fb07 100644
--- a/mods/ctf/ctf_modebase/recent_rankings.lua
+++ b/mods/ctf/ctf_modebase/recent_rankings.lua
@@ -27,6 +27,9 @@ return {
if team then
rankings_teams[team][stat] = (rankings_teams[team][stat] or 0) + amount
+ if stat == "score" then
+ rankings_players[player][team.."_"..stat] = (rankings_players[player][team.."_"..stat] or 0) + amount
+ end
end
end
@@ -73,7 +76,17 @@ return {
rankings_teams = {}
end,
players = function() return rankings_players end,
- teams = function() return rankings_teams end,
+ teams = function()
+ local out = {}
+
+ for k, v in pairs(rankings_teams) do
+ if not ctf_teams.team[k].not_playing then
+ out[k] = v
+ end
+ end
+
+ return out
+ end,
}
end
diff --git a/mods/ctf/ctf_modebase/register.lua b/mods/ctf/ctf_modebase/register.lua
index 591b9ebca..26571ef1b 100644
--- a/mods/ctf/ctf_modebase/register.lua
+++ b/mods/ctf/ctf_modebase/register.lua
@@ -1,6 +1,16 @@
-function ctf_modebase.register_mode(name, func)
- ctf_modebase.modes[name] = func
- table.insert(ctf_modebase.modelist, name)
+local registered_exclusive = false
+function ctf_modebase.register_mode(name, def)
+ if def.exclusive then
+ ctf_modebase.modes[name] = def
+ ctf_modebase.modelist = {name}
+ registered_exclusive = true
+ else
+ ctf_modebase.modes[name] = def
+
+ if not registered_exclusive then
+ table.insert(ctf_modebase.modelist, name)
+ end
+ end
end
function ctf_modebase.on_mode_end()
diff --git a/mods/ctf/ctf_modebase/respawn_delay.lua b/mods/ctf/ctf_modebase/respawn_delay.lua
index 7043900ae..0a81f66c4 100644
--- a/mods/ctf/ctf_modebase/respawn_delay.lua
+++ b/mods/ctf/ctf_modebase/respawn_delay.lua
@@ -1,6 +1,15 @@
local RESPAWN_SECONDS = 7
local AUTO_RESPAWN_TIME = 0.4
-local respawn_delay = {}
+local respawn_delay = {--[[
+ pname = {
+ obj = ,
+ timer = ,
+ timer = ,
+ hp_max = ,
+ left =