local modname = "yl_snowball" local modpath = core.get_modpath(modname) local Object = dofile(modpath .. DIR_DELIM .. "classic.lua") local snowgame = {} local get_player_participation = yl_snowball.get_player_participation -- game will end after this time when no one is shooting/hitting yl_snowball.GAME_TIMEOUT = 10 -- If people continue shooting without hitting anyone, game time -- will be extended, but only up to this time limit yl_snowball.PLAYER_SUCK_TIME = 30 -- Time in seconds after which non-moving person is considered AFK and -- hits stop counting yl_snowball.AFK_TIME = 45 local active_games = {} yl_snowball.active_games = active_games local games_by_player = yl_snowball.games_by_player or {} yl_snowball.games_by_player = games_by_player local f = string.format local game_hud = futil.define_hud( "yl_snowball:hud_", { period = 0.567, -- FIXME not needed if updated manually enabled_by_default = false, get_hud_def = function(player) return yl_snowball.update_hud(player) end, } ) core.register_on_leaveplayer( function(player, _timed_out) local player_name = player:get_player_name() if player_name ~= "" then local game = games_by_player[player_name] if game then game:leave(player_name) end games_by_player[player_name] = nil end end ) yl_snowball.update_hud = function(player) local player_name = player:get_player_name() local game = games_by_player[player_name] local text = "" if game then if game.player_num > 1 then text = game:get_info_string(true) else text = "" end else game_hud:set_enabled(player, false) end return { type = "text", text = text, number = 0xFFFFFF, direction = -1, -- right to left? position = { x = 0.7, y = 0.5 }, alignment = { x = 1, y = -1 }, offset = { x = -10, y = -10 }, style = 1, } end local GamePlayer = Object:extend() function GamePlayer:new(player_name) self.player_name = player_name self.active = true self.hits_taken = 0 self.hits_given = 0 self.shots = 0 game_hud:set_enabled(core.get_player_by_name(player_name), true) end function GamePlayer:on_hit_take() self.hits_taken = self.hits_taken + 1 end function GamePlayer:on_hit_give() self.hits_given = self.hits_given + 1 -- this is a bit weird: since we don't start counting shots until -- first one hits, we may have more hits than shots :D self.shots = math.max(self.shots, self.hits_given) end function GamePlayer:on_shoot() self.shots = self.shots + 1 end function GamePlayer:quit() --game_hud:set_enabled(core.get_player_by_name(self.player_name), false) -- this crashes XD end local Game = Object:extend() function Game:new() local current_time = core.get_gametime() self.time_started = current_time self.time_last_shot = current_time self.time_last_hit = current_time self.players = {} self.player_num = 0 active_games[self] = true return self end function Game:get_time_left() local current_time = core.get_gametime() local time_left if (self.time_last_shot - self.time_last_hit) < yl_snowball.PLAYER_SUCK_TIME then time_left = yl_snowball.GAME_TIMEOUT - (current_time - self.time_last_shot) else time_left = yl_snowball.GAME_TIMEOUT + yl_snowball.PLAYER_SUCK_TIME - (current_time - self.time_last_hit) end return time_left > 0 and time_left or 0 end function Game:get_info_string(show_time) local top = {} for p_name, player in pairs(self.players) do table.insert(top, {p_name, player}) end table.sort(top, function (a,b) return a[2].hits_given > b[2].hits_given end) local lines = {} if show_time then table.insert(lines, f("Game ends in: %ds", self:get_time_left())) end for _, info in ipairs(top) do local p_name = info[1] local player = info[2] local accuracy = player.hits_given/player.shots * 100 table.insert(lines, f("%03d|%03d|%05.1f%%|%s", player.hits_given, player.hits_taken, accuracy == accuracy and accuracy or 0, p_name)) end return table.concat(lines, "\n") end function Game:add_player(player_name, game_player) if self.players[player_name] then if game_player and game_player.active then self.players[player_name] = game_player games_by_player[player_name] = self end -- Do nothing here: player merged their old scores from the -- game where they weren't active in else if (not game_player) or (not game_player.active) then game_player = GamePlayer(player_name) end self.players[player_name] = game_player games_by_player[player_name] = self -- this is a new player, increase player count self.player_num = self.player_num + 1 end end function Game:get_player(player_name) return self.players[player_name] end function Game:leave(player_name) game_hud:set_enabled(core.get_player_by_name(player_name), false) self.players[player_name].active = false games_by_player[player_name] = nil end function Game:merge(other_game) for name, game_player in pairs(other_game.players) do self:add_player(name, game_player) end self.time_started = math.min(self.time_started, other_game.time_started) self.time_last_hit = math.max(self.time_last_hit, other_game.time_last_hit) self.time_last_shot = math.max(self.time_last_shot, other_game.time_last_shot) active_games[other_game] = nil end function Game:register_hit(shooter_name, target_name) local s_player = self:get_player(shooter_name) s_player:on_hit_give() local t_player = self:get_player(target_name) t_player:on_hit_take() local current_time = core.get_gametime() self.time_last_hit = current_time end function Game:register_shot(shooter_name) local s_player = self:get_player(shooter_name) s_player:on_shoot() local current_time = core.get_gametime() self.time_last_shot = current_time end function Game:stop() -- TODO show only top + your own scores local lasted = self.time_last_shot - self.time_started local lines = { f("* Snowball Game Ended (lasted %ds)", lasted), self:get_info_string(), } for player_name, player in pairs(self.players) do if games_by_player[player_name] then if self.player_num > 1 then core.chat_send_player(player_name, table.concat(lines, "\n")) end player:quit() games_by_player[player_name] = nil end end active_games[self] = nil end function snowgame.register_hit(shooter_name, target_name) local s_game = games_by_player[shooter_name] local t_game = games_by_player[target_name] if not s_game then -- somehow landed a hit before being in a game -- probably because left and rejoined? s_game = Game() s_game:add_player(shooter_name) s_game:register_shot(shooter_name) -- if was in the game, then shot was registered in on_use end if afk_api.is_afk(target_name, yl_snowball.AFK_TIME) then -- don't count hits on afk people return end if s_game and t_game then if (s_game ~= t_game) then -- if both playing, but different games, then merge s_game:merge(t_game) end -- either same game, or was merged into one s_game:register_hit(shooter_name, target_name) end end function snowgame.register_shot(shooter_name) local s_game = games_by_player[shooter_name] if not s_game then -- started shooting first time, started a game s_game = Game() s_game:add_player(shooter_name) games_by_player[shooter_name] = s_game end s_game:register_shot(shooter_name) end futil.register_globalstep({ period = 1.31, func = function(dtime) local current_time = core.get_gametime() for game,_ in pairs(active_games) do if game:get_time_left() <= 0 then game:stop() end end end, }) yl_snowball.snowgame = snowgame return snowgame