local floor = math.floor

local gplayers = game.players
local ghub = game.hub
local ggameplay = game.gameplay
local gforge = game.forge
local gmisc = game.misc
local pmaster = player.master
local pmisc = player.hub_misc
local psettings = player.settings
local pemojis = player.emojis_wheel

local ffateam, team1, team2, team3, team4 = ghub.gui_teams[1], ghub.gui_teams[2], ghub.gui_teams[3], ghub.gui_teams[4], ghub.gui_teams[5]
local teams_ptrs = { ffateam, team1, team2, team3, team4 }

local _participants
local _participants_by_teams
local _ai_participants
local _tribes_to_participants_ptrs

local powerups_table = powerups_table
local active_powerups = ggameplay.active_powerups
local mg_player_vars = ggameplay.mg_player_vars
local mg_vars = ggameplay.mg_vars

--====================================================================================================================================================
-- GENERAL ---- ======================================================================================================================================== --

function HasFlag(flags, flag)
    return (flags & flag) ~= 0
end

function EnableFlag(_f1, _f2)
    if ((_f1 & _f2) == 0) then
        _f1 = _f1 | _f2
    end
    return _f1
end

function DisableFlag(_f1, _f2)
    if ((_f1 & _f2) == _f2) then
        _f1 = _f1 ~ _f2
    end
    return _f1
end

function LOG(msg)
	log_msg(8,"" .. tostring(msg))
end

function player_in_options()
	return (_gnsi.Flags3 & GNS3_INGAME_OPTIONS) > 0
end

function player_in_player_stats()
	return (_gnsi.Flags3 & 	GNS3_DISPLAY_LEVEL_STATS) > 0
end

function no_menus_open()
	return (not player_in_options() and not player_in_player_stats())
end

function hide_panel()
	if GUIW > 0 then
		process_options(OPT_TOGGLE_PANEL, playernum, 1)
	end
end

function show_panel()
	if GUIW == 0 then
		process_options(OPT_TOGGLE_PANEL, playernum, 0)
	end
end

function unpause_if_paused()
	if isPaused() then
		process_options(OPT_SET_PAUSE, 0, FALSE)
	end
end

function pause_if_unpaused()
	if not isPaused() then
		process_options(OPT_SET_PAUSE, 0, FALSE)
	end
end

function isPaused()
	return _gnsi.Flags & GNS_PAUSED > 0
end

function game_is_win()
	return (_gnsi.Flags2 & GPF2_GAME_NO_WIN) > 0
end

function update_screen_res()
	GUIW = GFGetGuiWidth()
	local new_w, new_h = ScreenWidth(), ScreenHeight()
	
	if new_w ~= W or new_h ~= H then
		OnResolutionChange()
	end
	
	W, H = new_w, new_h
	W2, H2 = W // 2, H // 2
end

--====================================================================================================================================================
-- MATHS ---- ======================================================================================================================================== --

function rndb(a, b)
	return a + G_RANDOM(b - a + 1)
end

function chance(chance)
	return rndb(1, 100) <= chance
end

function XpercentOf(x, maxValue)
	return mf(x*maxValue, 100)
end

function XpercentOf_fixed(x, maxValue)
    return (x / maxValue) * 100
end

function mf(value,value2)
	return floor((value/value2)+0.5)
end

function mc(value,value2)
	return math.ceil((value/value2)+0.5)
end

function clamp(value, min, max)
	return math.min(max, math.max(min, value))
end

function toSigned16(v)
	return (v > 32767) and (v - 65536) or v
end

function atan2(y, x)
    if x > 0 then
        return math.atan(y / x)
    elseif x < 0 then
        return math.atan(y / x) + math.pi * (y >= 0 and 1 or -1)
    elseif y > 0 then
        return math.pi / 2
    elseif y < 0 then
        return -math.pi / 2
    else
        return 0 -- undefined, but return 0 for convenience
    end
end

function rand01()
    return rndb(0, 999999) / 1000000
end

--====================================================================================================================================================
-- TIMERS ---- ======================================================================================================================================== --

function everySeconds(a)
  return (turn % (a*12) == 0)
end

function everyTurns(a)
  return (turn % (a) == 0)
end

function everySeconds_mg(a)
	return (mg_turn % (a*12) == 0)
end

function everyTurns_mg(a)
	return (mg_turn % (a) == 0)
end

function seconds()
	return floor(_gsi.Counts.ProcessThings/12)
end

function minutes()
	return floor(seconds()/60)
end

function secondsToClock(seconds, useHours)
	if seconds <= 0 then
		return "00:00";
	else
		local h = floor(seconds/3600)
		local hours = string.format("%02.f", h)
		local mins = string.format("%02.f", floor(seconds/60 - (hours*60)))
		local secs = string.format("%02.f", floor(seconds - hours*3600 - mins *60))
		
		if useHours or h > 0 then
			return hours..":"..mins..":"..secs
		end
		
		return mins..":"..secs
	end
end

function increase_framer()
	framer = framer + 1
	
	if framer > 1000 then
		framer = 1
	end
end

function everyFramer(x)
	return framer % x == 0
end

--====================================================================================================================================================
-- GAME BEHAVIOR ---- ======================================================================================================================================== --

function remove_reincarnation_sites()
	for pn = 0, 7 do
		SET_NO_REINC(pn)
	end
end
remove_reincarnation_sites()

function GetPop(pn)
  return _gsi.Players[pn].NumPeople
end

function lock_shamans()
	for i = 0, 7 do
		local shaman = getShaman(i)
		
		if shaman then
			shaman.Flags = EnableFlag(shaman.Flags, TF_STATE_LOCKED)
		end
	end
end

function unlock_shamans()
	for i = 0, 7 do
		local shaman = getShaman(i)
		
		if shaman then
			shaman.Flags = DisableFlag(shaman.Flags, TF_STATE_LOCKED)
		end
	end
end

function set_everyone_enemy()
	for i = 0, 7 do
		for j = i + 1, 7 do
			set_players_enemies(i, j)
			set_players_enemies(j, i)
		end
	end
end

function Allies(a, b)
	if are_players_allied(a, b) > 0 then
		if are_players_allied(b, a) > 0 then
			return true
		end
	end
	
	return false
end

function sink_land_created_inside_mg()
	local radius = 6
	
	for _, v in ipairs(ggameplay.mg_created_land_locations) do
		local idx = world_coord3d_to_map_idx(v)
		
		
		--test some flag removal before sinking
		-- SearchMapCells(SQUARE, 0, 0, radius, idx, function(me)
			-- --for i = 0, 31 do
				-- --me.Flags = me.Flags ~ (1<<26)
				-- --DisableFlag(me.Flags, (1<<i)) --E2_ME_FLAGS_LAVA
				-- --me.Cliff = 1
				
				-- --createThing(T_EFFECT, M_EFFECT_BOAT_HUT_REPAIR, 0, v, false, false)
				-- --local a = createThing(T_EFFECT, M_EFFECT_BOAT_HUT_REPAIR, SAFE_NEUTRAL, v, false, false)
				-- --a.State = S_EFFECT_BOAT_HUT_REPAIR
				-- --me.ShadeIncr = 0
			-- --end
			-- --createThing(T_EFFECT, M_EFFECT_BOAT_HUT_REPAIR, 0, v, false, false)
		-- return true end)
		
		
		sink_land_idx_rad(idx, radius)
		affect_mapwho_area(AFFECT_ALTITUDE, idx, radius + 2)
		
		--createThing(T_EFFECT, M_EFFECT_GROUND_SHOCKWAVE, SAFE_NEUTRAL, v, false, false)
		--set_square_map_params(idx, radius + 2, TRUE)
		--affect_mapwho_area(AFFECT_ALTITUDE, idx, radius + 2)
		createThing(T_EFFECT, M_EFFECT_DIP, SAFE_NEUTRAL, v, false, false)
		createThing(T_EFFECT, M_EFFECT_DIP, SAFE_NEUTRAL, v, false, false)
		--createThing(T_EFFECT, M_EFFECT_EROSION, SAFE_NEUTRAL, v, false, false)
	end
	
	ggameplay.mg_created_land_locations = {}
end

function sink_land_idx_rad(idx, radius)
	SearchMapCells(SQUARE, 0, 0, radius, idx, function(me)
		--DisableFlag(me.Flags, (1<<26)) --E2_ME_FLAGS_LAVA
		me.Alt = 0
		--me.Cliff = 0
	return true end)
	
	set_square_map_params(idx, radius + 2, TRUE)
	--affect_mapwho_area(AFFECT_ALTITUDE, idx, radius + 2)
end

function insert_c3d_from_land_location(c3d)
	table.insert(ggameplay.mg_created_land_locations, c3d)
end

function on_land_spell(model, c3d_from, c3d_to)
	if model == M_SPELL_LAND_BRIDGE then
		insert_c3d_from_land_location(c3d_to)
		
		if c3d_from then
			insert_c3d_from_land_location(c3d_from)
		end
	elseif model == M_SPELL_FLATTEN then
		insert_c3d_from_land_location(c3d_to)
	end
end

--====================================================================================================================================================
-- WORLD ---- ======================================================================================================================================== --

function set_zone_unwalkable(c3d)
	local function set_flags_unwalkable(me)
		me.Flags = EnableFlag(me.Flags, UNWALKABLE_FLAGS)
		return true
	end

	SearchMapCells(SQUARE, 0, 0, 0, world_coord3d_to_map_idx(c3d), set_flags_unwalkable)
	table.insert(ggameplay.mg_unwalkable_zones, c3d)
end

function set_zone_walkable(c3d, clear_from_tbl)
	local function set_flags_walkable(me)
		me.Flags = DisableFlag(me.Flags, UNWALKABLE_FLAGS)
		return true
	end

	SearchMapCells(SQUARE, 0, 0, 0, world_coord3d_to_map_idx(c3d), set_flags_walkable)

	if clear_from_tbl then
		for k, zone in ipairs(ggameplay.mg_unwalkable_zones) do
			if c3d == zone then
				table.remove(ggameplay.mg_unwalkable_zones, k)
				break
			end
		end
	end
end

function reset_unwalkable_zones()
	for _, v in ipairs(ggameplay.mg_unwalkable_zones) do
		set_zone_walkable(v, false)
	end
	
	ggameplay.mg_unwalkable_zones = {}
end

function thing_close_to_land(t, radius)
	local land = false

	SearchMapCells(SQUARE, 0, 0, radius, world_coord3d_to_map_idx(t.Pos.D3), function(me)
		if is_map_elem_land_or_coast(me) > 0 then
			land = true
		end
		return true
	end)
	
	return land
end

function is_c3d_land_or_coast(c3d)
	local land = false
	
	SearchMapCells(SQUARE, 0, 0, 0, world_coord3d_to_map_idx(c3d), function(me)
		land = is_map_elem_land_or_coast(me) > 0
		return false
	end)
	
	return land
end

function is_c3d_land(c3d)
	local land = false
	
	SearchMapCells(SQUARE, 0, 0, 0, world_coord3d_to_map_idx(c3d), function(me)
		land = is_map_elem_all_land(me) > 0
		return false
	end)
	
	return land
end

function is_c3d_water(c3d)
	local water = false
	
	SearchMapCells(SQUARE, 0, 0, 0, world_coord3d_to_map_idx(c3d), function(me)
		water = is_map_elem_all_sea(me) > 0
		return false
	end)
	
	return water
end

function is_c2d_water(c2d)
	local water = false
	
	SearchMapCells(SQUARE, 0, 0, 0, world_coord2d_to_map_idx(c2d), function(me)
		water = is_map_elem_all_sea(me) > 0
		return false
	end)
	
	return water
end

function is_c3d_water_or_coast(c3d)
	local w_or_c = false
	
	SearchMapCells(SQUARE, 0, 0, 0, world_coord3d_to_map_idx(c3d), function(me)
		w_or_c = is_map_elem_sea_or_coast(me) > 0
		return false
	end)
	
	return w_or_c
end

function copy_c3d(c3d)
    local _c3d = Coord3D.new()
    _c3d.Xpos = c3d.Xpos
    _c3d.Ypos = c3d.Ypos
    _c3d.Zpos = c3d.Zpos
    return _c3d
end

function copy_c2d(c2d)
    local _c2d = Coord2D.new()
    _c2d.Xpos = c2d.Xpos
    _c2d.Zpos = c2d.Zpos
    return _c2d
end

function c3d_offsetted(c3d, offset)
	local new_c3d = copy_c3d(c3d)
	new_c3d.Xpos = new_c3d.Xpos + rndb(-offset, offset)
	new_c3d.Zpos = new_c3d.Zpos + rndb(-offset, offset)
	return new_c3d
end

function random_c3d_in_area(Xmin, Ymin, Xmax, Ymax)
	local x = rndb(Xmin, Xmax)
	local y = rndb(Ymin, Ymax)
	
	return coord_to_c3d(x, y)
end

function coord_to_c2d(x,z)
	local c2d = Coord2D.new()
	map_xz_to_world_coord2d(x, z, c2d)
	
	return c2d
end

function coord_to_c3d(x,z)
	return MAP_XZ_2_WORLD_XYZ(x, z)
end

function coord_to_map_idx(x,z)
	local c2d = coord_to_c2d(x,z)
	local pos = MapPosXZ.new()
	pos = world_coord2d_to_map_idx(c2d)
	return pos
end

function c2d_to_c3d(c2d)
	local c3d = Coord3D.new()
	coord2D_to_coord3D(copy_c2d(c2d), c3d)
	
	return c3d
end

function c3d_to_c2d(c3d)
	local c2d = Coord2D.new()
	coord3D_to_coord2D(copy_c3d(c3d), c2d)
	
	return c2d
end

function me2c3d(me)
	local _me = MAP_ELEM_PTR_2_IDX(me)
	local c3d = Coord3D.new()
	map_idx_to_world_coord3d_centre(_me, c3d)
	
	return c3d
end

function me2c2d(me)
	local _me = MAP_ELEM_PTR_2_IDX(me)
	local c2d = Coord2D.new()
	map_idx_to_world_coord2d_centre(_me, c2d)
	
	return c2d
end

function c3d_scenery_free(c3d)
	return is_map_cell_obstacle_free(world_coord3d_to_map_idx(c3d)) > 0
end

function c3d_person_free(c3d)
	local free = true
	
	SearchMapCells(SQUARE, 0, 0, 0, world_coord3d_to_map_idx(c3d), function(me)
		me.MapWhoList:processList(function(t)
			if t.Type == T_PERSON then
				free = false
			end
		return true end)
	return true end)
	
	return free
end

function c3d_bldg_vehicle_free(c3d)
	local free = true
	
	SearchMapCells(SQUARE, 0, 0, 0, world_coord3d_to_map_idx(c3d), function(me)
		me.MapWhoList:processList(function(t)
			local type = t.Type
			
			if type == T_VEHICLE or (type == T_BUILDING) then
				free = false
			end
		return true end)
	return true end)

	return free
end

function c3d_totally_free(c3d)
	return (c3d_scenery_free(c3d) and c3d_person_free(c3d) and c3d_bldg_vehicle_free(c3d))
end

function set_obj_drawinfo(t, tdi, drawnum, alpha)
	if t then
		set_thing_draw_info(t, tdi, drawnum)
		t.DrawInfo.Alpha = alpha or -16
	end
end

--====================================================================================================================================================
-- UNITS ---- ======================================================================================================================================== --

function face_thing_towards_with_optional_miss(thingA, thingB, min_error, max_error)
    local a = thingA.Pos.D3
    local b = thingB.Pos.D3

    local dx = b.Xpos - a.Xpos
    local dz = b.Zpos - a.Zpos

    local angle_rad = atan2(dx, dz)
    local angle = floor((angle_rad / (2 * math.pi)) * 2048) % 2048

    local sign = (G_RANDOM(2) == 0) and -1 or 1

    local err = min_error + G_RANDOM(max_error - min_error + 1)
    angle = (angle + sign * err) % 2048

    thingA.AngleXZ = angle
    if thingA.Move and thingA.Move.CurrDest then
        thingA.Move.CurrDest.AngleXZ = angle
    end
end

function try_to_select_thing(thing, owner)
	--this function seems to desync.
	if not (playernum == owner) then return end
	if not thing then return end
	
	local tpers = thing.u.Pers
	if not tpers then return end
	
	tpers.CmdGroupFlags = tpers.CmdGroupFlags | CGF_CURRENTLY_SELECTED
end

function cast_spell_safe(model, tribe, c3d, b1, b2)
	if not nilS(tribe) then return false end
	
	createThing(T_SPELL, model, tribe, c3d, b1, b2)
end

function create_thing_at_thing_pos_safe(type, model, owner, thing, cache_bool)
	if thing then
		createThing(type, model, owner, thing.Pos.D3, false, false)
	end
end

function set_shaman_height(tribe, height)
	if nilS(tribe) then
		local shaman = getShaman(tribe)
		shaman.Pos.D3.Ypos = height
	end
end

function increase_shaman_height(tribe, amount)
	if nilS(tribe) then
		local shaman = getShaman(tribe)
		shaman.Pos.D3.Ypos = shaman.Pos.D3.Ypos + amount
	end
end

function kill_all_shamans()
	for _, player in ipairs(_participants) do
		local tribe = player.tribe
		if nilS(tribe) then
			delete_thing_type(getShaman(tribe))
		end
	end
end

function clean_mg_cached_things()
	for thing_type = 1, NUM_THING_TYPES do
		clean_mg_cached_things_of_type(thing_type)
	end
	
	--extra safety for effects
	ProcessGlobalTypeList(T_EFFECT, function(ef)
		if ef then
			local effect = ef.u.Effect
			if effect then
				effect.Duration = 1
			end
			delete_thing_type(ef)
		end
	return true end)
	
	ggameplay.mg_obj_list = {}
	kill_all_shamans()
end

function clean_mg_cached_things_of_type(type)
	local tbl = ggameplay.mg_obj_list[type]
	if not tbl then return end
	
	for _, thing in ipairs(tbl) do
		delete_thing_type_safe(thing)
	end
end

function create_thing_cache(type, model, owner, c3d, f1, f2)
	local t = createThing(type, model, owner, c3d, f1 or false, f2 or false)
	table.insert(ggameplay.mg_obj_list[type], t)
	return t
end

function insert_thing_in_cache(t)
	local type = t.Type
	table.insert(ggameplay.mg_obj_list[type], t)
end

function set_unit_flag(t, flag, flag_tf, bool_set)
	if flag_tf == 1 then
		if bool_set then
			t.Flags = t.Flags | flag
		else
			t.Flags = t.Flags & ~flag
		end
	elseif flag_tf == 2 then
		if bool_set then
			t.Flags2 = t.Flags2 | flag
		else
			t.Flags2 = t.Flags2 & ~flag
		end
	else
		if bool_set then
			t.Flags3 = t.Flags3 | flag
		else
			t.Flags3 = t.Flags3 & ~flag
		end
	end
end

function delete_thing_type_safe(t)
	if t then
		delete_thing_type(t)
	end
end

function thing_has_state(thing, state)
	return thing.State == state
end

function thing_has_flag(thing, flag, flagid)
	if flagid == 1 then
		return thing.Flags & flag ~= 0
	elseif flagid == 2 then
		return thing.Flags2 & flag ~= 0
	else
		return thing.Flags3 & flag ~= 0
	end
	
end

function nilS(pn)
	return getShaman(pn) ~= nil
end

function shaman_idle(tribe) --check where this is called, to see if need a nil check
	local shaman = getShaman(tribe)
	
	if thing_has_state(shaman, S_PERSON_GOTO_BASE_AND_WAIT) then
		return true
	end
	
	if thing_has_state(shaman, S_PERSON_WAIT_AT_POINT) then
		return true
	end
	
	return false
end

function ZoomThing(thing, angle)
	if thing then
		local pos = MapPosXZ.new() 
		pos.Pos = world_coord3d_to_map_idx(thing.Pos.D3)	
		ZOOM_TO(pos.XZ.X, pos.XZ.Z, angle or 0)
	end
end

function c3d_has_shaman_rad(_c3d, radius)
	for _, player in ipairs(_participants) do
		local tribe = player.tribe
		if nilS(tribe) then
			if get_world_dist_xyz(_c3d, getShaman(tribe).Pos.D3) <= 256 + 512*radius then
				return true
			end
		end
	end
	
	return false
end

--test this functions, seems wrong?
function giveShield(thing, duration)
	thing.Flags3 = EnableFlag(thing.Flags3, TF3_SHIELD_ACTIVE)
	local old, new = thing.u.Pers.u.Owned.ShieldCount, floor(1.6 * duration)
	
	if duration ~= -1 then
		thing.u.Pers.u.Owned.ShieldCount = math.min(255, math.max(old, new))
	else
		thing.u.Pers.u.Owned.ShieldCount = -1
	end
end

function giveBloodlust(thing, duration)
	thing.Flags3 = EnableFlag(thing.Flags3, TF3_BLOODLUST_ACTIVE)
	local old, new = thing.u.Pers.u.Owned.BloodlustCount, floor(1.6 * duration)
	
	if duration ~= -1 then
		thing.u.Pers.u.Owned.BloodlustCount = math.min(255, math.max(old, new))
	else
		thing.u.Pers.u.Owned.BloodlustCount = -1
	end
end

function giveInvisibility(thing, duration)
	thing.Flags2 = EnableFlag(thing.Flags2, TF2_THING_IS_AN_INVISIBLE_PERSON)
	local old, new = thing.u.Pers.u.Owned.InvisibleCount, floor(1.6 * duration)
	
	if duration ~= -1 then
		thing.u.Pers.u.Owned.InvisibleCount = math.min(255, math.max(old, new))
	else
		thing.u.Pers.u.Owned.InvisibleCount = -1
	end
end

function isGhost(thing)
	return thing.Flags2 & TF2_THING_IS_A_GHOST_PERSON > 0
end

function isInvisible(thing)
	return thing.Flags2 & TF2_THING_IS_AN_INVISIBLE_PERSON > 0
end

function isBloodlusted(thing)
	return thing.Flags3 & TF3_BLOODLUST_ACTIVE > 0
end

function isShielded(thing)
	return thing.Flags3 & TF3_SHIELD_ACTIVE > 0
end

function ThingX(thing)
	if thing then
		local pos = MapPosXZ.new()
		pos.Pos = world_coord3d_to_map_idx(thing.Pos.D3)
		return pos.XZ.X
	end
end

function ThingZ(thing)
	if thing then
		local pos = MapPosXZ.new()
		pos.Pos = world_coord3d_to_map_idx(thing.Pos.D3)
		return pos.XZ.Z
	end
end

function remove_shaman_melee(tribe)
	local shaman = getShaman(tribe)
	local cmd = get_thing_curr_cmd_list_ptr(shaman)
	
	if cmd then
		local cmdtype = cmd.CommandType
		if (cmdtype == CMD_ATTACK_AREA_2) or (cmdtype == CMD_ATTACK_TARGET) then
			cmd.CommandType = CMD_HEAD_PRAY
		end
	end
end

function shaman_casting(tribe)
	return getShaman(tribe).State == S_PERSON_SPELL_TRANCE
end

function shaman_has_healthy_state(tribe)
	local shaman = getShaman(tribe)

	if shaman then
		if shaman.u.Pers.Life == 0 then
			return false
		end
		
		if thing_has_state(shaman, S_PERSON_ELECTROCUTED) then
			return false
		end
		
		if thing_has_state(shaman, S_PERSON_DYING) then
			return false
		end
		
		if thing_has_state(shaman, S_PERSON_DROWNING) then
			return false
		end
		
		if thing_has_state(shaman, S_PERSON_IN_WHIRLWIND) then
			return false
		end
		
		if thing_has_state(shaman, S_PERSON_ANGEL_ROAM) then
			return false
		end
		
		if thing_has_state(shaman, S_PERSON_AOD2_VICTIM) then
			return false
		end
		
		if thing_has_flag(shaman, TF2_THING_IN_AIR, 2) then
			return false
		end
		
		return true
	end
	
	return false
end

function shaman_is_drowning(shaman)
	return thing_has_state(shaman, S_PERSON_DROWNING)
end

function shaman_is_flying(shaman)
	if thing_has_state(shaman, S_PERSON_IN_WHIRLWIND) then
		return true
	end
	
	if thing_has_flag(shaman, TF2_THING_IN_AIR, 2) then
		return true
	end
	
	return false
end

function shaman_can_cast(shaman)
	if shaman_is_flying(shaman) then
		return false
	end
	
	if shaman_is_drowning(shaman) then
		return false
	end
	
	local owner = shaman.Owner
	
	if not shaman_has_healthy_state(owner) then
		return false
	end
	
	if shaman_casting(owner) then
		return false
	end
	
	return true
end

function get_closest_alive_enemy_shaman(tribe)
	local self_shaman = getShaman(tribe)

	local thing_c3d = self_shaman.Pos.D3
	local closest, min_dist = nil, math.huge

	for _, enemy in ipairs(get_participant_table(tribe).enemy_list) do
		local targ = getShaman(enemy)

		if targ then
			local dist = get_world_dist_xyz(thing_c3d, targ.Pos.D3)
			
			if dist < min_dist then
				closest, min_dist = enemy, dist
			end
		end
	end

	return closest, min_dist
end

function get_random_alive_enemy_shaman(tribe)
	local shamans = {}

	for _, enemy in ipairs(get_participant_table(tribe).enemy_list) do
		if nilS(enemy) then
			local t = getShaman(enemy)
			if not isInvisible(t) then
				table.insert(shamans, t)
			end
		end
	end

	return randomItemFromTable(shamans)
end

function get_random_enemy_shaman_near_thing(thing, radius)
	local owner = thing.Owner
	local thing_c3d = thing.Pos.D3
	local shamans = {}
	
	for _, enemy in ipairs(get_participant_table(owner).enemy_list) do
		local targ = getShaman(enemy)
		
		if targ then
			if not isInvisible(targ) then
				if get_world_dist_xyz(thing_c3d, targ.Pos.D3) <= 512*radius then
					table.insert(shamans, targ)
				end
			end
		end
	end
	
	return randomItemFromTable(shamans)
end

function set_all_shamans_speeds(value)
	ggameplay.shaman_speed = value
	tmi[TMI_PERSON_MEDICINE_MAN].BaseSpeed = value
end

function count_persons_of_model_in_area(tribe, only_enemies, c3d, radius, model, count_invisible, count_ghosts)
	local enemies_tbl = get_enemy_tribes(tribe)
	local count = 0
	
	SearchMapCells(SQUARE, 0, 0, radius, world_coord3d_to_map_idx(c3d), function(me)
		me.MapWhoList:processList(function(t)
			if t.Type == T_PERSON then
				if model == -1 or (t.Model == model) then
					if count_invisible or not isInvisible(t) then
						if count_ghosts or not isGhost(t) then
							if not only_enemies or isItemInTable(enemies_tbl, t.Owner) then
								count = count + 1
							end
						end
					end
				end
			end
			return true
		end)
	return true end)
	
	return count
end

function count_things_of_type(type, optional_model)
	--WOODLIST
	local count = 0
	
	ProcessGlobalTypeList(type, function(t)
		if not optional_model or (t.Model == optional_model) then
			count = count + 1
		end
	return true end)
	
	return count
end

--====================================================================================================================================================
-- BUILDINGS & VEHICLES ---- ======================================================================================================================================== --

function count_teepees(pn, fully_built)
	local gsiplayer = _gsi.Players[pn]
	local huts = 0
	
	if fully_built then 
		huts = gsiplayer.NumBuildingsOfType[M_BUILDING_TEPEE]
		huts = huts + gsiplayer.NumBuildingsOfType[M_BUILDING_TEPEE_2]
		huts = huts + gsiplayer.NumBuildingsOfType[M_BUILDING_TEPEE_3]
	else 
		huts = gsiplayer.NumBuiltOrPartBuiltBuildingsOfType[M_BUILDING_TEPEE]
		huts = huts + gsiplayer.NumBuiltOrPartBuiltBuildingsOfType[M_BUILDING_TEPEE_2]
		huts = huts + gsiplayer.NumBuiltOrPartBuiltBuildingsOfType[M_BUILDING_TEPEE_3]
	end
	
	return huts
end

function count_boats(c3d, x, z, rad, only_good_state_boat, only_empty)
	local boats = 0
	local location = c3d and world_coord3d_to_map_idx(c3d) or world_coord3d_to_map_idx(MAP_XZ_2_WORLD_XYZ(x, z))
	
	SearchMapCells(SQUARE, 0, 0, rad, location, function(me)
		me.MapWhoList:processList(function(t)
			if (t.Type == T_VEHICLE and t.Model == M_VEHICLE_BOAT_1) then
				if (not only_good_state_boat) or (t.State == S_VEHICLE_BOAT_STAND) then
					if (not only_empty) or (t.u.Vehicle.NumOccupants == 0) then
						boats = boats + 1
					end
				end
			end
			return true
		end)
		return true
	end)
	
	return boats
end

function vehicle_empty(vehicle)
	return vehicle.u.Vehicle.NumOccupants == 0
end

function get_me_a_random_boat_in_location(c3d, x, z, radius, only_empty)
	local position = c3d or MAP_XZ_2_WORLD_XYZ(x, z)
	local boats = {}
	
	SearchMapCells(SQUARE, 0, 0, radius, world_coord3d_to_map_idx(position), function(me)
		me.MapWhoList:processList(function(t)
			if (t.Type == T_VEHICLE and t.Model == M_VEHICLE_BOAT_1) then
				if (not only_empty) or (vehicle_empty(t)) then
					table.insert(boats, t)
				end
			end
			return true
		end)
		return true
	end)
	
	return randomItemFromTable(boats)
end

--====================================================================================================================================================
-- MISC ---- ======================================================================================================================================== --

private_future_actions = {} --these are playernum based and not synced (so can be called in turn or frame easily)
future_actions = {}

function afa(delay, fn, ...)
	table.insert(future_actions, { t = mg_turn + delay, fn = fn, data = { ... } })
end

function remove_all_future_actions()
	future_actions = {}
end

function process_future_actions()
	for i = #future_actions, 1, -1 do
		local action = future_actions[i]
		
		if mg_turn >= action.t then
			action.fn(table.unpack(action.data))
			table.remove(future_actions, i)
		end
	end
end

function pafa(delay, fn, ...)
	table.insert(private_future_actions, { t = mg_turn + delay, fn = fn, data = { ... } })
end

function remove_all_future_private_actions()
	private_future_actions = {}
end

function process_future_private_actions()
	for i = #private_future_actions, 1, -1 do
		local action = private_future_actions[i]
		
		if mg_turn >= action.t then
			action.fn(table.unpack(action.data))
			table.remove(private_future_actions, i)
		end
	end
end

function btn(bool)
	if bool then return 1 end
	return 0
end

function ntb(num)
	if num == 0 then return false end
	return true
end

function randomItemFromTable(t)
	if (#t == 0) then
		return nil
	end

	local idx = rndb(1, #t)
	return t[idx]
end

function isItemInTable(tbl, item)
	for _, v in ipairs(tbl) do
		if v == item then return true end
	end
	return false
end

function cursor_inside(x, y, w, h)
	if mouseX >= x then
		if mouseX <= x + w then
			if mouseY >= y then
				if mouseY <= y + h then
					return true
				end
			end
		end
	end
	
	return false
end

function get_tagless_short_name(nick)
	local len = string.len(nick)
	local new_nick = nick:sub(1, math.min(len, NICKNAME_MAX_SIZE))
	
	return new_nick
end

function clean_drop_infos()
	if ggameplay.clear_drop_infos then
		local curr = ggameplay.dropIcon
		KILL_ALL_MSG_ID(curr)
		ggameplay.dropIcon = curr + 1
		CLEAR_ALL_MSG()
	end
end

fonts = { [FONT_VERY_SMALL]=6, [FONT_SMALL]=4, [FONT_MEDIUM]=3, [FONT_BIG]=9 }
font_heights = {}

function set_font_heights()
	for f = FONT_VERY_SMALL, mAX_FONTS do
		set_font(f)
		font_heights[f] = CharHeight2()
	end
end

function set_font(new_font)
	--if curr_font == new_font then return end
	
	curr_font = new_font
	PopSetFont(fonts[new_font], 0)
	curr_font_h = font_heights[new_font]
end

function update_string_widths()
	-- changing res was bugging string width functions for some reason...
	set_font(FONT_MEDIUM)
	custom_string_widths.medium_m = string_width("m")
	set_font(FONT_SMALL)
	custom_string_widths.score_w = string_width(" 7777 ")
	custom_string_widths.m = string_width("m")
	custom_string_widths.score_size = string_width("-7777")
	custom_string_widths.setting_max_title = string_width("target score: x")
	custom_string_widths.setting_max_title_low_res = string_width("Duration: ")
	custom_string_widths.setting_max_value = string_width("777777 points")
end

function delete_effect_and_hide_anim_safe(thing)
	if thing then
		if thing.DrawInfo then
			thing.DrawInfo.Flags = EnableFlag(thing.DrawInfo.Flags, DF_THING_NO_DRAW)
		end
		delete_thing_type(thing)
	end
end

-- ======================================================================================================================================== --
-- ========================================================== HUB & STATE ================================================================= --
-- ======================================================================================================================================== --

function set_state(new_state)
	state = new_state
	reset_ingame_cursors()
	reset_zone_hovering()
	reset_mouse_btns()
end

function periodically_find_dc_humans(everySecond2)
	if not everySecond2 then return end

	for i = #ghub.humans_tbl, 1, -1 do
		local player = ghub.humans_tbl[i]
		local player_gsi = _gsi.Players[player]
		if player_gsi.PlayerActive == 0 then
			MP_on_player_disconnect(player)
			table.remove(ghub.humans_tbl, i)
		end
	end
end

function MP_on_player_disconnect(player)
	--LOG("player " .. player .. " disconnected.")
	
	ghub.active_players = ghub.active_players - 1
	local player_tbl = gplayers[player+1]
	
	player_tbl.mode = TRIBE_AI
	player_tbl.difficulty = HUMAN_DC_SET_AI_DIFF
	local old_name = player_tbl.full_name
	local new_name = old_name
	if string.sub(old_name, 1, 3) ~= "AI_" then
		new_name = "AI_" .. old_name
		player_tbl.full_name = new_name
		set_player_name(player, new_name, multiplayer)
	end
	player_tbl.human_to_ai_name = new_name --this field only exists now
	
	if (state == STATE_READY_SET) or (state == STATE_PLAYING) then
		log_msg(8, "Player " .. tostring(player+1) .. " (" .. old_name .. ") has disconnected, and has been replaced by an AI (difficulty " .. AI_difficulties[HUMAN_DC_SET_AI_DIFF] .. ").")
	
		if _tribes_to_participants_ptrs then
			local participant_ptr = get_participant_table(player)
			participant_ptr.ai = true
			participant_ptr.diff = HUMAN_DC_SET_AI_DIFF
			
			if not player_tbl.spectator then
				if scripts_ptr then
					local OnHumanDisconnect = scripts_ptr.OnHumanDisconnect
					if OnHumanDisconnect then
						OnHumanDisconnect(player)
					end
				end
				
				local team_ptr = participant_ptr.team_ptr
				participant_ptr.name = new_name
				
				if team_ptr.is_team then
					for _, v in ipairs(team_ptr.players) do
						if v.tribe == player then
							v.name = new_name
							v.ai_diff = HUMAN_DC_SET_AI_DIFF
							break
						end
					end
				else
					team_ptr.name = new_name
					team_ptr.players[1].name = new_name
					team_ptr.players[1].ai_diff = HUMAN_DC_SET_AI_DIFF
				end
				
				game_draw_cache.scoreboard = nil
			end
		end
	else
		unready_everyone()
	end

	add_msg_to_hub_log(52, HUB_LOG_SYSTEM_MSG_PTR, old_name)
	queue_custom_sound_event(nil, "y_human_dc.wav", 127)
end

function init_player_tables()
	local active_players = 0
	local humans_tbl = {}
	
	for pn = 0, 7 do
		local player = gplayers[pn+1]
		local nick, fullname, shortname
		
		
		if multiplayer then
			
			local player_gsi = _gsi.Players[pn]
			local ptype = player_gsi.PlayerType
			local active = player_gsi.PlayerActive
			
			-- if debug then
				-- if pn == 4 then
					-- ptype = COMPUTER_PLAYER
				-- end
			-- end
			--LOG(pn .. ":   active: " .. tostring(active) .. "   deadcount: " .. player_gsi.DeadCount .. "    type: " .. ptype)
			
			if ptype == HUMAN_PLAYER and player_gsi.DeadCount == 0 then
				player.mode = TRIBE_HUMAN
				nick = get_player_name(pn, multiplayer)
				active_players = active_players + 1
				table.insert(ffateam.players, pn)
				table.insert(humans_tbl, pn)
			else
				player.mode = TRIBE_UNUSED
				nick = "AI_" .. AI_names[pn + 1]
			end
		else
			if pn == TRIBE_BLUE then
				player.mode = TRIBE_HUMAN
				nick = "Player" --get_player_name(pn, multiplayer) 
				active_players = active_players + 1
				table.insert(ffateam.players, pn)
			else
				player.mode = TRIBE_UNUSED
				nick = "AI_" .. AI_names[pn + 1]
			end
		end
		
		player.full_name = nick
		player.short_name = get_tagless_short_name(nick)
	end
	
	ghub.active_players = active_players
	ghub.humans_tbl = humans_tbl
end

function prepare_for_hub()
	DESELECT_ALL_PEOPLE(playernum)
	--set_everyone_enemy()
	refresh_game_map_default_settings()
	set_state(STATE_HUB)
	queue_custom_sound_event(nil, "y_intro_long.wav", 127)
end

function refresh_game_map_default_settings()
	local map_info_ptr = map_list[ghub.selected_map].info
	ghub.map_info_ptr = map_info_ptr
	local default_objectives = map_info_ptr.map_objectives
	
	for k, objective in ipairs(default_objectives) do
		ghub.map_objectives[k] = {}
		local obj_tbl = ghub.map_objectives[k]
		obj_tbl.available = objective.default ~= nil
		obj_tbl.active = objective.default
		obj_tbl.value = objective.value
		obj_tbl.step = objective.step
		obj_tbl.limit = objective.limit or 100
	end
	
	reset_map_rules_offset()
	refresh_map_hover_img()
	refresh_map_custom_rules()
	refresh_circular_maps()
end

function process_log_fading_timer(everySecond1)
	if everySecond1 then
		if ghub.log_fading_timer > 0 then
			ghub.log_fading_timer = ghub.log_fading_timer - 1
		end
	end
end

function increase_card_border()
	if everyFramer(4) then
		local min, max = 1780, 1790
		
		local curr = pmisc.card_border
		pmisc.card_border = curr + 1
		
		if pmisc.card_border > max then
			pmisc.card_border = min
		end
	end
end

function lock_or_unlock_section(section, unlocker)
	local locked = is_location_locked(section)

	if locked then
		add_msg_to_hub_log(34, unlocker, section)
		queue_custom_sound_event(nil, "y_unlock.wav", 127)
	else
		add_msg_to_hub_log(33, unlocker, section)
		queue_custom_sound_event(nil, "y_lock.wav", 127)
	end
	
	teams_ptrs[section].locked = not locked
end

function is_location_locked(section)
	return teams_ptrs[section].locked
end

function shake_lock_section(section, turns)
	local tbl = pmisc.lock
	tbl.id = section
	tbl.moving = turns
	queue_custom_sound_event(nil, "y_locked_place.wav", 127)
end

function process_lock_moving()
	local tbl = pmisc.lock
	
	if tbl.id then
		local curr = tbl.moving
		if curr > 0 then
			tbl.moving = curr - 1
		else
			tbl.id = nil
		end
	end
end

function reset_lock_moving()
	local tbl = pmisc.lock
	tbl.id = nil
	tbl.moving = 0
end

function player_is_spectator(pn)
	return gplayers[pn+1].spectator
end

function swap_spectator(id, clicker)
	local spectator = player_is_spectator(id)
	
	if id == clicker then
		if spectator then
			add_msg_to_hub_log(16, clicker, nil)
		else
			add_msg_to_hub_log(15, clicker, nil)
		end
	else
		if spectator then
			add_msg_to_hub_log(18, clicker, id)
		else
			add_msg_to_hub_log(17, clicker, id)
		end
	end
	
	if spectator then
		queue_custom_sound_event(nil, "y_spec.wav", 127)
	else
		queue_custom_sound_event(nil, "y_spec2.wav", 127)
	end

	gplayers[id+1].spectator = not spectator
	if playernum == id then
		im_spectating = not spectator
	end
	unready_everyone()
end

function swap_ready(id, clicker, left)
	local card_human = gplayers[id+1].mode == TRIBE_HUMAN
	
	if card_human then
		if not left then return end
		
		local ready = gplayers[id+1].ready
		
		if id == clicker then
			if ready then
				add_msg_to_hub_log(12, clicker, nil)
			else
				add_msg_to_hub_log(11, clicker, nil)
				queue_custom_sound_event(nil, "y_ready.wav", 127)
			end
		else
			if ready then
				add_msg_to_hub_log(14, clicker, id)
			else
				add_msg_to_hub_log(13, clicker, id)
				queue_custom_sound_event(nil, "y_ready.wav", 127)
			end
		end
		
		gplayers[id+1].ready = not ready
	else
		if clicker == TRIBE_BLUE then
			local speed = left and 1 or -1
			local ptr = gplayers[id+1]
			local curr = ptr.difficulty
			ptr.difficulty = curr + speed
			if ptr.difficulty < AI_EASY then
				ptr.difficulty = AI_HARD
			elseif ptr.difficulty > AI_HARD then
				ptr.difficulty = AI_EASY
			end
			queue_custom_sound_event(nil, "y_ai_diff.wav", 127)
			add_msg_to_hub_log(44, HUB_LOG_SYSTEM_MSG_PTR, id)
			unready_everyone()
		end
	end
end

function remove_ai_card(clicker, id)
	local ptr = gplayers[id+1]
	local card_human = ptr.mode == TRIBE_HUMAN
	
	if not card_human then
		ptr.mode = TRIBE_UNUSED
		ptr.spectator = false
		ptr.difficulty = AI_EASY
		ghub.active_players = ghub.active_players - 1
		remove_player_from_hub_tbl(id)
		queue_custom_sound_event(nil, "y_remove_ai.wav", 127)
		add_msg_to_hub_log(43, HUB_LOG_SYSTEM_MSG_PTR, ptr.full_name)
		unready_everyone()
		
		if dragging_card() == id then
			reset_dragging_card()
		end
	end
end

function player_is_in_this_hub_team(pn, section)
	for _, player in ipairs(teams_ptrs[section].players) do 
		if player == pn then
			return true
		end
	end
	return false
end

function section_has_free_slot(section)
	local limit = section == 1 and 8 or 4
	return #teams_ptrs[section].players < limit
end

function unready_everyone()
	--except AI
	for pn = 0, 7 do
		if gplayers[pn+1].mode ~= TRIBE_AI then
			gplayers[pn+1].ready = false
		end
	end
end

function insert_AI_to_hub()
	local active = ghub.active_players
	if active >= 8 then return end
	
	ghub.active_players = active + 1
	
	for pn = 0, 7 do
		local player = gplayers[pn+1]
		local mode = player.mode

		if mode == TRIBE_UNUSED then
			player.mode = TRIBE_AI
			player.ready = true
			local nick = player.human_to_ai_name or "AI_" .. AI_names[pn+1]
			player.full_name = nick
			player.short_name = get_tagless_short_name(nick)
			table.insert(ffateam.players, pn)
			queue_custom_sound_event(nil, "y_add_ai.wav", 127)
			add_msg_to_hub_log(42, HUB_LOG_SYSTEM_MSG_PTR, nick)
			break
		end
	end
	
	unready_everyone()
end

function dragging_card()
	return pmisc.dragging_card
end

function reset_map_rules_offset()
	pmisc.rules_offset = 0
end

function refresh_map_custom_rules()
	local map_ptr = ghub.map_info_ptr
	if not map_ptr.custom_rules then return end
	local rules_cache_tbl = ghub.custom_rules
	for k, rule in ipairs(map_ptr.custom_rules) do
		rules_cache_tbl[k] = rule.default
	end
end

function set_all_custom_rules_except(bool, exception)
	local rules_cache_tbl = ghub.custom_rules
	for k, rule in ipairs(rules_cache_tbl) do
		rules_cache_tbl[k] = (k == exception) and (not bool) or bool
	end
	unready_everyone()
end

function is_map_rule_active(id)
	local rules_cache_tbl = ghub.custom_rules
	return rules_cache_tbl[id] or false
end

function OnMgInitGeneral()
	local map_ptr = ghub.map_info_ptr
	if not map_ptr.custom_rules then return end

	for k, rule in ipairs(map_ptr.custom_rules) do
		if rule.init then
			if is_map_rule_active(k) then
				rule.init()
				--LOG("inited rule " .. k)
			end
		end
	end
end

function swap_advanced_option(id, clicker)
	local rules_cache_tbl = ghub.custom_rules
	local was_enabled = rules_cache_tbl[id]
	rules_cache_tbl[id] = not was_enabled
	
	local msg = true
	
	local info = ghub.map_info_ptr
	if info then
		if info.custom_rules then
			if info.custom_rules[id] then
				local onpostclick = info.custom_rules[id].OnPostClick
				if onpostclick then
					onpostclick(id, clicker)
				end
				
				local no_click_msg = info.custom_rules[id].no_click_msg
				if no_click_msg then
					msg = false
				end
			end
		end
	end
	
	if was_enabled then
		queue_custom_sound_event(nil, msg and "y_c_rule2.wav" or "y_c_rule.wav", 127)
		if msg then add_msg_to_hub_log(46, clicker, id) end
	else
		queue_custom_sound_event(nil, "y_c_rule.wav", 127)
		if msg then add_msg_to_hub_log(45, clicker, id) end
	end
	
	unready_everyone()
end

function swap_map_objective(id, clicker)
	local objective = ghub.map_objectives[id]
	local enable = not objective.active
	
	if enable then
		objective.active = true
		add_msg_to_hub_log(20 + (id-1)*2, clicker, nil)
		unready_everyone()
		queue_custom_sound_event(nil, "y_add_setting.wav", 127)
	else
		if active_objectives_greater_than_1() then
			objective.active = false
			add_msg_to_hub_log(21 + (id-1)*2, clicker, nil)
			unready_everyone()
			queue_custom_sound_event(nil, "y_remove_setting.wav", 127)
		end
	end
	
	unready_everyone()
end

function active_objectives_greater_than_1()
	local amt = 0
	
	for _, objective in ipairs(ghub.map_objectives) do
		if objective.active then amt = amt + 1 end
	end
	
	return amt > 1
end

function modify_objective_value(id, is_increase, clicker)
	local objective = ghub.map_objectives[id]
	local curr = objective.value
	local step = objective.step
	
	if is_increase then
		local limit = objective.limit
		local new_value = math.min(curr + step, limit)
		if curr == new_value then return end
		objective.value = new_value
		queue_custom_sound_event(nil, "y_setting_inc.wav", 127)
	else
		local new_value = math.max(step, curr - step)
		if curr == new_value then return end
		objective.value = new_value
		queue_custom_sound_event(nil, "y_setting_dec.wav", 127)
	end
	
	unready_everyone()
	add_msg_to_hub_log(26+btn(not is_increase) + (id-1)*2, clicker, nil)
end

function change_map(clicker, direction, new_map_id)
	local curr_map = ghub.selected_map
	local new_map
	
	if direction then
		local offset = font_heights[FONT_SMALL] + 4
		if direction == CHANGE_MAP_PREVIOUS then
			new_map = curr_map - 1
			if new_map < 1 then new_map = MAX_MAPS end
			ghub.map_changer_offset = -offset
		else
			new_map = curr_map + 1
			if new_map > MAX_MAPS then new_map = 1 end
			ghub.map_changer_offset = offset
		end
	else
		ghub.map_changer_offset = 0
	end
	
	ghub.selected_map = new_map_id or new_map
	refresh_game_map_default_settings()
	unready_everyone()
	add_msg_to_hub_log(19, clicker, nil)
	queue_custom_sound_event(nil, "y_change_map.wav", 127)
	refresh_circular_maps(new_map)
	reset_map_searching()
end

function refresh_circular_maps(current)
	--maps in circular fashion
	local curr_map = current or ghub.selected_map
    local range = CACHE_NEXT_MAPS_EACH_DIRECTION
    ghub.circular_maps_cache = {}
	
    for i = -range, range do
        local id = ((curr_map - 1 + i) % MAX_MAPS) + 1
		local map_name = map_list[id].info.name
        table.insert(ghub.circular_maps_cache, map_name)
    end
end

function start_map_searching()
	local search = pmaster.searching
	if search.active then return end
	search.active = true
end

function reset_map_searching()
	pmaster.searching = { active = false, str = "", map_list = {} }
end

function update_searching_maps(fresh)
	local search = pmaster.searching
	local str_searcher = string.lower(search.str)
	local maps_list
	local updated_list = {}
	
	if fresh then
		search.map_list = {}
		maps_list = map_list_names
	else
		maps_list = #search.map_list == 0 and map_list_names or search.map_list
	end
	
	for _, map in ipairs(maps_list) do
		local name = string.lower(map)
		
		if string.find(name, str_searcher, 1, true) then
			table.insert(updated_list, map)
		-- else
			-- --map_list_authors
			-- local author = string.lower(map.author)
			
			-- if string.find(author, str_searcher, 1, true) then
				-- table.insert(search.map_list, map)
			-- end
		end
	end
	
	search.map_list = updated_list
end

function searching_type(key)
	if not im_host then return true end
	local search = pmaster.searching
	if not search.active then return false end
	
	local curr = #search.str
	local add = _inputKeys[key]
	
	if add then
		if key == LB_KEY_BACKSPACE then
			if curr > 0 then
				if curr == 1 then
					search.map_list = {}
					search.str = ""
				else
					search.str = search.str:sub(1, -2)
					update_searching_maps(true)
				end
			end
		else
			if not (curr == 0 and key == LB_KEY_SPACE) then
				if curr < SEARCHING_MAX_CHARS then
					search.str = search.str .. add
					update_searching_maps(false)
					return false
				end
			end
		end
	end
	
	return true
	
	--if p remove pause
end

function manual_awarding_pts(key)
	if not im_host then return end
	local shift, ctrl = pmisc.l_shift_pressing, pmisc.l_ctrl_pressing
	if (not shift) and (not ctrl) then return end
	if shift and ctrl then return end
	if ggameplay.countdown > 0 then return end

	if (key >= 49 and key <= 56) or (key >= 65 and key <= 68) then
		local pts = shift and 5 or 1
		local single_tribe = ((key >= 49) and (key <= 56))
		local team_ptr
		local packet_team
		local ptr_uid --(1-12)
		
		if single_tribe then
			local tribe = key - 48
			team_ptr = gplayers[tribe]
			packet_team = tostring(tribe - 1)
			ptr_uid = tribe
		else
			local idx = key - 64
			team_ptr = teams_ptrs[idx + 1]
			local ttbl = {"a","b","c","d"}
			packet_team = ttbl[idx]
			ptr_uid = 8 + idx
		end
		
		if multiplayer then
			local data = encode_boolean(shift)..encode_boolean(ctrl)..encode_base62(key)
			send_packet_now("f", data)
		else
			team_ptr.points = team_ptr.points + pts
			ghub.awarded_manual_points[ptr_uid] = ghub.awarded_manual_points[ptr_uid] + pts
			add_msg_to_hub_log(50, HUB_LOG_SYSTEM_MSG_PTR, pts, packet_team)
			ghub.did_award_manual_points = true
		end
	end
end

function manual_awarding_pts_multiplayer(from, shift, ctrl, key)
	if from ~= TRIBE_BLUE then return end
	
	if (key >= 49 and key <= 56) or (key >= 65 and key <= 68) then
		local pts = shift and 5 or 1
		local single_tribe = ((key >= 49) and (key <= 56))
		local team_ptr
		local packet_team
		local ptr_uid --(1-12)
		
		if single_tribe then
			local tribe = key - 48
			team_ptr = gplayers[tribe]
			packet_team = tostring(tribe - 1)
			ptr_uid = tribe
		else
			local idx = key - 64
			team_ptr = teams_ptrs[idx + 1]
			local ttbl = {"a","b","c","d"}
			packet_team = ttbl[idx]
			ptr_uid = 8 + idx
		end
		
		team_ptr.points = team_ptr.points + pts
		ghub.awarded_manual_points[ptr_uid] = ghub.awarded_manual_points[ptr_uid] + pts
		add_msg_to_hub_log(50, HUB_LOG_SYSTEM_MSG_PTR, pts, packet_team)
		ghub.did_award_manual_points = true
	end
end

function map_cache_id_to_real_id(id)
	local maps_cache_name = pmaster.searching.map_list[id]
	
	for map = 1, MAX_MAPS do
		local name = map_list[map].info.name
		if name == maps_cache_name then return map end
	end
end

points_systems = {
					{ name = "classic normal", 			distribution = {8,7,6,5,4,3,2,1}, 			spr = 1828,		dist_str = "8,7,6,5,4,3,2,1"		},
					{ name = "classic hardcore", 		distribution = {15,11,8,5,4,2,1,0}, 		spr = 2515,		dist_str = "15,11,8,5,4,2,1,0" 		},
					{ name = "podium normal", 			distribution = {3,2,1,0,0,0,0,0}, 			spr = 1941,		dist_str = "3,2,1,0,0,0,0,0" 		},
					{ name = "podium hardcore", 		distribution = {3,2,1,0,0,-1,-2,-3}, 		spr = 1942,		dist_str = "3,2,1,0,0,-1,-2,-3" 	},
					{ name = "heroic normal", 			distribution = {1,0,0,0,0,0,0,0}, 			spr = 1834,		dist_str = "1,0,0,0,0,0,0,0" 		},
					{ name = "heroic hardcore", 		distribution = {3,-1,-2,-3,-4,-5,-6,-7}, 	spr = 1835,		dist_str = "3,-1,-2,-3,-4,-5,-6,-7" },
					{ name = "deadly normal", 			distribution = {4,3,2,1,-1,-2,-3,-4}, 		spr = 1830,		dist_str = "4,3,2,1,-1,-2,-3,-4" 	},
					{ name = "deadly hardcore", 		distribution = {8,6,4,2,-2,-4,-6,-8}, 		spr = 1831,		dist_str = "8,6,4,2,-2,-4,-6,-8" 	},
					{ name = "no points", 				distribution = {0,0,0,0,0,0,0,0}, 			spr = 1836,		dist_str = "0,0,0,0,0,0,0,0"		},
}

points_systems_longest_string = "classic hardcore"
points_systems_longest_pts_string = "3,-1,-2,-3,-4,-5,-6,-7"

function hub_mid_btns(id, left, clicker)
	
	if id == BOT_MIDDLE_BUTTON_DISABLE_EMOJIS then
		if left then
			gmisc.emojis_enabled = not gmisc.emojis_enabled
			queue_custom_sound_event(nil, "20_Eat1.wav", 127)
			local str = gmisc.emojis_enabled and "enabled" or "disabled"
			add_msg_to_hub_log(53, clicker, str)
		end
	
	--(deprecated. AIs will have individual difficulties.)
	-- if id == BOT_MIDDLE_BUTTON_DISABLE_EMOJIS then
		-- local curr = ggameplay.AI_difficulty
		
		-- if left then
			-- ggameplay.AI_difficulty = curr + 1
			-- if curr + 1 > AI_MAX_DIFFICULTIES - 1 then
				-- ggameplay.AI_difficulty = 0
			-- end
		-- else
			-- ggameplay.AI_difficulty = curr - 1
			-- if curr - 1 < 0 then
				-- ggameplay.AI_difficulty = AI_MAX_DIFFICULTIES - 1
			-- end
		-- end
		
		-- queue_custom_sound_event(nil, "y_map_points_system.wav", 127)
		-- add_msg_to_hub_log(44, clicker, nil)
	elseif id == BOT_MIDDLE_BUTTON_POINT_SYSTEM then
		reset_point_system_cache()
		local curr = ggameplay.map_target_score_type
		
		if left then
			ggameplay.map_target_score_type = curr + 1
			if curr + 1 > POINTS_SYSTEM_MAX_TYPES then
				ggameplay.map_target_score_type = 1
			end
		else
			ggameplay.map_target_score_type = curr - 1
			if curr - 1 < 1 then
				ggameplay.map_target_score_type = POINTS_SYSTEM_MAX_TYPES
			end
		end
		
		queue_custom_sound_event(nil, "y_map_points_system.wav", 127)
		add_msg_to_hub_log(39, clicker, nil)
		unready_everyone()
	end
end

function reset_point_system_cache()
	hub_cache.point_system_cache = nil
end

function an_ai_exists()
	for k, player in ipairs(gplayers) do
		if player.mode == TRIBE_AI then return true end
	end
	return false
end

function get_host_name()
	return gplayers[1].full_name
end

hub_msg_ids = {
				[1] = function(ptr1) local name1 = gplayers[ptr1+1].full_name return string.format("%s joined the solo players", name1) end,
				[2] = function(ptr1) local name1 = gplayers[ptr1+1].full_name return string.format("%s joined team A", name1) end,
				[3] = function(ptr1) local name1 = gplayers[ptr1+1].full_name return string.format("%s joined team B", name1) end,
				[4] = function(ptr1) local name1 = gplayers[ptr1+1].full_name return string.format("%s joined team C", name1) end,
				[5] = function(ptr1) local name1 = gplayers[ptr1+1].full_name return string.format("%s joined team D", name1) end,
				
				[6] = function(ptr1, ptr2) local name1, name2 = gplayers[ptr1+1].full_name, gplayers[ptr2+1].full_name return string.format("%s moved %s to the solo players", name1, name2) end,
				[7] = function(ptr1, ptr2) local name1, name2 = gplayers[ptr1+1].full_name, gplayers[ptr2+1].full_name return string.format("%s moved %s to team A", name1, name2) end,
				[8] = function(ptr1, ptr2) local name1, name2 = gplayers[ptr1+1].full_name, gplayers[ptr2+1].full_name return string.format("%s moved %s to team B", name1, name2) end,
				[9] = function(ptr1, ptr2) local name1, name2 = gplayers[ptr1+1].full_name, gplayers[ptr2+1].full_name return string.format("%s moved %s to team C", name1, name2) end,
				[10] = function(ptr1, ptr2) local name1, name2 = gplayers[ptr1+1].full_name, gplayers[ptr2+1].full_name return string.format("%s moved %s to team D", name1, name2) end,
				
				[11] = function(ptr1) local name1 = gplayers[ptr1+1].full_name return string.format("%s is ready!", name1) end,
				[12] = function(ptr1) local name1 = gplayers[ptr1+1].full_name return string.format("%s is not ready...", name1) end,
				[13] = function(ptr1, ptr2) local name1, name2 = gplayers[ptr1+1].full_name, gplayers[ptr2+1].full_name return string.format("%s changed %s's status to ready!", name1, name2) end,
				[14] = function(ptr1, ptr2) local name1, name2 = gplayers[ptr1+1].full_name, gplayers[ptr2+1].full_name return string.format("%s removed %s's ready status...", name1, name2) end,
				
				[15] = function(ptr1) local name1 = gplayers[ptr1+1].full_name return string.format("%s will be a spectator.", name1) end,
				[16] = function(ptr1) local name1 = gplayers[ptr1+1].full_name return string.format("%s will be a player.", name1) end,
				[17] = function(ptr1, ptr2) local name1, name2 = gplayers[ptr1+1].full_name, gplayers[ptr2+1].full_name return string.format("%s set %s as spectator", name1, name2) end,
				[18] = function(ptr1, ptr2) local name1, name2 = gplayers[ptr1+1].full_name, gplayers[ptr2+1].full_name return string.format("%s set %s as player", name1, name2) end,
				
				[19] = function(ptr1) local name1 = gplayers[ptr1+1].full_name return string.format("%s changed the mini-game to '%s'", name1, ghub.map_info_ptr.name) end,
				
				[20] = function(ptr1) local name1 = gplayers[ptr1+1].full_name return string.format("%s has enabled a max duration ( game ends after %s minutes )", name1, ghub.map_objectives[1].value) end,
				[21] = function(ptr1) local name1 = gplayers[ptr1+1].full_name return string.format("%s has disabled the max duration", name1) end,	
				[22] = function(ptr1) local name1 = gplayers[ptr1+1].full_name return string.format("%s has enabled a target score ( game ends when someone reaches %s points )", name1, ghub.map_objectives[2].value) end,
				[23] = function(ptr1) local name1 = gplayers[ptr1+1].full_name return string.format("%s has disabled the target score", name1) end,
				[24] = function(ptr1) local name1 = gplayers[ptr1+1].full_name return string.format("%s has enabled a max rounds ( game ends after %s rounds )", name1, ghub.map_objectives[3].value) end,
				[25] = function(ptr1) local name1 = gplayers[ptr1+1].full_name return string.format("%s has disabled the max rounds", name1) end,
				
				[26] = function(ptr1) local name1 = gplayers[ptr1+1].full_name return string.format("%s increased the 'max duration' to %s minutes", name1, ghub.map_objectives[1].value) end,
				[27] = function(ptr1) local name1 = gplayers[ptr1+1].full_name return string.format("%s decreased the 'max duration' to %s minutes", name1, ghub.map_objectives[1].value) end,
				[28] = function(ptr1) local name1 = gplayers[ptr1+1].full_name return string.format("%s increased the 'target score' to %s", name1, ghub.map_objectives[2].value) end,
				[29] = function(ptr1) local name1 = gplayers[ptr1+1].full_name return string.format("%s decreased the 'target score' to %s", name1, ghub.map_objectives[2].value) end,
				[30] = function(ptr1) local name1 = gplayers[ptr1+1].full_name return string.format("%s increased the 'max rounds' to %s", name1, ghub.map_objectives[3].value) end,
				[31] = function(ptr1) local name1 = gplayers[ptr1+1].full_name return string.format("%s decreased the 'max rounds' to %s", name1, ghub.map_objectives[3].value) end,
				
				[32] = function(ptr1) local name1 = gplayers[ptr1+1].full_name return string.format("%s has started the game!", name1) end,
				
				[33] = function(ptr1, section) local sect_name = teams_ptrs[section].default_name return string.format("%s has locked %s.", get_host_name(), sect_name) end,
				[34] = function(ptr1, section) local sect_name = teams_ptrs[section].default_name return string.format("%s has unlocked %s.", get_host_name(), sect_name) end,
				
				[35] = function(ptr1) local name1 = gplayers[ptr1+1].full_name return string.format("%s has cancelled the game!", name1) end,
				
				--sound enable/disable will now be local
				--[36] = function(ptr1) local name1 = gplayers[ptr1+1].full_name return string.format("%s has disabled the sounds", name1) end,
				--[37] = function(ptr1) local name1 = gplayers[ptr1+1].full_name return string.format("%s has enabled the sounds", name1) end,
				
				[38] = function(ptr1) local name1 = gplayers[ptr1+1].full_name return string.format("%s tried to start the game, but there are less than 2 participants and/or teams.", name1) end,
				[39] = function(ptr1) local name1 = gplayers[ptr1+1].full_name return string.format("%s changed the points system to %s.", name1, points_systems[ggameplay.map_target_score_type].name) end,
				[40] = function(ptr1) local name1 = gplayers[ptr1+1].full_name return string.format("%s tried to start the game, but not everyone is ready.", name1) end,
				[47] = function(ptr1) local name1 = gplayers[ptr1+1].full_name return string.format("%s tried to start the game, but the last map is still unforging. Wait a few more moments.", name1) end,
				[54] = function(ptr1) local name1 = gplayers[ptr1+1].full_name return string.format("%s tried to start the game, butthis level is disabled for multiplayer (when human players > 1).", name1) end,
				
				[45] = function(ptr1, ptr2) local name1 = gplayers[ptr1+1].full_name return string.format("%s added advanced option #%s.", name1, ptr2) end,
				[46] = function(ptr1, ptr2) local name1 = gplayers[ptr1+1].full_name return string.format("%s removed advanced option #%s.", name1, ptr2) end,
				
				
				--system msgs
				[41] = function() return string.format("Mini-Game %s has ended, and points have been awarded to players/teams.", ghub.games_played) end,
				[48] = function() return string.format("Mini-Game %s has ended prematurely by the host, but points have still been awarded to players/teams.", ghub.games_played) end,
				[49] = function() return string.format("Mini-Game %s has ended prematurely by the host, and no points have been awarded.", ghub.games_played) end,
				[50] = function(ptr1, points, team_str) local points_str = points > 1 and "points" or "point" return string.format("The host has manually awarded %s %s to %s.", points, points_str, team_special_ptr_to_str(team_str)) end,
				[51] = function() return "The previous map is now done unforging. It is now safe to start the game." end,
				[52] = function(ptr1, ptr2) return string.format("%s has disconnected, and had been replaced by an AI. Right click their card to remove them.", ptr2) end,
				[53] = function(ptr1, ptr2) return string.format("In-game emojis %s.", ptr2) end,
				--(ai msgs)
				[42] = function(ptr1, ptr2) return string.format("AI inserted: %s. Right click their card to remove them.", ptr2) end,
				[43] = function(ptr1, ptr2) return string.format("AI removed: %s.", ptr2) end,
				--[44] = function(ptr1) local name1 = gplayers[ptr1+1].full_name return string.format("%s changed AI's difficulty to %s.", name1, AI_difficulties[ggameplay.AI_difficulty]) end, --deprecated. now AIs
				[44] = function(ptr1, ptr2) local ai_ptr = gplayers[ptr2+1] return string.format("AI %s's difficulty set to %s.", ai_ptr.full_name, AI_difficulties[ai_ptr.difficulty] or "???") end,
}

function team_special_ptr_to_str(team_str)
	local str = ""
	local team_map = { a="Team A", b="Team B", c="Team C", d="Team D" }
	local ptr = team_map[team_str]
	
	if not ptr then
		ptr = gplayers[team_str+1]
		if ptr then
			str = ptr.full_name
		end
	else
		str = ptr
	end
	
	return str
end

function add_msg_to_hub_log(hub_msg_id, ptr1, ptr2, ptr3)
	local hub_log = ghub.hub_log
	local str = hub_msg_ids[hub_msg_id](ptr1, ptr2, ptr3)
	local seconds = seconds()
	local timer_str = secondsToClock(seconds)
	
	if (ptr1 ~= HUB_LOG_SYSTEM_MSG_PTR) then
		local color = hub_msg_id ~= 44 and gplayers[ptr1+1].color or 11
		local t = {str=str, color=color, host=hub_msg_id ~= 44 and im_host or false, seconds=seconds, timer_str=timer_str}
		
		table.insert(hub_log, t)
	elseif ptr1 == HUB_LOG_SYSTEM_MSG_PTR then
		local ai_msgs = {42,43,44}
		local clr = isItemInTable(ai_msgs, hub_msg_id) and 11 or 0
		local t = {str=str, color=clr, host=false, seconds=seconds, timer_str=timer_str}
		
		table.insert(hub_log, t)
	end
	
	ghub.log_fading_timer = MAX_LOG_FADING_TIMER

	if #hub_log > 64 then
		for i = #hub_log // 2, 1, -1 do
			if hub_log[i] then
				table.remove(hub_log, i)
			end
		end
	end
end

function remove_player_from_hub_tbl(dragging_card)
	for _, tbl in ipairs(teams_ptrs) do
		for k, player in ipairs(tbl.players) do
			if player == dragging_card then
				table.remove(tbl.players, k)
				return
			end
		end
	end
end

function insert_card_to_section(clicker, pn, section)
	table.insert(teams_ptrs[section].players, pn)
	gplayers[pn+1].spectator = false
	queue_custom_sound_event(nil, "y_move_player.wav", 127)
	if clicker == pn then
		add_msg_to_hub_log(section, clicker, nil)
	else
		add_msg_to_hub_log(5+section, clicker, pn)
	end
	unready_everyone()
end

function try_to_apply_dragging_card()
	local dragging_card = dragging_card()
	
	if dragging_card then
		if hovering_zone then
			if multiplayer then
				local data = tostring(dragging_card)..tostring(hovering_zone)
				send_packet_now("i", data)
			else
				if section_has_free_slot(hovering_zone) then
					local locked = is_location_locked(hovering_zone)
					
					if im_host or not locked then
						if not player_is_in_this_hub_team(dragging_card, hovering_zone) then
							remove_player_from_hub_tbl(dragging_card)
							insert_card_to_section(playernum, dragging_card, hovering_zone)
						end
					else
						shake_lock_section(hovering_zone, LOCK_SHAKE_TURNS_DUR)
					end
				end
			end
		end
		reset_dragging_card()
	end
end

function try_to_apply_dragging_card_multiplayer(card, zone, dragger)
	if section_has_free_slot(zone) then
		local locked = is_location_locked(zone)
		
		if dragger == TRIBE_BLUE or not locked then
			if not player_is_in_this_hub_team(card, zone) then
				remove_player_from_hub_tbl(card)
				insert_card_to_section(dragger, card, zone)
			end
		else
			if playernum == dragger then
				shake_lock_section(zone, LOCK_SHAKE_TURNS_DUR)
			end
		end
	end

	if dragging_card() == card then
		reset_dragging_card()
	end
end

function reset_start_game_countdown()
	ggameplay.countdown = -1
end

function process_start_game_countdown()
	local curr_countdown = ggameplay.countdown
	if curr_countdown > 0 then
		ggameplay.countdown = curr_countdown - 1
		if ((curr_countdown) % 12 == 0) and (curr_countdown - 1 > 0) then
			ggameplay.processing = true
			set_map_powerups_pointer()
			refresh_powerups_cdrs()
			queue_custom_sound_event(nil, "y_swap_soundboard.wav", 127)
		end
	elseif curr_countdown == 0 then
		set_state(STATE_FORGING)
		reset_start_game_countdown()
		set_map_texture(false)
	end
end

function start_game(clicker)
	local curr_countdown = ggameplay.countdown
	local start_game = (curr_countdown == -1)
	local humans = #ghub.humans_tbl
	
	if start_game then
		if everyone_ready() or debug or (multiplayer and (humans < 2)) then
			local participants = count_individual_teams_or_participants()

			if participants > 1 or debug then
				if not gforge.unforging then
					if (ghub.selected_map ~= MAP_POTION_BATTLE) or (humans <= 1) then
						ggameplay.countdown = START_GAME_COUNTDOWN * 12
						reset_dragging_card()
						scripts_ptr = map_list[ghub.selected_map].scripts
						add_msg_to_hub_log(32, clicker, nil)
					else
						queue_custom_sound_event(nil, "y_unable.wav", 127)
						add_msg_to_hub_log(54, clicker, nil)
					end
				else
					queue_custom_sound_event(nil, "y_unable.wav", 127)
					add_msg_to_hub_log(47, clicker, nil)
					gforge.unforging_done_notificate = true
				end
			else
				queue_custom_sound_event(nil, "y_unable.wav", 127)
				add_msg_to_hub_log(38, clicker, nil)
			end
		else
			queue_custom_sound_event(nil, "y_unable.wav", 127)
			add_msg_to_hub_log(40, clicker, nil)
		end
	else
		reset_start_game_countdown()
		queue_custom_sound_event(nil, "y_unable.wav", 127)
		add_msg_to_hub_log(35, clicker, nil)
	end
end

function everyone_ready()
	if not multiplayer then
		return true
	else
		if ghub.active_players <= 1 then
			return true
		else
			for _, player in ipairs(gplayers) do
				--if not player.spectator then -- should this be enabled?
					if player.mode == TRIBE_HUMAN then
						if not player.ready then
							return false
						end
					end
				--end
			end
			
			return true
		end
	end
end

function count_individual_teams_or_participants()
	local count = 0
	
	for _, v in ipairs(ffateam.players) do
		local player = gplayers[v+1]
		
		if not player.spectator then
			count = count + 1
		end
	end
	
	for i = 2, 5 do
		if #teams_ptrs[i].players > 0 then
			count = count + 1
		end
	end
	
	return count
end

function set_map_texture(hub)
	if hub then
		set_level_type(1)
		set_object_type(0)
	else
		local txt, bank
		local txt_ptr = ghub.map_info_ptr.texture
		if type(txt_ptr) == "function" then
			txt, bank = table.unpack(txt_ptr())
		else
			txt, bank = txt_ptr[1], txt_ptr[2]
		end
		
		set_level_type(txt)
		set_object_type(bank)
	end
end

function process_ready_set()
	local curr = gmisc.ready_set_timer
	gmisc.ready_set_timer = curr - 1
	curr = curr - 1
	if curr <= 0 then
		queue_custom_sound_event(nil, "476_bell3.wav", 127)
		tmi[TMI_PERSON_MEDICINE_MAN].BaseSpeed = ggameplay.shaman_speed
		set_state(STATE_PLAYING)
		for _, player in ipairs(_participants) do
			local tribe = player.tribe
			
			if playernum == tribe then
				if nilS(tribe) then
					local angle = 0
					local optional_angle = ghub.map_info_ptr.optional_shaman_zoom_angle
					if optional_angle then
						angle = type(optional_angle) == "function" and optional_angle(tribe) or optional_angle
					end
					ZoomThing(getShaman(tribe), angle)
				end
			end
		end
	else
		if curr % 12 == 0 then
			queue_custom_sound_event(nil, "y_swap_soundboard.wav", 127)
		end
	end
end

function process_mg_timer(everySecond1)
	mg_turn = mg_turn + 1
	
	if everySecond1 then
		local curr = ggameplay.map_curr_duration + 1
		ggameplay.map_curr_duration = curr
		ggameplay.cached_curr_values_strings[MAP_OBJECTIVE_TIMER] = secondsToClock(curr)
		local tbl = ghub.map_objectives[MAP_OBJECTIVE_TIMER]
		
		if tbl.active then
			if curr >= tbl.value * 60 then
				if not mg_vars.ignore_timer_objective then
					end_game()
				end
			end
		end
	end
end

function process_final_results_timer()
	local curr = gmisc.before_results_timer - 1
	gmisc.before_results_timer = math.max(0, curr)
	
	if curr == 0 then
		destroy_all_emojis()
		reset_emoji_wheel()
		clear_power_ups()
		init_deforging()
	end
end

function before_results_frame()
	local curr = gmisc.before_results_timer
	local percent = XpercentOf_fixed(curr, BEFORE_RESULTS_TIMER)
	local w, h = floor(((W - GUIW) * percent) / 100), H // 64
	DrawBox(GUIW-1, 1, w+2, h, 1)
	DrawBox(GUIW, 0, w, h, 14)
end

function award_pts_to_participants(host_hard_ended_it)
	local no_point_awarding_round
	
	if host_hard_ended_it then
		if host_hard_ended_it == HARDEND_MG_MODE_NO_PTS then
			no_point_awarding_round = true
			add_msg_to_hub_log(49, HUB_LOG_SYSTEM_MSG_PTR, nil)
			leave_final_results_to_hub()
			--return
			for _, team in ipairs(_participants_by_teams) do
				team.points = 0
			end
		elseif host_hard_ended_it == HARDEND_MG_MODE_AWARD_PTS then
			add_msg_to_hub_log(48, HUB_LOG_SYSTEM_MSG_PTR, nil)
		end
	end
	
	gmisc.post_mg_table = {}
	local map = ghub.selected_map
	local map_name = ghub.map_info_ptr.name
	local pts_distribution = points_systems[ggameplay.map_target_score_type]
	local curr_place = 1

	while (#_participants_by_teams > 0) do
		local highest = -math.huge
		
		for _, team in ipairs(_participants_by_teams) do
			local score = team.points
			
			if score > highest then
				highest = score
			end
		end

		for i = #_participants_by_teams, 1, -1 do
			local team = _participants_by_teams[i]
			
			if team.points == highest then
				team.position = curr_place
				local awarded = no_point_awarding_round and 0 or pts_distribution.distribution[curr_place]
				team.awarded = awarded
				table.insert(gmisc.post_mg_table, { uid = team.uid, name = team.name, players = team.players, score = highest, position = curr_place, awarded = awarded })
				table.remove(_participants_by_teams, i)
				
				if not no_point_awarding_round then
					local global_tbl = team.global_table
					global_tbl.points = global_tbl.points + awarded
					-- if global_tbl.played_games then
						-- global_tbl.played_games = global_tbl.played_games + 1
					-- end
				end
			end
		end
		
		curr_place = curr_place + 1
	end

	local map_duration = ggameplay.map_curr_duration
	--local highest_score_reached = ggameplay.highest_score
	--local round_reached = ggameplay.map_curr_round
	local tts = {
		"The game ended as the time limit was reached.",
		"The game ended as a player reached the score limit.",
		"The game ended as the round limit was reached."
	}
	
	local stats = {}
	for _, obj in ipairs(game_draw_cache.map_objectives) do
		local uid = obj.uid
		local raw_goal = obj.raw_value
		local max_reached = obj.curr_value_ptr()
		local max_reached_str = (uid == MAP_OBJECTIVE_TIMER) and secondsToClock(max_reached) or tostring(max_reached)
		local goal = obj.value_integer
		local goal_str = obj.value
		local obj_was_reached = (max_reached >= goal)
		local tt = obj_was_reached and string.format(tts[uid]) or ""
		
		--LOG(raw_goal .. " " .. goal .. " " .. max_reached)
		stats[uid] = { uid=uid, raw_goal=raw_goal, max_reached=max_reached, max_reached_str=max_reached_str, goal=goal, goal_str=goal_str, flash=obj_was_reached, tt=tt }
	end
	
	ghub.total_game_time = ghub.total_game_time + map_duration
	table.insert(gmisc.past_played_minigames, { map = map, map_name = map_name, pts_system = ggameplay.map_target_score_type, teams = gmisc.post_mg_table, stats = stats, cached_duration_str = secondsToClock(map_duration)--[[, special_end_mode = host_hard_ended_it]] })
end

function draw_mg_end_results()
	local h, w = H, W
	
	if gmisc.before_results_timer > 0 then return end
	if not no_menus_open() then return end
	
	local hover = false
	local f1, f2, f3 = FONT_MEDIUM, FONT_SMALL, FONT_BIG
	PopSetFont(f3, 0)
	local f3_h = CharHeight2()
	PopSetFont(f2, 0)
	local f2_h = CharHeight2()
	PopSetFont(f1, 0)
	local f1_h = CharHeight2()
	local font_giant_h = 28
	local thickness = 4
	local objectives_h = f1_h+font_giant_h+f3_h+4
	local y_gap = math.max(8, h // 64)
	local top_total_h_and_y = y_gap*3 + objectives_h + font_giant_h--f3_h
	local bot_total_h = 4 + f1_h + y_gap
	local remaining_zone_h = h - (top_total_h_and_y + bot_total_h)
	
	
	--local y = math.max(8+f1_h+16+8+thickness+f1_h, mf(h, 8))
	local h_o = mf(h, 24)
	local each_podium_stack = mf(h, 18)
	local item_w = mf(w, 16)
	local o_w = mf(w, 38)
	local max_items = 8 --#post_mg_table
	local total_w = (item_w + o_w) * max_items + o_w
	local total_h = each_podium_stack * 9 + h_o*2 + f1_h*2 + f2_h + 8 + 16
	local x = mf(w - total_w, 2)
	local center = mf(w, 2)
	
	local minY = top_total_h_and_y
	local maxY = math.max(minY, top_total_h_and_y + top_total_h_and_y//2 - total_h//2)
	local y = clamp(h//8, minY, maxY)
	
	
	--   { uid=uid, raw_goal=raw_goal, max_reached=max_reached, max_reached_str=max_reached_str, goal=goal, goal_str=goal_str, spr=spr }
	
	
	local stats_ptr = gmisc.past_played_minigames[ghub.games_played]
	local stats = stats_ptr.stats
	local box_w = w // 10
	local box_w2 = box_w // 2
	local half_icon = font_giant_h//2
	local objs_total_w = box_w*3 + 4*thickness
	local obj_x = center - objs_total_w//2
	local obj_y = top_total_h_and_y - y_gap - objectives_h
	local sprites = {1940, 175, 2563}
	local mod = (framer % 64) < 32
	local obj_tt

	for i = MAP_OBJECTIVE_TIMER, MAX_MAP_OBJECTIVES do
		local items_y = obj_y+2
		local spr = sprites[i]
		local obj_center_x = obj_x + box_w2
		local clr = 2567
		local border = "orange_shadow"
		local goal, reached = "-", "-"
		
		local obj_stat = stats[i]
		if obj_stat then
			goal = obj_stat.goal_str
			reached = obj_stat.max_reached_str
			clr = GUI_COLOR_K
			if mod and obj_stat.flash then
				border = "orange_red"
			end
		end
		
		draw_border(obj_x, obj_y, box_w, objectives_h, border, true, true, true, true, true, true, true, true, 	true)
		fill_with_hfx_sprite_clipped(obj_x, obj_y, box_w, objectives_h, clr, nil)
		
		if not obj_stat then
			LbDraw_SetFlagsOn(LB_DRAW_FLAG_INVERT_GLASS)
		end
		
		LbDraw_ScaledSprite(obj_center_x-half_icon, items_y, get_sprite(0, spr), font_giant_h, font_giant_h)
		
		PopSetFont(f1, 0)
		local goal_w = string_width(goal)
		items_y = items_y + font_giant_h
		LbDraw_Text(obj_center_x - goal_w//2, items_y, goal, 0)
		
		PopSetFont(f3, 0)
		local reached_w = string_width(reached)
		items_y = items_y + f1_h
		LbDraw_Text(obj_center_x - reached_w//2, items_y, reached, 0)
		
		if obj_stat then
			if not hover then
				if cursor_inside(obj_x, obj_y, box_w, objectives_h) then
					hover = true
					local str = obj_stat.tt
					if str ~= "" then
						obj_tt = { mouseX+24, mouseY, str }
					end
				end
			end
		end
		
		obj_x = obj_x + thickness + box_w
		LbDraw_SetFlagsOff(LB_DRAW_FLAG_INVERT_GLASS)
	end
	
	if obj_tt then
		PopSetFont(8, 0)
		local a,b,str = table.unpack(obj_tt)
		str_w = string_width(str)
		local strh = CharHeight2()
		-- DrawBox(a-1-1, b+1-1, str_w-2+3, strh-2+3, 0)
		-- DrawBox(a-1, b+1, str_w-2, strh-2, 1)
		DrawBox(a-2, b-2, str_w+4, strh+4, 0)
		DrawBox(a-1, b-1, str_w+2, strh+2, 1)
		LbDraw_Text(a, b, str, 0)
	end
	
	set_font(FONT_BIG)
	local str = "Mini-Game #" .. ghub.games_played .. " Results"
	LbDraw_Text(center - mf(string_width(str), 2), y_gap, str, 0)
	
	-- str = "Points Distribution"
	-- local str_w = string_width(str)
	-- local _x = center - mf(str_w, 2)
	
	-- draw_border(_x, y, str_w, CharHeight2(), "orange_shadow", true, false, true, true, true, true, true, true, 	true)
	-- --fill_with_hfx_sprite_clipped(_x, y, str_w, CharHeight2(), GUI_COLOR_I, nil)
	-- DrawBox(_x, y, str_w, CharHeight2(), 1)
	-- LbDraw_Text(_x, y, str, 0)
	-- y = y + CharHeight2()
	
	draw_border(x, y, total_w, total_h, "creme", true, true, true, true, true, true, true, true, 	true)
	fill_with_hfx_sprite_clipped(x, y, total_w, total_h, GUI_COLOR_K, nil)
	
	if im_master then
		if framer % 80 < 40 then
			PopSetFont(3, 0)
			local str = "press spacebar to continue"
			LbDraw_Text(center - mf(string_width(str), 2), h - CharHeight2() - 2, str, 0)
		end
	end
	
	set_font(FONT_SMALL)
	local score_type_tbl = points_systems[ggameplay.map_target_score_type]
	str = "This round used the '" .. score_type_tbl.name .. "' point system: " .. score_type_tbl.dist_str .. "."
	str_w = string_width(str)
	_y = y + total_h + thickness
	_x = center - mf(str_w, 2)
	draw_border(_x-2, _y, str_w+4, CharHeight2(), "orange_shadow", false, true, true, true, false, false, true, true, 	true)
	DrawBox(_x-2, _y, str_w+4, CharHeight2(), 1)
	LbDraw_Text(_x, _y, str, 0)
	_x = x + o_w
	local bottom = _y - thickness - h_o
	_y = bottom - f1_h - f2_h
	local shaman_w = mf(item_w, 2)
	local half_shaman_w = mf(shaman_w, 2)
	local center_item_w = mf(item_w, 2)
	local framerP = framer % 64 < 50

	for i = 1, max_items do
		local __y = _y
		local tbl = gmisc.post_mg_table[i] --stats_ptr.teams[i]

		if tbl then
			local names = false
			local position = tbl.position
			local _players = #tbl.players
			local spr = get_sprite(0, 1837 + i)
			local sh_w = shaman_w + (_players-1)*half_shaman_w
			
			for stack = 1, (9-position) do
				__y = __y - each_podium_stack
				LbDraw_ScaledSprite(_x, __y, spr, item_w, each_podium_stack)
			end
			
			__y = __y - each_podium_stack
			
			if not hover then
				local h_x = _x + center_item_w - mf(sh_w, 2)
				if cursor_inside(h_x, __y, sh_w, each_podium_stack) then
					hover = true
					names = true
				end
			end
			
			PopSetFont(4, 0)
			
			for k, player in ipairs(tbl.players) do
				local tribe = player.tribe
				LbDraw_ScaledSprite(_x + center_item_w - mf(sh_w, 2) + (k-1)*half_shaman_w, __y, get_sprite(0, 1809+tribe), shaman_w, each_podium_stack)
				
				if names then
					local name = gplayers[tribe + 1].full_name
					LbDraw_Text(_x + center_item_w - mf(sh_w, 2) + (k-1)*half_shaman_w + half_shaman_w - mf(string_width(name), 2), __y - CharHeight2()*k, name, 0)
				end
			end
			
			PopSetFont(9, 0)
			local str = tostring(tbl.score)
			LbDraw_Text(_x + center_item_w - mf(string_width(str), 2), y + 4, str, 0)
			
			__y = __y - each_podium_stack
			PopSetFont(f1, 0)
			str = position .. (position == 1 and "st" or position == 2 and "nd" or position == 3 and "rd" or "th")
			
			LbDraw_Text(_x + center_item_w - mf(string_width(str), 2), bottom - f1_h - f2_h, str, 0)
			
			if framerP then
				local awarded = tbl.awarded
				str = tostring(awarded)
				local f = 4 + 7*btn(awarded < 0)
				if awarded > 0 then f = 10 str = "+" .. str end
				PopSetFont(f, 0)
				
				LbDraw_Text(_x + center_item_w - mf(string_width(str), 2), bottom - f2_h, str, 0)
			end
		else
			local pfloor = mf(each_podium_stack, 4)
			LbDraw_ScaledSprite(_x, __y - pfloor, get_sprite(0, 1837), item_w, pfloor)
		end
		
		_x = _x + (item_w + o_w)
	end
end

function leave_final_results_to_hub()
	if gmisc.before_results_timer <= 0 then
		game_draw_cache.map_objectives = nil
		game_draw_cache.scoreboard = nil
		ggameplay.highest_score = 0
		ggameplay.highest_score_decimals = 1
		ggameplay.shaman_speed = 58
		ggameplay.map_curr_duration = 0
		ggameplay.map_curr_round = 0
		ggameplay.map_curr_round_timer = -1
		ggameplay.max_powerups_at_once = -1
		ggameplay.participants_by_teams = {}
		ggameplay.participants = {}
		ggameplay.AI_participants = {}
		ggameplay.tribes_to_participants_ptrs = {}
		ggameplay.post_mg_table = {}

		set_state(STATE_HUB)
		set_map_texture(true)
	end
end

function try_to_end_game_prematurely(key)
	if not pmisc.l_shift_pressing then return end
	local end_mode = (key == LB_KEY_F11) and HARDEND_MG_MODE_AWARD_PTS or HARDEND_MG_MODE_NO_PTS
	
	if not multiplayer then
		end_game(end_mode)
	else
		send_packet_now("e", tostring(end_mode))
	end
end

function end_game(host_hard_ended_it)
	if not ggameplay.processing then return end --avoids double end_game call when more than 1 goal obj was reached and called end_game()
	--host_hard_ended_it is NIL or a MODE
	reset_mouse_btns()
	remove_all_future_actions()
	ggameplay.processing = false
	destroy_bloods()
	if scripts_ptr.OnEnd then
		scripts_ptr.OnEnd()
	end
	ghub.previous_map = ghub.selected_map
	ghub.games_played = ghub.games_played + 1
	set_state(STATE_FINAL_RESULTS)
	gmisc.before_results_timer = BEFORE_RESULTS_TIMER
	award_pts_to_participants(host_hard_ended_it)
	queue_custom_sound_event(nil, "476_bell3.wav", 127)
	if not host_hard_ended_it then add_msg_to_hub_log(41, HUB_LOG_SYSTEM_MSG_PTR, nil) end
	DESELECT_ALL_PEOPLE(playernum)
	
	local units = {}
	ProcessGlobalTypeList(T_PERSON, function(d)
		if d.Model == M_PERSON_MEDICINE_MAN then
			local _c3d = copy_c3d(d.Pos.D3)
			table.insert(units, {c3d = _c3d, owner = d.Owner, y = _c3d.Ypos})
		end
		delete_thing_type(d)
	return true end)
	
	for _, v in ipairs(units) do
		local t = createThing(T_PERSON, M_PERSON_MEDICINE_MAN, v.owner, v.c3d, false, false)
		t.Pos.D3.Ypos = v.y
		remove_all_persons_commands(t)
		set_unit_flag(t, TF_NO_MOVE_PROCESS , 1, true)
		if t.u.Pers then
			local s_ondmg = t.u.Pers.OnDamage
			if s_ondmg then
				s_ondmg:clear()
			end
		end
	end
	
	unready_everyone()
	reset_constants()
	remove_all_spells_and_mana()
	unlock_spell_order()
	remove_all_buildings()
	reset_unwalkable_zones()
end

function uid_to_table_ptr(uid)
	if uid <= 8 then
		return gplayers[uid]
	end
	
	return teams_ptrs[uid - 7]
end

function deal_zero_damage(victim, damager)
	if nilS(victim) then
		damage_person(getShaman(victim), damager, 0, 1)
	end
end

function process_death_shamans(tribe, player_tbl)
	local alive = nilS(tribe)
	
	if alive then
		local shaman = getShaman(tribe)
		local game_ldb = shaman.u.Pers.u.Owned.LastDamagedBy
		local custom_ldb = player_tbl.ldb
		local ldb_cdr = math.max(-1, player_tbl.ldb_cdr - 1)
		
		if game_ldb ~= SAFE_NEUTRAL then
			local ldb_cdr_value = ggameplay.mg_player_vars.game_ldb_cdr or GAME_LDB_CDR
			
			if custom_ldb ~= game_ldb then
				player_tbl.ldb = game_ldb
				player_tbl.ldb_cdr = ldb_cdr_value
			else
				if ldb_cdr == 0 then
					player_tbl.ldb = SAFE_NEUTRAL
				end
				player_tbl.ldb_cdr = ldb_cdr
			end
		else
			player_tbl.ldb_cdr = - 1
			player_tbl.ldb = SAFE_NEUTRAL
		end
	else
		if mg_player_vars and mg_player_vars[tribe].dont_process_death then return end
		
		if not player_tbl.dead_lock then
			if player_tbl.can_reinc then
				local curr_reinc_timer = math.max(0, player_tbl.reinc - 1)
				player_tbl.reinc = curr_reinc_timer
				
				if curr_reinc_timer == 0 then
					player_tbl.reinc = ggameplay.map_respawn_timer
					player_tbl.ldb = SAFE_NEUTRAL
					player_tbl.ldb_cdr = -1
					spawn_shaman_at_map_position(tribe, true, ghub.map_info_ptr.shield_respawn, false, ghub.map_info_ptr.custom_shield_spawn_timer)
					player_tbl.dead_lock = true
					local onshamanborn = map_list[ghub.selected_map].OnShamanReincarnate
					
					if onshamanborn then
						onshamanborn(tribe, player_tbl)
					end
				end
			end
		else
			player_tbl.dead_lock = false
			local ldb = player_tbl.ldb
			local were_allies = player_tbl.ally_or_enemy[ldb]
			local onshamandeath = scripts_ptr and scripts_ptr.OnShamanDeath
			player_tbl.reinc = ggameplay.map_respawn_timer

			if onshamandeath then
				onshamandeath(tribe, ldb, were_allies) --warning!!! were_allies can be nil, true or false!
			end
			
			if not were_allies and ldb ~= SAFE_NEUTRAL then
				local killer_table = get_participant_table(ldb)
				
				if killer_table and killer_table.ai then
					if chance(20) then
						create_emote(ldb, randomItemFromTable(_make_fun_emojis))
					end
				end
			end
		end
	end
	
	return alive
end

function set_participants_can_reinc(bool)
	for _, player in ipairs(_participants) do
		local tribe = player.tribe
		player.can_reinc = bool
	end
end

function increase_round()
	ggameplay.map_curr_round = ggameplay.map_curr_round + 1
	
	local obj = ghub.map_objectives[MAP_OBJECTIVE_ROUNDS]
	if obj.active then
		local curr_round = ggameplay.map_curr_round
		ggameplay.cached_curr_values_strings[MAP_OBJECTIVE_ROUNDS] = tostring(curr_round)
		
		if curr_round > obj.value then
			ggameplay.map_curr_round = obj.value
			end_game()
			return false
		end
	end
	return true
end

function give_points_to_team(points, player, order_scores_bool, dont_go_under_0)
	if not ggameplay.processing then return end
	
	if player >= 0 and player <= 7 then
		local tbl = get_participant_table(player)
		local old_pts = tbl.team_ptr.points
		local new_pts = old_pts + points
		if dont_go_under_0 then
			new_pts = math.max(0, new_pts)
		end
		tbl.team_ptr.points = new_pts
		
		local on_pts_func = scripts_ptr.OnPlayerPoints
		if on_pts_func then
			on_pts_func(player, points, old_pts, new_pts)
		end
		
		if order_scores_bool then
			order_scores()
		end
	end	
end

function order_scores()
	table.sort(_participants_by_teams, function(a,b) if a.points ~= b.points then return a.points > b.points else return a.uid < b.uid end end)
	
	local highest = 0
	
	for _, v in ipairs(_participants_by_teams) do
		highest = math.max(highest, math.abs(v.points))
	end
	
	ggameplay.highest_score_decimals = #tostring(highest)
	ggameplay.highest_score = _participants_by_teams[1].points
	
	local obj = ghub.map_objectives[MAP_OBJECTIVE_TARGET_SCORE]
	if obj.active then
		--local highest = _participants_by_teams[1].points
		ggameplay.cached_curr_values_strings[MAP_OBJECTIVE_TARGET_SCORE] = tostring(highest)
		
		if highest >= obj.value then
			if not mg_vars.ignore_high_score_objective then
				end_game()
			end
		end
	end
end

function get_participant_table(tribe)
	return _tribes_to_participants_ptrs[tribe]
end

function remove_objects_of_type_model_owned_by(type, model, owner)
	local tbl = ggameplay.mg_obj_list[type]
	local objs = #tbl
	
	for i = objs, 1, -1 do
		local obj = tbl[i]
		
		if obj.Model == model then
			if obj.Owner == owner then
				obj.u.Effect.Duration = 1
				table.remove(tbl, i)
			end
		end
	end
end

function remove_objects_of_type_model_not_owned_by(type, model, owner)
	local tbl = ggameplay.mg_obj_list[type]
	local objs = #tbl
	
	for i = objs, 1, -1 do
		local obj = tbl[i]
		
		if obj.Model == model then
			if obj.Owner ~= owner then
				obj.u.Effect.Duration = 1
				table.remove(tbl, i)
			end
		end
	end
end

function get_enemy_tribes(player)
	local team_ptr = get_participant_table(player)
	return team_ptr.enemy_list
end

function get_allied_tribes(player)
	local team_ptr = get_participant_table(player)
	return team_ptr.ally_list
end

function is_player_enemy(tribe, targ_player)
	local team_ptr = get_participant_table(tribe)
	return not team_ptr.ally_or_enemy[targ_player]
end


-- ======================================================================================================================================== --
-- ========================================================== AI ========================================================================== --
-- ======================================================================================================================================== --

AI_names = { "Blue","Dakini","Chumara","Matak","Cyan","Magenta","Black","Orange"}
AI_difficulties = { [0]="Low", "Medium", "High" }
ai_difficulty_longest_string = "Medium"

function player_is_ai(tribe)
	if tribe > 7 or tribe < 0 then return false end
	return get_participant_table(tribe).ai
end

function reset_mg_player_vars()
	for i = 0, 7 do
		mg_player_vars[i] = {}
	end
end

function aim_spell_at_target(caster_tribe, target, spell_range)
	--target isnt nil, caster isnt nil --> returns a c3d or nil
	
	local cmd = get_thing_curr_cmd_list_ptr(target)
	
	if cmd then
		local targ_coord = cmd.u.TargetCoord
		
		if targ_coord then
			local z = (targ_coord.Zpos - target.Pos.D2.Zpos)
			local x = (targ_coord.Xpos - target.Pos.D2.Xpos)
			local ax = math.abs(x)
			local az = math.abs(z)
			local angle = math.atan(z, x) * 180 / math.pi
			angle = math.ceil(angle)
			
			if angle < 0 then
				angle = angle + 360
			end
			
			local caster_target_dist = get_world_dist_xz(getShaman(caster_tribe).Pos.D2, target.Pos.D2)
			local rotation = (angle * math.pi) / 180
			local distOffset = 512 + spell_range*floor(caster_target_dist/256)
			local spawnC3d = Coord3D.new()
			spawnC3d.Xpos = math.ceil(target.Pos.D3.Xpos + (math.cos(rotation) * distOffset))
			spawnC3d.Zpos = math.ceil(target.Pos.D3.Zpos + (math.sin(rotation) * distOffset))
			spawnC3d.Ypos = target.Pos.D3.Ypos
			
			return spawnC3d
		end
	end
	
	return nil
end

function get_cell_in_direction_of_walking_thing(thing, radius, return_c2d_instead)
	-- thing is not nil

	local cmd = get_thing_curr_cmd_list_ptr(thing)
	if not cmd then return nil end
	
	local targ_coord = cmd.u.TargetCoord
	if not targ_coord then return nil end
	
	-- Compute direction of movement
	local dz = targ_coord.Zpos - thing.Pos.D2.Zpos
	local dx = targ_coord.Xpos - thing.Pos.D2.Xpos
	
	-- angle in radians
	local rotation = math.atan(dz, dx)
	
	-- how far to offset (radius cells)
	local dist = radius * 512
	
	local offsetX = math.cos(rotation) * dist
	local offsetZ = math.sin(rotation) * dist
	
	if not return_c2d_instead then
		-- return C3D
		local c = Coord3D.new()
		c.Xpos = math.floor(thing.Pos.D3.Xpos + offsetX)
		c.Zpos = math.floor(thing.Pos.D3.Zpos + offsetZ)
		c.Ypos = thing.Pos.D3.Ypos
		return c
	else
		-- return C2D
		local c = Coord2D.new()
        -- Note: Coord2D uses D2 coords; converting from D3 is fine for cell selection
		c.Xpos = math.floor(thing.Pos.D3.Xpos + offsetX)
		c.Zpos = math.floor(thing.Pos.D3.Zpos + offsetZ)
		return c
	end
end

function try_to_dodge_on_water(pn, rad, _chance)
	--dodger(pn) isnt nil
	
	if chance(_chance) then
		local tbl = {}
		local sh = getShaman(pn)
		
		SearchMapCells(SQUARE, 0, 1, rad, world_coord3d_to_map_idx(sh.Pos.D3), function(me)
			if (is_map_elem_all_sea(me) > 0) then
				local c2d = Coord2D.new()
				coord3D_to_coord2D(me2c3d(me), c2d)
				table.insert(tbl, c2d)
			end
		return true end)
		
		if #tbl > 0 then
			local location = randomItemFromTable(tbl)
			command_person_go_to_coord2d(sh, location)
		end
	end
end

function try_to_dodge_on_land(pn, rad, _chance)
	--dodger(pn) isnt nil
	
	if chance(_chance) then
		local tbl = {}
		local sh = getShaman(pn)
		
		SearchMapCells(SQUARE, 0, 1, rad, world_coord3d_to_map_idx(sh.Pos.D3), function(me)
			if (is_map_elem_land_or_coast(me) > 0) then
				local c2d = Coord2D.new()
				coord3D_to_coord2D(me2c3d(me), c2d)
				table.insert(tbl, c2d)
			end
		return true end)
		
		if #tbl > 0 then
			local location = randomItemFromTable(tbl)
			command_person_go_to_coord2d(sh, location)
			return true, location
		end
	end
	
	return false
end

function tribe_try_blast_trick(tribe, blast_ground_if_no_target, _chance)
	if _chance and (not chance(_chance)) then return false end
	if not nilS(tribe) then return false end
	if (not shaman_has_healthy_state(tribe)) or (shaman_casting(tribe)) then return false end
	
	local shaman = getShaman(tribe)
	local random_enemy = get_random_enemy_shaman_near_thing(shaman, 5)
	
	if random_enemy then
		local t = createThing(T_SPELL, M_SPELL_BLAST, tribe, random_enemy.Pos.D3, false, false)
				
		if shaman_has_healthy_state(random_enemy.Owner) then
			t.u.Spell.TargetThingIdx:set(random_enemy.ThingNum)
			return true
		end
	else
		if blast_ground_if_no_target then
			local t = createThing(T_SPELL, M_SPELL_BLAST, tribe, shaman.Pos.D3, false, false)
			return true
		end
	end
	
	return false
end

function try_to_spell_an_enemy_general(tribe, spell, radius, _chance, diff, s_click_chance)
	if chance(_chance) then
		local random_enemy = get_random_enemy_shaman_near_thing(getShaman(tribe), radius)

		if random_enemy then
			if shaman_has_healthy_state(tribe) and not shaman_casting(tribe) then
				local t = createThing(T_SPELL, spell, tribe, random_enemy.Pos.D3, false, false)
				
				if shaman_has_healthy_state(random_enemy.Owner) then
					if chance(s_click_chance) then
						t.u.Spell.TargetThingIdx:set(random_enemy.ThingNum)
					end
				end
				return true
			end
		end
	end
	
	return false
end

function try_to_blast_own_boat(tribe, shaman, _chance)
	if chance(_chance) then
		if shaman_has_healthy_state(tribe) and not shaman_casting(tribe) then
			local t = createThing(T_SPELL, M_SPELL_BLAST, tribe, shaman.Pos.D3, false, false)
			t.u.Spell.TargetThingIdx:set(shaman.ThingNum)
			
			return true
		end
	end
	
	return false
end

function AI_shaman_stuck_checker(radius_dist, unstuck_method)
	local radius = radius_dist or 512
	local method = unstuck_method or AI_UNSTUCK_METHOD_KILL
	
	for _, aiplayer in ipairs(_ai_participants) do
		local player = aiplayer.ptr
		local tribe = player.tribe
		local shaman = getShaman(tribe)
		
		if nilS(tribe) then
			local old_c3d = mg_player_vars[tribe].cached_safe_c3d
			local curr_c3d = copy_c3d(shaman.Pos.D3)
			
			if not old_c3d then
				mg_player_vars[tribe].cached_safe_c3d = curr_c3d
			else
				local dist = get_world_dist_xyz(old_c3d, curr_c3d)
				
				if dist <= radius_dist then
					if method == AI_UNSTUCK_METHOD_KILL then
						shaman.u.Pers.Life = 0
					elseif method == AI_UNSTUCK_METHOD_LAND_DODGE then
						try_to_dodge_on_land(tribe, 2, 100)
					elseif method == AI_UNSTUCK_METHOD_WATER_DODGE then
						try_to_dodge_on_water(tribe, 2, 100)
					elseif method == AI_UNSTUCK_METHOD_CLEAR_C2D then
						mg_player_vars[tribe].c2d = nil
					end
				end
				
				mg_player_vars[tribe].cached_safe_c3d = curr_c3d
			end
		end
	end
end

-- ======================================================================================================================================== --
-- ============================================================== POWER UPS ============================================================== --
-- ======================================================================================================================================== --

function set_map_powerups_pointer()
	ggameplay.powerups_pointer = powerups_table[ghub.selected_map]
end

function get_me_the_closest_powerup_max_rad(tribe, thing, radius)
	local thing_c2d = thing.Pos.D2
	local places = {}

	for _, v in ipairs(active_powerups) do
		local pup = v.THING
		local dist = 512*radius
		
		if get_world_dist_xz(thing_c2d, pup.Pos.D2) <= dist then
			table.insert(places, {c2d = copy_c2d(pup.Pos.D2), dist = dist})
		end
	end
	
	local amt_places = #places
	
	if amt_places > 0 then
		table.sort(places, function (a, b) return a.dist < b.dist end)
	end
	
	return places
end

function process_automatic_powerup_creation()
	if not ggameplay.powerups_pointer then return end
	if mg_vars and mg_vars.dont_auto_spawn_powerups then return end
	local powerups = #active_powerups

	if powerups < ggameplay.max_powerups_at_once then
		for _, v in ipairs(ggameplay.powerups_pointer) do
			local pup_timer = v.timer
			
			if mg_turn >= pup_timer.cdr then
				local limit = pup_timer.max_of_this_type
				local id = v.general.id
				
				if limit == -1 or (count_powerups_of_id(id) < limit) then
					local everyturns = pup_timer.spawn_every_seconds() * 12
					
					if everyTurns_mg(everyturns) then
						if chance(pup_timer.chance) then
							local turn_min = pup_timer.only_after_turn_x
							
							if turn_min == -1 or (mg_turn >= turn_min) then
								spawn_power_up(id)
								pup_timer.cdr = math.max(mg_turn + 1, mg_turn + everyturns)
							end
						end
					end
				end
			end
		end
	end
end

function refresh_powerups_cdrs()
	if not ggameplay.powerups_pointer then return end
	for _, v in ipairs(ggameplay.powerups_pointer) do
		v.timer.cdr = 0
	end
end

function process_power_ups(everySecond1)
	if not ggameplay.powerups_pointer then return end
	
	for i = #active_powerups, 1, -1 do
		local powerup = active_powerups[i]
		local thing = powerup.THING
		if not thing then table.remove(active_powerups, i) goto end_of_single_pup end
		
		local tbl = ggameplay.powerups_pointer[powerup.ID]
		local faded = false
		
		if tbl.movement.moving then
			local movement = tbl.movement
			local speed = movement.speed
			local currY = thing.Pos.D3.Ypos
			local min, max = powerup.minY, powerup.maxY

			if powerup.DIRECTION then
				if currY + speed >= max then powerup.DIRECTION = not powerup.DIRECTION end
				thing.Pos.D3.Ypos = math.min(currY + speed, max)
			else
				if currY - speed <= min then powerup.DIRECTION = not powerup.DIRECTION end
				thing.Pos.D3.Ypos = math.max(currY - speed, min)
			end
		end
			
		if powerup.DRAW_CHANGE then
			if everyTurns_mg(tbl.drawinfo.turnChanger) then
				local new_sprite = powerup.THING.DrawInfo.DrawNum + 1
				
				thing.DrawInfo.DrawNum = new_sprite
				
				if new_sprite > tbl.drawinfo.max then
					thing.DrawInfo.DrawNum = tbl.drawinfo.min
				end
			end
		end
		
		if powerup.FADE ~= false then
			if mg_turn >= powerup.FADE then
				table.remove(active_powerups, i)
				delete_thing_type(thing)
				faded = true
			end
		end
		
		if not faded then
			-- if everySecond1 then
				-- --ensure power up not underground sometimes
				-- local p_c2d = thing.Pos.D2
				-- thing.Pos.D3.Ypos = math.max(point_altitude(p_c2d.Xpos, p_c2d.Zpos) + 16, thing.Pos.D3.Ypos)
			-- end
			
			local catch_rad = tbl.general.catch_rad
			
			for _, player in ipairs(_participants) do
				local tribe = player.tribe
				
				if nilS(tribe) then
					if shaman_has_healthy_state(tribe) then
						local shaman = getShaman(tribe)
						
						if get_world_dist_xyz(thing.Pos.D3, shaman.Pos.D3) <= catch_rad then
							local caught_powerup = tbl.effect(tribe, powerup.ID) ~= false
							
							if caught_powerup then
								local sound = tbl.general.snd_catch
								
								if sound then
									--sadly has to be nil (global), thing-associated sounds with this func not working properly and no one fixes it :)
									--queue_custom_sound_event(shaman, sound, 4)
									queue_custom_sound_event(nil, sound, 127)
									--queue_custom_sound_event(getShaman(tribe), sound, 127)
								end
								
								table.remove(active_powerups, i)
								delete_thing_type(thing)
								goto only_one_catcher
							end
						end
					end
				end
			end
			
			::only_one_catcher::
		end
		
		::end_of_single_pup::
	end
end

function spawn_power_up(id, custom_powerup_tbl)
	local tbl = custom_powerup_tbl or ggameplay.powerups_pointer[id]
	local timer = tbl.timer
	local location = nil
	local tries = 0
	
	while (tries < 6) do
		local _c3d = tbl.location()
		if not _c3d then break end
		
		local c3d = copy_c3d(_c3d)
		
		location = is_powerup_location_free(c3d)
		
		if location ~= nil then break end
		tries = tries + 1
	end
	
	if location ~= nil then
		--water vs land check? maybe not needed
		local general, creation, drawinfo, movement = tbl.general, tbl.creation, tbl.drawinfo, tbl.movement
		local powerup = createThing(creation[1], creation[2], creation[3], location, false, false)
		local changing = drawinfo.min ~= drawinfo.max
		powerup.DrawInfo.DrawNum = drawinfo.min
		powerup.DrawInfo.Alpha = drawinfo.alpha
		powerup.Pos.D3.Ypos = powerup.Pos.D3.Ypos + movement.startingY
		local minY = powerup.Pos.D3.Ypos + movement.startingY
		local maxY = minY + movement.Ybounce_offset
		local fade = timer.fade_after_turns
		powerup.DrawInfo.Flags = EnableFlag(powerup.DrawInfo.Flags, DF_POINTABLE) -- this is just to know this thing is a powerup (no effect)
		
		if fade ~= false then
			fade = mg_turn + timer.fade_after_turns
		end
		
		if drawinfo.dfflag ~= nil then
			powerup.Flags = EnableFlag(powerup.Flags2, drawinfo.dfflag)
		end
		
		if drawinfo.shadow ~= nil then
			powerup.DrawInfo.Flags = EnableFlag(powerup.DrawInfo.Flags, DF_USE_ENGINE_SHADOW)
		end
		
		if general.snd_appear ~= nil then
			queue_sound_event(powerup, general.snd_appear, 1)
		end
		
		table.insert(active_powerups, { ID=id, THING=powerup, DRAW_CHANGE=changing, MOVE=movement.moving, DIRECTION=true, FADE=fade, minY=minY, maxY=maxY})
		
		local mg_on_powerup = scripts_ptr.OnPowerUp
		if mg_on_powerup then
			mg_on_powerup(id, location)
		end
	end
end

function is_powerup_location_free(c3d)
	--cant be above shamans, or above other powerups
	local pup_c2d = Coord2D.new()
    coord3D_to_coord2D(c3d, pup_c2d)
	
	for _, v in ipairs(active_powerups) do
		if get_world_dist_xz(pup_c2d, v.THING.Pos.D2) <= POWERUP_CATCH_RADIUS_DEFAULT then
			return nil
		end
	end
	
	for _, player in ipairs(_participants) do
		local tribe = player.tribe
		
		if nilS(tribe) then
			if get_world_dist_xz(pup_c2d, getShaman(tribe).Pos.D2) <= POWERUP_CATCH_RADIUS_DEFAULT then
				return nil
			end
		end
	end

	return c3d
end

function count_powerups_of_id(id)
	local count = 0
	
	for _, v in ipairs(active_powerups) do
		if v.ID == id then
			count = count + 1
		end
	end
	
	return count
end

function is_powerup_in_c3d(c3d)
	local is = false
	local idx = world_coord3d_to_map_idx(c3d)
	
	SearchMapCells(SQUARE, 0, 0, 0, idx, function(me)
		me.MapWhoList:processList(function(t)
			if t.Type == T_GENERAL then
				if t.Model == M_GENERAL_MAPWHO_THING then
					--if t.DrawInfo.DrawNum >= 1852 then --cant use this. we use powerups with low drawnums
					if HasFlag(t.DrawInfo.Flags, DF_POINTABLE) then
						is = true
					end
				end
			end
			return true
		end)
	return true end)
	
	return is
end

function is_powerup_in_c2d(c2d)
	local c3d = c2d_to_c3d(c2d)
	return is_powerup_in_c3d(c3d)
end

function clear_power_ups()
	for _, v in ipairs(active_powerups) do
		if v.THING then
			delete_thing_type(v.THING)
		end
	end
end

-- ======================================================================================================================================== --
-- ========================================================== SPELLS & CONSTANTS ========================================================== --
-- ======================================================================================================================================== --

-- CONSTANTS

local constants_cached_defaults = {}

function cache_default_constants()
	for spell = M_SPELL_BLAST, NUM_SPELL_TYPES do
		local ptr = constants_cached_defaults[spell] or {}
		local spell_sti = sti[spell]
		
		ptr.cost = 				spell_sti.Cost
		ptr.shots = 			spell_sti.OneOffMaximum
		ptr.range = 			spell_sti.WorldCoordRange
		ptr.discovery_idx = 	spell_sti.DiscoveryDrawIdx
		ptr.cursor = 			spell_sti.CursorSpriteNum
		ptr.available = 		spell_sti.AvailableSpriteIdx
		ptr.not_available = 	spell_sti.NotAvailableSpriteIdx
		ptr.clicked = 			spell_sti.ClickedSpriteIdx
		ptr.spell_effects = 	spell_sti.EffectModels[0]
		
		constants_cached_defaults[spell] = ptr
	end
end

function reset_constants()
	
	for spell = M_SPELL_BLAST, NUM_SPELL_TYPES do
		local default_spell = constants_cached_defaults[spell]
		local spell_sti = sti[spell]
	
		spell_sti.Cost = 					default_spell.cost
		spell_sti.OneOffMaximum = 			default_spell.shots
		spell_sti.WorldCoordRange = 		default_spell.range
		spell_sti.DiscoveryDrawIdx = 		default_spell.discovery_idx
		spell_sti.CursorSpriteNum = 		default_spell.cursor
		spell_sti.AvailableSpriteIdx = 		default_spell.available
		spell_sti.NotAvailableSpriteIdx = 	default_spell.not_available
		spell_sti.ClickedSpriteIdx = 		default_spell.clicked
		spell_sti.EffectModels[0] = 		default_spell.spell_effects
	end

	for tribe = 0, 7 do
		local limits_spell = getPlayer(tribe).LimitsSpell

		for spell = M_SPELL_BLAST, NUM_SPELL_TYPES do
			local default_spell = constants_cached_defaults[spell]
			
			limits_spell.Cost[spell] = default_spell.cost
			limits_spell.MaxCharges[spell] = default_spell.shots
			limits_spell.WorldCoordRange[spell] = default_spell.range
			
			disable_spell(tribe, spell, true, true)
		end
	end

	--stuff used in mini games. update as new mini games are added
	pti[M_PERSON_ANGEL].DefaultLife = 10000
	pti[M_PERSON_MEDICINE_MAN].DefaultLife = 2000
	pti[M_PERSON_MEDICINE_MAN].LifeIncrease = 32
	pti[M_PERSON_MEDICINE_MAN].ManaValue = 30
	pti[M_PERSON_MEDICINE_MAN].AttackPower = 60
	tmi[TMI_PERSON_MEDICINE_MAN].BaseSpeed = 58
	_constants.ShamenDeadManaPer256Gained = 64
end

--this seems safer than turning spell model to MODEL_NONE since i think that causes issues (faster shots, fast shamans moving (including inside boats) etc
function disable_spells_effects_for_this_level(spell_tbl)
	for _, spell in ipairs(spell_tbl) do
		sti[spell].EffectModels[0] = 0
	end
end

function remove_all_spells_and_mana()
	for tribe = 0, 7 do
		for spell = M_SPELL_BLAST, NUM_SPELL_TYPES do
			disable_spell(tribe, spell, true, true)
		end
	end
end

function remove_all_buildings()
	for pn = 0, 7 do
		for bldg = 1, M_BUILDING_AIRSHIP_HUT_2 do
			disable_bldg(pn, bldg)
		end
	end
end

function lock_spell_order()
	_gnsi.Flags4 = _gnsi.Flags4 | GNS4_LOCK_SPELLS_ORDER
end

function unlock_spell_order()
	_gnsi.Flags4 = _gnsi.Flags4 & (~GNS4_LOCK_SPELLS_ORDER)
end

function remove_all_spells_mana()
	for pn = 0, 7 do
		local _player = getPlayer(pn)
		
		for spell = M_SPELL_BLAST, NUM_SPELL_TYPES do
			_player.SpellsMana[spell] = 0
		end
	end
end

function start_charging_spell(pn, model)
	_gsi.ThisLevelInfo.PlayerThings[pn].SpellsNotCharging = DisableFlag(_gsi.ThisLevelInfo.PlayerThings[pn].SpellsNotCharging, (1 << (model - 1)))
end

function stop_charging_spell(pn, model)
	_gsi.ThisLevelInfo.PlayerThings[pn].SpellsNotCharging = EnableFlag(_gsi.ThisLevelInfo.PlayerThings[pn].SpellsNotCharging, (1 << (model - 1)))
end

function set_spell_charges(pn, model, num)
	_gsi.ThisLevelInfo.PlayerThings[pn].SpellsAvailableOnce[model] = _gsi.ThisLevelInfo.PlayerThings[pn].SpellsAvailableOnce[model] & 240
	_gsi.ThisLevelInfo.PlayerThings[pn].SpellsAvailableOnce[model] = _gsi.ThisLevelInfo.PlayerThings[pn].SpellsAvailableOnce[model] | num
end

function enable_spell(pn, model)
	_gsi.ThisLevelInfo.PlayerThings[pn].SpellsAvailable = EnableFlag(_gsi.ThisLevelInfo.PlayerThings[pn].SpellsAvailable, (1 << model))
end

function disable_spell(pn, model, do_remove_shots, do_remove_mana)
	_gsi.ThisLevelInfo.PlayerThings[pn].SpellsAvailable = DisableFlag(_gsi.ThisLevelInfo.PlayerThings[pn].SpellsAvailable, (1 << model))
	local getplayer = getPlayer(pn)
	
	if (do_remove_shots == true) then
		set_spell_charges(pn, model, 0)
		if do_remove_mana then
			getplayer.SpellsMana[model] = 0
		end
	end
end

function enable_bldg(pn, model)
	_gsi.ThisLevelInfo.PlayerThings[pn].BuildingsAvailable = EnableFlag(_gsi.ThisLevelInfo.PlayerThings[pn].BuildingsAvailable, (1 << model))
end

function disable_bldg(pn, model)
	_gsi.ThisLevelInfo.PlayerThings[pn].BuildingsAvailable = DisableFlag(_gsi.ThisLevelInfo.PlayerThings[pn].BuildingsAvailable, (1 << model))
	_gsi.ThisLevelInfo.PlayerThings[pn].BuildingsAvailableOnce = DisableFlag(_gsi.ThisLevelInfo.PlayerThings[pn].BuildingsAvailableOnce, (1 << model))
	_gsi.ThisLevelInfo.PlayerThings[pn].BuildingsAvailableLevel = DisableFlag(_gsi.ThisLevelInfo.PlayerThings[pn].BuildingsAvailableLevel, (1 << model))
end

function count_player_spell_shots(pn, spell)
	return GET_NUM_ONE_OFF_SPELLS(pn, spell)
end

function give_shots_to_player(pn, spell, shots)
	for i = 1, shots do
		GIVE_ONE_SHOT(spell, pn)
	end
end

-- ======================================================================================================================================== --
-- ======================================================== HOVERING & BUTTONS ============================================================ --
-- ======================================================================================================================================== --

_inputKeys = { 
    [LB_KEY_A] = 'A',
    [LB_KEY_B] = 'B',
    [LB_KEY_C] = 'C',
    [LB_KEY_D] = 'D',
    [LB_KEY_E] = 'E',
    [LB_KEY_F] = 'F',
    [LB_KEY_G] = 'G',
    [LB_KEY_H] = 'H',
    [LB_KEY_I] = 'I',
    [LB_KEY_J] = 'J',
    [LB_KEY_K] = 'K', --trouble team chat
    [LB_KEY_L] = 'L',
    [LB_KEY_M] = 'M', --trouble chat
    [LB_KEY_N] = 'N',
    [LB_KEY_O] = 'O',
    [LB_KEY_P] = 'P', --trouble pause
    [LB_KEY_Q] = 'Q',
    [LB_KEY_R] = 'R',
    [LB_KEY_S] = 'S', --trouble stats
    [LB_KEY_T] = 'T',
    [LB_KEY_U] = 'U',
    [LB_KEY_V] = 'V',
    [LB_KEY_W] = 'W',
    [LB_KEY_X] = 'X',
    [LB_KEY_Y] = 'Y',
    [LB_KEY_Z] = 'Z',
    [LB_KEY_1] = '1',
    [LB_KEY_2] = '2',
    [LB_KEY_3] = '3',
    [LB_KEY_4] = '4',
    [LB_KEY_5] = '5',
    [LB_KEY_6] = '6',
    [LB_KEY_7] = '7',
    [LB_KEY_8] = '8',
    [LB_KEY_9] = '9',
    [LB_KEY_0] = '0',
	[LB_KEY_DOT] = '.',
	[LB_KEY_COMMA] = ',',
	[LB_KEY_HASH] = '#',
	[LB_KEY_SLASH ] = '/',
	[LB_KEY_MINUS ] = '-',
	[LB_KEY_EQUAL ] = '=',
	[LB_KEY_NUM_PLUS ] = '+',
	[LB_KEY_COLON ] = ':',
	[LB_KEY_SPACE] = ' ',
	[LB_KEY_BACKSPACE] = 'backspace',
	--[LB_KEY_ESC] = 'escape',
	--[LB_KEY_RETURN] = 'enter'
}

function press_hub_button(left)
	if not hovering then
		reset_map_searching()
		return
	end
	
	if ggameplay.countdown >= 0 and (hovering ~= SECTION_START_GAME) then
		return
	end
	
	local btn_action = button_list[hovering]
	if not btn_action then return end
	if hovering ~= SECTION_CARD and dragging_card() then return end
	if hovering ~= SECTION_SEARCH_MAP and hovering ~= SECTION_SEARCH_MAP_OPTION then reset_map_searching() end
	
	btn_action(hovering_id, left, playernum)
	reset_mouse_btns()
	reset_zone_hovering()
end

function reset_mouse_btns()
	hovering, hovering_id = nil, nil
end

function reset_zone_hovering()
	hovering_zone = nil
end

function try_to_hover(x, y, w, h, button, button_id)
	local hover = cursor_inside(x, y, w, h)
	
	if hover then
		hovering = button
		hovering_id = button_id or 1
	end
	
	return hover
end

function try_to_hover_zone(x, y, w, h, zone_id)
	local hover = cursor_inside(x, y, w, h)
	
	if hover then
		hovering_zone = zone_id
	end
	
	return hover
end

function set_left_mouse_pressing(bool)
	pmisc.l_mouse_pressing = bool
end

function set_right_mouse_pressing(bool)
	pmisc.r_mouse_pressing = bool
end

function set_left_shift_pressing(bool)
	pmisc.l_shift_pressing = bool
end

function set_left_ctrl_pressing(bool)
	pmisc.l_ctrl_pressing = bool
end

function try_to_drag_card()
	local dragging_card = dragging_card()
	if dragging_card then return end
	if ggameplay.countdown >= 0 then return end
	
	if hovering == SECTION_CARD then
		if hovering_id and ((playernum == hovering_id) or im_host) then
			if not is_location_locked(hovering_zone) or im_host then
				pmisc.dragging_card = hovering_id
				reset_map_searching()
			else
				shake_lock_section(hovering_zone, LOCK_SHAKE_TURNS_DUR)
			end
		end
	end
end

function reset_dragging_card()
	pmisc.dragging_card = nil
end

function refresh_map_hover_img()
	local start = ghub.map_info_ptr.image
	local map = ghub.selected_map
	local no_increase = (map == MAP_QUIZ_EASY or map == MAP_QUIZ_HARD)
	pmisc.map_hover_img = { curr = start, min = start, max = no_increase and start or start + HUB_MAP_GIF_FRAMES - 1 }
end

function reset_ingame_cursors()
	hovering_ingame = nil
	hovering_ingame_id = nil
end

function click_ingame_button(clicker, left)
	if not hovering_ingame then return end
	if not hovering_ingame_id then return end

	if hovering_ingame == HOVERING_PERSONAL_SETTINGS_BTN then
		if left then
			local was_enabled = personal_settings[hovering_ingame_id].var()
			personal_settings[hovering_ingame_id].action()
			queue_custom_sound_event(nil, was_enabled and "y_c_option2.wav" or "y_c_option1.wav", 127)
		end
	else
		-- mini game buttons (do some sort of hash table with pointers)
		local mg_onmouseup = scripts_ptr.OnMouseUp
		if mg_onmouseup then
			if not multiplayer then
				mg_onmouseup(left)
			else
				local data = encode_boolean(left)..tostring(hovering_ingame_id)
				send_packet_now("b", data)
			end
		end
	end
end

function ingame_on_key(key, is_down)
	if not scripts_ptr then return end
	if mg_turn < 1 then return end

	local mg_onkey = scripts_ptr.OnKey
	if mg_onkey then
		--lets only use onKeyUp!!!
		if not is_down then
			if not multiplayer then
				mg_onkey(key, is_down, playernum)
			else
				local data = encode_boolean(is_down)..encode_base62(key)
				send_packet_now("c", data)
			end
		end
	end
end

-- ======================================================================================================================================== --
-- ========================================================== DRAWING ===================================================================== --
-- ======================================================================================================================================== --

-- hub

function draw_mg_hub()
	set_font(FONT_SMALL)
	local o = 2
	local oo = o * 2
	local thickness = 4
	local h1, h2, h3 = font_heights[FONT_SMALL], font_heights[FONT_MEDIUM], font_heights[FONT_BIG]
	local lock_size = h2
	local team_score_size = custom_string_widths.score_size + 4
	local centered_team_score_y = lock_size // 2 - h1 // 2
	local custom_rules_font = 8
	local custom_rules_font_h = 15
	local map_desc_font = pmisc.rules_font
	
	if not hub_cache then
		hub_cache = {}
		local icons_box = curr_font_h*3
		local ready_or_ai_icon = floor(icons_box / 1.3)
		local eye_box = icons_box // 2
		local score_w = oo + custom_string_widths.score_w
		local full_card_w = 6*oo + eye_box + icons_box + ready_or_ai_icon + score_w
		local full_card_w_simpler = full_card_w - 2*o - eye_box - score_w
		local big_o = thickness * 4 --clamp(floor(full_card_w_simpler / 8), 1, 16)
		local small_o = big_o // 2
		local full_content_w_without_nicks = full_card_w * 2 + o*3 + o*8 + full_card_w_simpler * 4 + big_o*6 --+ thickness*12 
		local spare_w = math.max(0, W - thickness*3 - full_content_w_without_nicks)
		local nicks_max_room = custom_string_widths.m * NICKNAME_MAX_SIZE
		local nicks_w = clamp(floor(spare_w / 6), 0, nicks_max_room)
		full_card_w, full_card_w_simpler = full_card_w + nicks_w, full_card_w_simpler + nicks_w
		local totalW = full_content_w_without_nicks + nicks_w * 6
		local big_panel_w = o*3 + full_card_w*2
		local panel_w = oo + full_card_w_simpler
		local sound_btn_size = totalW // 42
		local o_h = W > 800 and h3*2 or h3 + oo 			--H // 32
		local o_h2 = o_h // 2
		local map_picking_card_h = h3 + h2*2 + h1*2 + oo*4
		local content_x = W2 - floor(totalW / 2) --+ thickness
		local centered_w = content_x + totalW // 2
		local players_rectangle_h = icons_box + oo
		local panel_h = o*5 + players_rectangle_h*4
		local custom_rules_h = h1 + MAX_MAP_CUSTOM_RULES * custom_rules_font_h + (o * (MAX_MAP_CUSTOM_RULES+2))
		local bottom_mid_icons_size = H // 64
		local totalH = thickness*4 + o_h*2 + panel_h + map_picking_card_h + custom_rules_h + oo*2
		local content_y = H2 - totalH // 2
		local setting_max_title, setting_max_value = W > 800 and custom_string_widths.setting_max_title or custom_string_widths.setting_max_title_low_res, custom_string_widths.setting_max_value
		local each_setting_w = o*3 + setting_max_title + setting_max_value + h1*2 + 1
		local game_settings_w = small_o*2 + each_setting_w
		local rules_w = totalW - big_o*5 - big_panel_w - panel_w - game_settings_w
		local game_setting_h = (map_picking_card_h - oo*2 - 2) // 3
		local search_w = 8 + custom_string_widths.medium_m * SEARCHING_MAX_CHARS + 2
		local t1 = content_y + o_h + thickness 					--top rectangles y
		local t2 = t1 + panel_h + thickness 					--locks y
		local t3 = t2 + o_h + thickness 						--middle rectangles y
		local t4 = t3 + map_picking_card_h + thickness + oo		--search and custom rules y
		local t5 = t4 + custom_rules_h + oo + thickness 		--bottom icons below main zone

		hub_cache.sound_btn_size = sound_btn_size
		hub_cache.players_rectangle_h = players_rectangle_h
		hub_cache.map_picking_card_h = map_picking_card_h
		hub_cache.panel_h = panel_h
		hub_cache.custom_rules_h = custom_rules_h
		hub_cache.totalH = totalH
		hub_cache.content_y = content_y
		hub_cache.t3 = t3
		hub_cache.t4 = t4
		hub_cache.t5 = t5
		local zones_x = {} --starting X position for major 6 different zones (5 top and 1 diff one on middle (game conditions))
		local zones_w = {} --width for 4 middle zones
		reset_map_rules_offset()
		
		-- team names
		
		set_font(FONT_BIG)
		local team_names = {}
		local team_names_tbl = ghub.gui_teams
		local name_y = t1 - thickness - h3
		local name_x = content_x + big_o + big_panel_w // 2
		for i = 1, 5 do
			local name = team_names_tbl[i].default_name
			local name_w = string_width(name)
			local half_name_w = name_w // 2
			team_names[i] = { str = team_names_tbl[i].default_name, x = name_x - half_name_w, y = name_y }
			if i == 1 then
				name_x = name_x + big_panel_w // 2 + big_o + panel_w // 2
			else
				name_x = name_x + panel_w + big_o
			end
		end
		hub_cache.team_names = team_names
		
		-- borders & textures
		
		local borders = {}
		local curr_x = content_x + big_o
		zones_x[1] = curr_x
		local curr_y = t1
		table.insert(borders, { x = content_x, y = content_y, w = totalW, h = totalH, name = "creme", half = true, texture = GUI_COLOR_K, flag = false })
		table.insert(borders, { x = curr_x, y = curr_y, w = big_panel_w, h = panel_h, name = "white", half = true, texture = GUI_COLOR_F, flag = LB_DRAW_FLAG_GLASS, id = HOVER_ZONE_SOLO_PLAYERS })
		curr_x = curr_x + big_panel_w + big_o
		for i = 1, 4 do
			zones_x[i + 1] = curr_x
			table.insert(borders, { x = curr_x, y = curr_y, w = panel_w, h = panel_h, name = "white", half = true, texture = GUI_COLOR_F, flag = LB_DRAW_FLAG_GLASS, id = HOVER_ZONE_TEAM_1 + i - 1 })
			curr_x = curr_x + panel_w + big_o
		end
		curr_x = content_x + big_o
		curr_y = t3
		local middle_panels_widths = {big_panel_w, panel_w, rules_w, game_settings_w}
		local middle_panels_x = {}
		for i = 1, 4 do
			local texture, flag = nil, nil
			if i == 1 then
				texture, flag = GUI_COLOR_I, LB_DRAW_FLAG_GLASS
			elseif i == 3 then
				texture, flag = GUI_COLOR_K, LB_DRAW_FLAG_INVERT_GLASS
			elseif i == 4 then
				zones_x[6] = curr_x
			end
			local _w = middle_panels_widths[i]
			zones_w[i] = _w
			table.insert(borders, { x = curr_x, y = curr_y, w = _w, h = map_picking_card_h, name = "creme", half = true, texture = texture, flag = flag })
			middle_panels_x[i] = curr_x
			curr_x = curr_x + _w + big_o
		end
		hub_cache.borders = borders
		hub_cache.zones_x = zones_x
		hub_cache.zones_w = zones_w

		local centered_card_y = players_rectangle_h // 2
		curr_x = oo
		curr_y = centered_card_y - eye_box // 2
		local eye_x, eye_y, eye_size = curr_x, curr_y, eye_box
		curr_x = curr_x + eye_size + oo
		curr_y = centered_card_y - h1 // 2
		local nick_x, nick_y, nick_max_w = curr_x, curr_y, nicks_w
		curr_x = curr_x + nicks_w + oo
		curr_y = o
		local icon_x, icon_y, icon_size = curr_x, curr_y, icons_box
		local score_h = h1 + 4
		curr_x = icon_x + icons_box + oo
		curr_y = centered_card_y - score_h // 2
		local score_x, score_y, score_w = curr_x, curr_y, score_w
		curr_x = score_x + score_w + oo
		curr_y = centered_card_y - ready_or_ai_icon // 2
		local rdy_x, rdy_y, rdy_size = curr_x, curr_y, ready_or_ai_icon
		
		hub_cache.card_full = { w = full_card_w, h = players_rectangle_h, eye_x = eye_x, eye_y = eye_y, eye_size = eye_size,
								nick_x = nick_x, nick_y = nick_y, nick_max_w = nick_max_w,
								icon_x = icon_x, icon_y = icon_y, icon_size = icon_size,
								score_x = score_x, score_y = score_y, score_w = score_w, score_h = score_h,
								rdy_x = rdy_x, rdy_y = rdy_y, rdy_size = rdy_size
							  }
						
		icon_x = oo*3 + nick_max_w
		score_x = icon_x + oo + icon_size
		rdy_x = score_x + score_w + oo
						
		hub_cache.card_short = { w = full_card_w_simpler, h = players_rectangle_h,
								nick_x = oo, nick_y = nick_y, nick_max_w = nick_max_w,
								icon_x = icon_x, icon_y = icon_y, icon_size = icon_size,
								score_x = score_x, score_y = score_y, score_w = score_w, score_h = score_h,
								rdy_x = rdy_x, rdy_y = rdy_y, rdy_size = rdy_size
							  }
		
		-- 7 maps
		
		curr_x = content_x + big_o
		curr_y = t3
		local maps_centered_x = curr_x + big_panel_w // 2
		local maps_texts_y = {}
		local maps_y = curr_y - h1 - oo
		local maps_sizes = {h1,h1,h2,h3,h2,h1,h1}
		
		for i = 1, 7 do
			maps_texts_y[i] = {x = maps_centered_x, y = maps_y}
			local map_font_h = maps_sizes[i]
			maps_y = maps_y + map_font_h + oo
		end
		hub_cache.maps_texts_y = maps_texts_y
		
		-- locks and team scores
		
		local locks = {}
		local team_scores = {}
		curr_x = content_x + big_o + big_panel_w - lock_size
		curr_y = t2
		
		table.insert(locks, { x = curr_x, y = curr_y })
		for i = 1, 4 do
			curr_x = curr_x + panel_w + big_o
			table.insert(locks, { x = curr_x, y = curr_y })
			table.insert(team_scores, { x = curr_x - 4 - team_score_size, y = curr_y })
		end
		hub_cache.locks = locks
		hub_cache.team_scores = team_scores
		
		-- map description buttons
		
		local desc_buttons = {}
		local desc_btns_y = t3 + map_picking_card_h + thickness
		local desc_btn_size = map_picking_card_h // 6
		local desc_x = zones_x[3]
		local desc_w = zones_w[3]
		local desc_btns_tbl = { {x = desc_x, spr = 171, hover_spr = 1750}, {x = desc_x + desc_w - desc_btn_size, spr = 422, hover_spr = 421}, {x = desc_x + desc_w - desc_btn_size*2 - 6, spr = 425, hover_spr = 424} }
		for k, btn in ipairs(desc_btns_tbl) do
			desc_buttons[k] = { x = btn.x, y = desc_btns_y + 4, size = desc_btn_size, spr = btn.spr, hover_spr = btn.hover_spr }
		end
		hub_cache.desc_buttons = desc_buttons
		
		-- map goals zone
		
		local map_goals = {}
		curr_x = zones_x[6] + small_o
		local goals_txt = {"Max Duration", "Target Score", "Max Rounds"}
		local goals_txt_low_res = {"Duration", "Score", "Rounds"}
		local goals_ptr = W > 800 and goals_txt or goals_txt_low_res
		for i = 1, 3 do
			local y = t3 + oo + (i-1) * (game_setting_h + 1)
			local centered_y = y + game_setting_h // 2 - h1 // 2
			local _x = curr_x + o
			local txt = goals_ptr[i]
			local txt_w = string_width(txt)
			local txt_x = _x
			_x = _x + setting_max_title
			local goal_value_x = _x
			_x = _x + setting_max_value + o
			map_goals[i] = { x = curr_x, y = y, w = each_setting_w, h = game_setting_h, txt = txt, txt_x = txt_x, goal_value_x = goal_value_x, goal_value_w = setting_max_value, centered_y = centered_y, btn_x = _x }
		end
		hub_cache.map_goals = map_goals
		
		-- map search
		
		local search_x = zones_x[1]
		local search_h = h2
		local search_zones = { {x = search_x, y = t4, w = h2, h = h2}, {x = search_x + 6 + h2, y = t4, w = search_w, h = h2, centered_y = search_h // 2 - h1 // 2} }
		hub_cache.search_zones = search_zones
		
		-- map custom rules
		
		local custom_rules = {}
		curr_x, curr_y = zones_x[6], t4
		local full_rules_w, full_rules_h = zones_w[4], custom_rules_h
		custom_rules.bg = {x=curr_x, y = curr_y, w = full_rules_w, h = full_rules_h}
		set_font(FONT_SMALL)
		local str = "Advanced Options"
		local str_w = string_width(str)
		custom_rules.title = {x = curr_x + full_rules_w // 2 - str_w // 2, y = curr_y + o, txt = "Advanced Options" }
		curr_y = curr_y + o*2 + h1
		--set_font(FONT_MEDIUM)
		PopSetFont(custom_rules_font, 0)
		local box = custom_rules_font_h
		local box2 = box // 2
		local questionmark = floor(box / 1.2)
		local questionmark2 = questionmark // 2
		local gaps = o*5
		local text_w = full_rules_w - box - questionmark - gaps
		custom_rules.rule_name_w = text_w
		custom_rules.content = {}
		for i = 1, MAX_MAP_CUSTOM_RULES do
			local qx = curr_x + o
			local btnX = qx + questionmark + o
			local txtX = btnX + o + box
			local qy = curr_y + box2 - questionmark2
			custom_rules.content[i] = { q_x = qx, q_y = qy, q_s = questionmark, btn_x = btnX, btn_y = curr_y + 0, btn_s = box, txt_x = txtX }
			curr_y = curr_y + custom_rules_font_h + o
		end
		hub_cache.custom_rules = custom_rules
		
		--bottom middle btns
		
		local bottom_btns = {}
		local bottom_btns_size = totalW // 30
		local bottom_btns_gap = 4
		hub_cache.bottom_btns_size = bottom_btns_size
		--local bottom_btns_tbl = { {spr = 1827, req = function() return btn(ghub.games_played > 0) end}, {spr = function() return get_point_system_sprite() end, req = function() return true end}, {spr = 1948 + ggameplay.AI_difficulty, req = function() return true end}, }
		local bottom_btns_tbl = { {spr = 1827, id = BOT_MIDDLE_BUTTON_PAST_MG_SCORES, req = function() return ghub.games_played > 0 end}, {spr = function() return hub_cache.point_system_cache.spr end, id = BOT_MIDDLE_BUTTON_POINT_SYSTEM, req = function() return true end}, {spr = function() return 2084 - 41*btn(gmisc.emojis_enabled) end, id = BOT_MIDDLE_BUTTON_DISABLE_EMOJIS, req = function() return --[[an_ai_exists()]] multiplayer and (#ghub.humans_tbl > 1)  end}, }
		for k, bottom_btn in ipairs(bottom_btns_tbl) do
			bottom_btns[k] = bottom_btn
		end
		hub_cache.bottom_btns = bottom_btns
		
		-- bottom volume icons
		
		local sound_buttons = {}
		local sound_amt = 1 + MAX_SOUNDS_BUTTONS
		local sound_gap = math.max(2, sound_btn_size // 8)
		local sound_total_w = sound_amt * (sound_btn_size + sound_gap)
		curr_x = content_x + totalW - sound_total_w
		hub_cache.sound_buttons_cdr = { x = curr_x, y = t5, w = sound_total_w }
		for i = 1, sound_amt do
			sound_buttons[i] = { x = curr_x, y = t5 }
			curr_x = curr_x + (sound_btn_size + sound_gap)
		end
		hub_cache.sound_buttons = sound_buttons
		
		return
	end
	
	-- hug log
	
	local hub_ret = false
	local hard_glass = false
	local fading_timer = ghub.log_fading_timer
	
	if mouseX < XpercentOf(10, W) then
		if mouseY > XpercentOf(90, H) then
			hub_ret = true
			ghub.log_fading_timer = MAX_LOG_FADING_TIMER
			hard_glass = true
		end
	end
	
	if fading_timer > 0 then
		local tbl_size = #ghub.hub_log

		if tbl_size > 0 then
			--PopSetFont(get_font(FONT_SMALL --[[+ btn(w > 800)]]), 0)
			set_font(FONT_SMALL)
			local font_h = curr_font_h
			local o = 4
			local x = o
			local _x = x + font_h*2 + o
			local max_lines = mf(H2 - o, font_h)
			local y = H-o-font_h
			local limit = math.max(1, tbl_size - max_lines + 1)
			local k = 0
			local t_w_o = string_width("00:00 - ")
			local glassed = false
			local second = seconds()
		
			for i = tbl_size, limit, -1 do
				local t = ghub.hub_log[i]
				local _y = y - k*(font_h + 2)
				local seconds = t.seconds
				
				if seconds >= 3600 then
					t_w_o = string_width("00:00:00 - ")
				end
				
				if not glassed then if (second - seconds >= MAX_LOG_GLASS_TIMER) and not hard_glass then glassed = true LbDraw_SetFlagsOn(LB_DRAW_FLAG_GLASS) end end

				DrawBox(x, _y, font_h*2, font_h, 0)
				DrawBox(x+1, _y+1, font_h*2-2, font_h-2, t.color)
				LbDraw_Text(_x, _y, t.timer_str .. " - ", 0) --timer_str
				LbDraw_Text(_x + t_w_o, _y, t.str, 0)
				
				if t.host then LbDraw_ScaledSprite(x+1, _y+1, get_sprite(0, GAME_MANAGER_SPRITE) , font_h*2-2, font_h-2) end
				
				k = k + 1
			end
			
			LbDraw_SetFlagsOff(LB_DRAW_FLAG_GLASS)
		else
			hub_ret = false
		end
	end
	
	if hub_ret then return end
	
	-- hub
	
	local tt
	local map_ptr = ghub.map_info_ptr
	local zone, hover = nil, nil
	local t3 = hub_cache.t3
	local t4 = hub_cache.t4
	local t5 = hub_cache.t5
	local centered_w = hub_cache.centered_w
	local content_y = hub_cache.content_y
	local totalH = hub_cache.totalH
	local map_picking_card_h = hub_cache.map_picking_card_h
	local zones_x = hub_cache.zones_x
	local zones_w = hub_cache.zones_w
	local sound_btn_size = hub_cache.sound_btn_size
	local bottom_btns_size = hub_cache.bottom_btns_size
	local l_holding = pmisc.l_mouse_pressing
	if not hub_cache.point_system_cache then hub_cache.point_system_cache = points_systems[ggameplay.map_target_score_type] end
	--LbDraw_Text(0, H - 20, tostring(hovering) .. " " .. tostring(hovering_id) .. " " .. tostring(hovering_zone), 0)

	-- borders and textures
	
	for k, border in ipairs(hub_cache.borders) do
		local x, y, w, h = border.x, border.y, border.w, border.h
		local texture = border.texture
		if texture then
			local actual_texture = texture
			local border_id = border.id
			if border_id then
				if not zone then
					zone = try_to_hover_zone(x, y, w, h, border_id)
					if zone then actual_texture = actual_texture - 1 end
				end
				if teams_ptrs[border_id].locked then
					actual_texture = GUI_COLOR_LOCKED
				end
			end
			fill_with_hfx_sprite_clipped(x, y, w, h, actual_texture, border.flag)
		end
		draw_border(x, y, w, h, border.name, true, true, true, true, true, true, true, true, 	border.half) --border.name or "gray" ???
	
		--cards
		set_font(FONT_SMALL)
		if k > 1 and k <= 6 then
			local idx = k - 1
			local players_tbl = teams_ptrs[idx].players
			--if #players_tbl > 0 then
				if idx == 1 then
					local card_tbl = hub_cache.card_full
					local _x, _y = x + o, y + o
					
					for p, player in ipairs(players_tbl, _x, _y) do
						if draw_player_card(_x, _y, card_tbl, player, hover) then
							hover = true
						end
						
						if p % 2 == 0 then
							_x = x + o
							_y = _y + card_tbl.h + o
						else
							_x = _x + card_tbl.w + o
						end
					end
					
					if ghub.active_players < 8 then
						local size = floor(card_tbl.icon_size / 1.25)
						local half_size = size // 2
						local add_x, add_y = x + w - w // 4 - half_size, y + card_tbl.h*3 + o*4 + card_tbl.h // 2 - size // 2
						local spr = 1946
						if not hover then
							hover = try_to_hover(add_x, add_y, size, size, SECTION_ADD_AI)
							if hover then
								spr = spr + 1
								PopSetFont(10, 0)
								LbDraw_Text(add_x + size + size // 2, add_y + half_size - h1 // 2, "add AI", 0)
							end
						end
						LbDraw_ScaledSprite(add_x, add_y, get_sprite(0, spr), size, size)
						set_font(FONT_SMALL)
					end
				else
					local card_tbl = hub_cache.card_short
					local _x, _y = x + o, y + o
					
					for p, player in ipairs(players_tbl, _x, _y) do
						if draw_player_card_short(_x, _y, card_tbl, player, hover) then
							hover = true
						end
						
						_y = _y + card_tbl.h + o
					end
				end
			--end
		end
	end
	
	--team names
	
	set_font(FONT_BIG)
	for _, team_name in ipairs(hub_cache.team_names) do
		LbDraw_Text(team_name.x, team_name.y, team_name.str, 0)
	end
	
	-- top title
	
	local str = "Setting Up Mini-Game #" .. ghub.games_played + 1
	local str_w2 = string_width(str) // 2
	local str_x = W2 - str_w2
	local max_title_y = math.max(content_y - h3, 0)
	local str_y = clamp(content_y - h3*2, 0, max_title_y)
	LbDraw_Text(str_x, str_y, str, 0)

	-- start game btn
	
	local countdown = ggameplay.countdown
	str = countdown < 0 and "Start Game" or "Starting in " .. countdown // 12
	local str_w = string_width(str)
	str_w2 = str_w // 2
	str_x = W2 - str_w2
	str_y = content_y + totalH - thickness - h3*2
	draw_border(str_x-2, str_y-2, str_w+4, h3+4, "gray", true, true, true, true, true, true, true, true, 	true)
	local texture = GUI_COLOR_I
	if not hover then
		hover = try_to_hover(str_x, str_y, str_w, h3, SECTION_START_GAME)
		if hover then texture = texture - 1 end
	end
	fill_with_hfx_sprite_clipped(str_x-2, str_y-2, str_w+4, h3+4, texture, LB_DRAW_FLAG_INVERT_GLASS)
	LbDraw_Text(str_x, str_y, str, 0)
	if im_host and countdown >= 0 then
		PopSetFont(11, 0)
		str = "click to cancel"
		LbDraw_Text(str_x + str_w // 2 - string_width(str) // 2, str_y + h3 + thickness, str, 0)
	end

	-- lock and team scores
	
	local locked_spr = 1748
	local unlocked_spr = 1746
	for k, lock in ipairs(hub_cache.locks) do
		local shake = 0
		local shake_tbl = pmisc.lock
		if shake_tbl.id == k then shake = math.random(-1, 1) end
		local ptr = teams_ptrs[k]
		local locked = ptr.locked
		local spr = locked and locked_spr or unlocked_spr
		local x, y = lock.x, lock.y
		if not hover then
			hover = try_to_hover(x, y, lock_size, lock_size, SECTION_LOCKS, k)
			if hover then texture = texture - 1 spr = spr + 1 if (not multiplayer) then tt = "Locks zone. Multiplayer only feature." elseif (#ghub.humans_tbl < 2) then tt = "Locks Zone. Requires > 1 humans playing." end end
		end
		LbDraw_ScaledSprite(x + shake, y, get_sprite(0, spr), lock_size, lock_size)
	end
	--set_font(FONT_SMALL)
	PopSetFont(11, 0)
	for k, team_score in ipairs(hub_cache.team_scores) do
		local ptr = teams_ptrs[k + 1]
		local score = tostring(ptr.points)
		local scorex, scorey = team_score.x, team_score.y
		DrawBox(scorex, scorey, team_score_size, lock_size, 10)
		DrawBox(scorex+1, scorey+1, team_score_size-2, lock_size-2, 0)
		LbDraw_Text(scorex+2, scorey + centered_team_score_y, score, 0)
	end
	
	-- map list
	
	local names_list = ghub.circular_maps_cache
	local map_y_offset = ghub.map_changer_offset
	local maps_fonts
	
	if map_y_offset == 0 then
		maps_fonts = {FONT_SMALL, FONT_SMALL, FONT_MEDIUM, FONT_BIG, FONT_MEDIUM, FONT_SMALL, FONT_SMALL}
	elseif map_y_offset > 0 then
		if everyFramer(2) then ghub.map_changer_offset = map_y_offset - 1 end
		maps_fonts = {FONT_SMALL, FONT_MEDIUM, FONT_BIG, FONT_MEDIUM, FONT_SMALL, FONT_SMALL, FONT_SMALL}
	elseif map_y_offset < 0 then
		if everyFramer(2) then ghub.map_changer_offset = map_y_offset + 1 end
		maps_fonts = {FONT_SMALL, FONT_SMALL, FONT_SMALL, FONT_MEDIUM, FONT_BIG, FONT_MEDIUM, FONT_SMALL}
	end
	
	local zone_x = zones_x[1]
	local zone_w = zones_w[1]
	clip_region(zone_x, t3, zone_w, map_picking_card_h)
	local longest_map_str = 0
	local map_name_y = hub_cache.maps_texts_y[1].y + map_y_offset
	for k, map_name in ipairs(hub_cache.maps_texts_y) do
		LbDraw_SetFlagsOn(LB_DRAW_FLAG_GLASS)
		if k == 4 then LbDraw_SetFlagsOff(LB_DRAW_FLAG_GLASS) end
		set_font(maps_fonts[k])
		local name_str = names_list[k]
		local x, y = map_name.x - string_width(name_str) // 2, map_name_y
		LbDraw_Text(x, y, name_str, 0)
		longest_map_str = math.max(longest_map_str, string_width(name_str))
		map_name_y = map_name_y + CharHeight2() + oo
	end
	stop_clipping()
	PopSetFont(5, 0)
	local maps_amt = MAX_MAPS
	local str = ghub.selected_map .. "/" .. maps_amt
	LbDraw_Text(zone_x + zone_w - string_width(str) - 1, t3 + map_picking_card_h - font_heights[FONT_SMALL] - 1, str, 0)
	LbDraw_SetFlagsOff(LB_DRAW_FLAG_GLASS)
	local map_hover_x = zone_x + zone_w // 2 - longest_map_str // 2
	local move_map_h = h1 + h2 + oo
	if im_host then
		for i = 1, 2 do
			local bot_btn = btn(i == 2)
			local _y = t3 + bot_btn*(h3 + move_map_h + oo*2)
			if im_host and not hover then
				hover = try_to_hover(map_hover_x, _y, longest_map_str, move_map_h, SECTION_CHANGE_MAP, i)
				if --[[framer % 64 < 32 and]] hover then
					local arrow_s = h2
					local arrow_gap = arrow_s // 2
					local arrow_x, arrow_y = map_hover_x - arrow_s - arrow_gap, _y + move_map_h // 2 - arrow_s // 2
					local arrow_spr = get_sprite(0, 2548 - bot_btn*2)
					LbDraw_ScaledSprite(arrow_x, arrow_y, arrow_spr, arrow_s, arrow_s)
					LbDraw_ScaledSprite(arrow_x + arrow_gap*2 + arrow_s + longest_map_str, arrow_y, arrow_spr, arrow_s, arrow_s)
				end
			end
		end
	end

	-- map preview and author

	local preview_x, preview_w = zones_x[2], zones_w[2]
	local map_hover_img = pmisc.map_hover_img
	local spr = map_hover_img.curr
	LbDraw_ScaledSprite(preview_x, t3, get_sprite(0, spr), preview_w, map_picking_card_h)
	--if not dragging_card() then
		--if not hover then
			local min = map_hover_img.min
			--if cursor_inside(preview_x, t3, preview_w, map_picking_card_h) then
				if framer % 4 == 0 then
					map_hover_img.curr = spr + 1
					if spr + 1 >= map_hover_img.max then
						map_hover_img.curr = min
					end
				end
			--else
				--map_hover_img.curr = min
			--end
		--end
	--end
	local author = map_ptr.author
	LbDraw_Text(preview_x + preview_w - string_width(author) - 1, t3 + map_picking_card_h - h1 - 1, author, 0)
	
	-- map text description/rules

	PopSetFont(map_desc_font, 0)
	local desc = map_ptr.desc
	local desc_y_offset = 0
	local desc_x, desc_w = zones_x[3], zones_w[3]
	local offset = pmisc.rules_offset
	clip_region(desc_x, t3, desc_w, map_picking_card_h)
	local content_h = draw_map_rules(desc, desc_x + 2, t3 + 2 - offset, desc_w - 4, map_picking_card_h - 4)
	local offset_max = math.max(0, content_h - (map_picking_card_h - 4))
	stop_clipping()
	for k, desc_button in ipairs(hub_cache.desc_buttons) do
		local x, y, size = desc_button.x, desc_button.y, desc_button.size
		local spr = desc_button.spr
		if not hover then
			hover = try_to_hover(x, y, size, size, SECTION_MAP_DESCRIPTION_BTNS, k)
			if hover then
				spr = desc_button.hover_spr
				if l_holding and not dragging_card() then
					if k == 2 then
						pmisc.rules_offset = clamp(offset - 3, 0, offset_max)
					elseif k == 3 then
						pmisc.rules_offset = clamp(offset + 3, 0, offset_max)
					end
				end
			end
		end
		LbDraw_ScaledSprite(x, y, get_sprite(0, spr), size, size) --hover_spr
	end

	-- map objectives
	
	set_font(FONT_SMALL)
	local cached_objectives = ghub.map_objectives
	local value_types = { " minutes", " points", " rounds" }
	for k, map_goal in ipairs(hub_cache.map_goals) do
		local objective = cached_objectives[k]
		local x, y, w, h = map_goal.x, map_goal.y, map_goal.w, map_goal.h
		local _o = 0
		local available = objective.available
		local activated = objective.active
		
		local hovering_goal_zone = cursor_inside(x, y, w, h)
		local hovering_btns_cache = {false, false}
		if hovering_goal_zone then
			_o = 1
			if available and not hover then
				if activated then
					local btn_x = map_goal.btn_x
					for i = 1, 2 do
						local section = SECTION_MAP_OBJ_BTN_MINUS + btn(i == 2)
						if not hover then hover = try_to_hover(btn_x, map_goal.centered_y, h1, h1, section, k) if hover then hovering_btns_cache[i] = true end end
						btn_x = btn_x + h1 + 1
					end
				end
				if not hover then
					hover = try_to_hover(x, y, w, h, SECTION_MAP_OBJECTIVES, k)
				end
			end
		end
		local texture = not available and GUI_COLOR_H or (activated and GUI_COLOR_E - _o or GUI_COLOR_F - _o)
		local flag = not activated and LB_DRAW_FLAG_GLASS or nil
		fill_with_hfx_sprite_clipped(x, y, w, h, texture, flag)
		if available then
			if not activated then LbDraw_SetFlagsOn(LB_DRAW_FLAG_GLASS) end
			local centered_content = map_goal.centered_y
			LbDraw_Text(map_goal.txt_x, centered_content, map_goal.txt, 0)
			x, w = map_goal.goal_value_x, map_goal.goal_value_w
			DrawBox(x, centered_content - 2, w, h1 + 4, 0)
			DrawBox(x+1, centered_content - 2+1, w-2, h1 + 4-2, 1)
			local value = objective.value
			local value_type = value_types[k]
			if value == 1 then  value_type = value_type:sub(1, -2) end
			LbDraw_Text(x+2, centered_content, value .. value_type, 0)
			x = x + w + 2
			for i = 1, 2 do
				spr = 1742 + (i-1)*2 + btn(hovering_btns_cache[i])
				LbDraw_ScaledSprite(x, centered_content, get_sprite(0, GUI_COLOR_K), h1, h1)
				LbDraw_ScaledSprite(x+1, centered_content+1, get_sprite(0, spr), h1-2, h1-2)
				x = x + h1 + 1
			end
			LbDraw_SetFlagsOff(LB_DRAW_FLAG_GLASS)
		end
	end
	
	-- map search
	
	for k, search_zone in ipairs(hub_cache.search_zones) do
		local x, y, w, h = search_zone.x, search_zone.y, search_zone.w, search_zone.h
		DrawBox(x, y, w, h, 0)
		if k == 1 then
			LbDraw_ScaledSprite(x + 2, y + 2, get_sprite(0, 1944), w - 4, h - 4)
		else
			local searching = pmaster.searching
			local active = searching.active
			local txt_y
			local curr_txt
			if active then
				set_font(FONT_MEDIUM)
				txt_y = y
				curr_txt = searching.str
			else
				PopSetFont(0, 0)
				txt_y = y + search_zone.centered_y
				curr_txt = "search for a map..."
			end
			
			if not hover then
				hover = try_to_hover(x, y, w, h, SECTION_SEARCH_MAP)
			end
			LbDraw_Text(x + 2, txt_y, curr_txt, 0)
			if active then
				if framer % 64 < 16 then
					LbDraw_Text(x + 2 + string_width(curr_txt), txt_y, "/", 0)
				end
				
				set_font(FONT_SMALL)
				local maps = searching.map_list
				local search_y = y + h2 + 1
				
				for m, map in ipairs(maps) do
					local _h = 2 + h1
					if search_y + _h >= H then break end
					
					local bg = 1
					if not hover then
						hover = try_to_hover(x, search_y, w, _h, SECTION_SEARCH_MAP_OPTION, m)
						if hover then bg = 10 end
					end
					
					
					DrawBox(x, search_y, w, _h, 0)
					DrawBox(x+1, search_y, w-2, _h-2, bg)
					LbDraw_Text(x + 2, search_y, map, 0)
					search_y = search_y + _h
				end
			end
		end
	end
	
	-- map custom rules / advanced options

	local custom_rules_tbl = map_ptr.custom_rules
	if custom_rules_tbl then
		set_font(FONT_SMALL)
		local rules_tbl = hub_cache.custom_rules
		local rules_bg = rules_tbl.bg
		local zone_x, zone_y, zone_w, zone_h = rules_bg.x, rules_bg.y, rules_bg.w, rules_bg.h
		draw_border(zone_x, zone_y, zone_w, zone_h, "gray", true, true, true, true, true, true, true, true, 	true)
		fill_with_hfx_sprite_clipped(zone_x, zone_y, zone_w, zone_h, GUI_COLOR_H, LB_DRAW_FLAG_INVERT_GLASS)
		local rules_title = rules_tbl.title
		LbDraw_Text(rules_title.x, rules_title.y, rules_title.txt, 0)
		local q_spr, cross_spr = get_sprite(0, 551), get_sprite(0, 449)
		PopSetFont(custom_rules_font, 0)
		local rules_cache_tbl = ghub.custom_rules
		for k, rule in ipairs(rules_tbl.content) do
			if not custom_rules_tbl[k] then break end
			local rule_active = rules_cache_tbl[k]
			local q_x, q_y, q_s = rule.q_x, rule.q_y, rule.q_s
			if not hover then
				if cursor_inside(q_x, q_y, q_s, q_s) then
					tt = custom_rules_tbl[k].desc
					hover = true
				end
			end
			LbDraw_ScaledSprite(q_x, q_y, q_spr, q_s, q_s)
			local btn_x, btn_y, btn_s = rule.btn_x, rule.btn_y, rule.btn_s
			local clr = 1
			if not hover then
				hover = try_to_hover(btn_x, btn_y, btn_s, btn_s, SECTION_ADVANCED_OPTIONS_BTNS, k)
				if hover then LbDraw_SetFlagsOn(LB_DRAW_FLAG_GLASS) clr = 10 end
			end
			DrawBox(btn_x, btn_y, btn_s, btn_s, clr)
			LbDraw_SetFlagsOff(LB_DRAW_FLAG_GLASS)
			local _o = mf(btn_s, 8)
			if rule_active then
				LbDraw_ScaledSprite(btn_x + _o, btn_y + _o, cross_spr, btn_s - _o*2, btn_s - _o*2)
			end
			local rule_max_w = rules_tbl.rule_name_w
			local rule_name = custom_rules_tbl[k].name or ""
			local txt_x, txt_y = rule.txt_x + 1, rule.btn_y
			local txt_max_w = rule_max_w - 2
			DrawBox(txt_x, txt_y, rule_max_w, custom_rules_font_h, 1)
			local text_fit = fit_text_in_line(rule_name, txt_max_w)
			LbDraw_Text(txt_x, txt_y, text_fit, 0)
		end
	end
	
	-- bottom mid icons
	
	local bottom_btns_amt = 0
	local reqs_cache = {}
	for k, bottom_btn in ipairs(hub_cache.bottom_btns) do 
		local req = bottom_btn.req()
		reqs_cache[k] = req
		if req then
			bottom_btns_amt = bottom_btns_amt + 1
		end
	end
	local bottom_btns_gap = 4
	local unit_w = (bottom_btns_size + bottom_btns_gap)
	local bottom_btns_total = bottom_btns_amt * unit_w + bottom_btns_gap
	local bottom_btns_x = W2 - bottom_btns_total // 2
	local bottom_btn_hovering_tbl = nil
	for k, bottom_btn in ipairs(hub_cache.bottom_btns) do
		if reqs_cache[k] then
			local spr = type(bottom_btn.spr) == "function" and bottom_btn.spr() or bottom_btn.spr
			DrawBox(bottom_btns_x, t5, bottom_btns_size, bottom_btns_size, 0)
			DrawBox(bottom_btns_x+1, t5+1, bottom_btns_size-2, bottom_btns_size-2, 1)
			local _o = 0
			if not hover then
				hover = try_to_hover(bottom_btns_x, t5, bottom_btns_size, bottom_btns_size, SECTION_BOT_MIDDLE_BUTTONS, bottom_btn.id)
				if hover then _o = math.max(1, bottom_btns_size // 16) bottom_btn_hovering_tbl = { id = bottom_btn.id, x = bottom_btns_x, y = t5 } end
			end
			LbDraw_ScaledSprite(bottom_btns_x+1 + _o, t5+1 + _o, get_sprite(0, spr), bottom_btns_size-2 - _o*2, bottom_btns_size-2 - _o*2)
			bottom_btns_x = bottom_btns_x + unit_w
		end
	end
	
	-- sound icons
	
	local muted_btn = btn(pmisc.sounds_muted)
	for k, sound_button in ipairs(hub_cache.sound_buttons) do
		local clr = k == 1 and 10 or 1
		local x, y = sound_button.x, sound_button.y
		local bg = k == 1 and (3 - muted_btn) or 0
		local sprite = k == 1 and 1801 or 1801 + muted_btn*4
		DrawBox(x, y, sound_btn_size, sound_btn_size, bg)
		DrawBox(x+1, y+1, sound_btn_size-2, sound_btn_size-2, clr)
		local _o = 0
		if not hover then
			hover = try_to_hover(x, y, sound_btn_size, sound_btn_size, SECTION_VOLUME_BUTTONS, k)
			if hover then
				_o = math.max(1, sound_btn_size // 12)
				if not pmisc.sounds_muted and k > 1 then
					PopSetFont(11, 0)
					local sound_name = hub_sounds[#hub_sounds - (k-2)]:match("^yyy_s_(.*)")
					LbDraw_Text(math.min(x, W - string_width(sound_name) - 1), y + sound_btn_size + 5, sound_name, 0)
				end
			end
		end
		LbDraw_ScaledSprite(x+2 + _o, y+2 + _o, get_sprite(0, sprite), sound_btn_size-4 - _o*2, sound_btn_size-4 - _o*2)
	end
	
	local cdr = pmisc.sounds_cdr
	if cdr > 0 then
		local sounds_cdr_tbl = hub_cache.sound_buttons_cdr
		local _x, _y, _w, _h = sounds_cdr_tbl.x, sounds_cdr_tbl.y + sound_btn_size + 3, sounds_cdr_tbl.w, 2
		local max_cdr = 12 * HUB_SOUND_CDR_SECONDS
		local percent = floor((cdr / max_cdr) * 100)
		local barpercent = math.min(_w, floor((_w * percent) / 100))
		DrawBox(_x, _y, barpercent, _h, 14)		
	end
	
	-- dragging card
	
	local dragging_player = dragging_card()
	if dragging_player then
		set_font(FONT_SMALL)
		draw_player_card_short(mouseX, mouseY, hub_cache.card_short, dragging_player, hover, true)
	end
	
	-- front menus (when hovering something)
	
	local hovering_past_mg = false

	if bottom_btn_hovering_tbl and not dragging_card() then
		local id = bottom_btn_hovering_tbl.id
		local x, y = bottom_btn_hovering_tbl.x, bottom_btn_hovering_tbl.y
		local f1, f2 = h3
		o = 3
		
		if id == BOT_MIDDLE_BUTTON_POINT_SYSTEM then
		
			set_font(FONT_BIG)
			local str = "Points System:"
			local title_w = string_width(str)
			set_font(FONT_SMALL)
			local arrow_w = h1 * 2
			local longest = string_width(points_systems_longest_string)
			local big_oo = oo * 2
			local w = math.max(longest + big_oo + string_width(points_systems_longest_pts_string) + o*4 + arrow_w, title_w + o*2)
			local h = POINTS_SYSTEM_MAX_TYPES * (h1+o) + h3 + o*2
			y = y - h - 3
			draw_border(x, y, w, h, "orange_shadow", true, true, true, true, true, true, true, true, 	true)
			fill_with_hfx_sprite_clipped(x, y, w, h, GUI_COLOR_J)
			set_font(FONT_BIG)
			local curr_y = y + o
			LbDraw_Text(x + w // 2 - title_w // 2, curr_y, str, 0)
			curr_y = curr_y + o + h3
			set_font(FONT_SMALL)
			x = x + o*2 + arrow_w
			local curr = ggameplay.map_target_score_type
			
			for i = 1, POINTS_SYSTEM_MAX_TYPES do
				if i == curr then
					LbDraw_SetFlagsOff(LB_DRAW_FLAG_GLASS)
					LbDraw_ScaledSprite(x - o - arrow_w, curr_y, get_sprite(0, 1943), arrow_w, h1)
				else
					LbDraw_SetFlagsOn(LB_DRAW_FLAG_GLASS)
				end
				
				str = points_systems[i].name
				LbDraw_Text(x, curr_y, str, 0)
				LbDraw_Text(x + longest + big_oo, curr_y, points_systems[i].dist_str, 0)
				
				curr_y = curr_y + o + h1
			end
			
			LbDraw_SetFlagsOff(LB_DRAW_FLAG_GLASS)
			
			--deprecated (AIs will now have individual difficulties)
		elseif id == BOT_MIDDLE_BUTTON_DISABLE_EMOJIS then
			if multiplayer then
				local curr = gmisc.emojis_enabled
				local str = curr and "In-game emojis are enabled, for both humans and AI. Players can press 'T' to create emojis." 
									or "In-game emojis are disabled, for both humans and AI. This overrides the in-game personal emoji setting."

				tt = str
				hover = true
			end
			
			--[[
			set_font(FONT_BIG)
			local str = "AI Difficulty:"
			local title_w = string_width(str)
			set_font(FONT_SMALL)
			local arrow_w = h1 * 2
			local big_oo = oo * 2
			local w = math.max(big_oo + string_width(ai_difficulty_longest_string) + o*4 + arrow_w, title_w + o*2)
			local h = AI_MAX_DIFFICULTIES * (h1+o) + h3 + o*2
			y = y - h - 3
			draw_border(x, y, w, h, "orange_shadow", true, true, true, true, true, true, true, true, 	true)
			fill_with_hfx_sprite_clipped(x, y, w, h, GUI_COLOR_J)
			set_font(FONT_BIG)
			local curr_y = y + o
			LbDraw_Text(x + w // 2 - title_w // 2, curr_y, str, 0)
			curr_y = curr_y + o + h3
			set_font(FONT_SMALL)
			x = x + o*2 + arrow_w
			local curr = ggameplay.AI_difficulty
			
			for i = 0, AI_MAX_DIFFICULTIES-1 do
				if i == curr then
					LbDraw_SetFlagsOff(LB_DRAW_FLAG_GLASS)
					LbDraw_ScaledSprite(x - o - arrow_w, curr_y, get_sprite(0, 1943), arrow_w, h1)
				else
					LbDraw_SetFlagsOn(LB_DRAW_FLAG_GLASS)
				end
				
				str = AI_difficulties[i]
				LbDraw_Text(x, curr_y, str, 0)
				
				curr_y = curr_y + o + h1
			end
			
			LbDraw_SetFlagsOff(LB_DRAW_FLAG_GLASS)]]
			
		else --BOT_MIDDLE_BUTTON_PAST_MG_SCORES
			
			--table.insert(gmisc.past_played_minigames, { map = map, map_name = map_name, pts_system = ggameplay.map_target_score_type, teams = gmisc.post_mg_table, stats = stats })
			local past_played_amt = #gmisc.past_played_minigames
			
			if past_played_amt > 0 then
				hovering_past_mg = true
				local w, h = W, H
				local ps_font = w <= 1024 and 3 or 9
				PopSetFont(ps_font, 0)
				local box = CharHeight2()
				local half_box = box // 2
				local thickness = 4
				local longest_map = string_width(LONGEST_MAP_STR) + 4 + string_width((ghub.games_played >= 10) and "#77: " or "#7: ")
				local ps_horizontal_element = box*2
				local max_dur_str = "77:77:77"
				local max_dur_str_w = string_width(max_dur_str)
				local ps_w = box*3 + 12*(ps_horizontal_element) + longest_map + max_dur_str_w
			
				local ps_top = box*3
				local ps_bot = box*2
				local end_y = y - thickness - 3
				local ps_zone_h = h - (h - end_y) - box - ps_bot - ps_top
				local ps_h = ps_zone_h + ps_bot + ps_top
				local ps_y =  y - ps_h - thickness - 3
				local ps_x = math.min(mouseX, w - box - ps_w)
				local curr_y = ps_y + box
				local curr_x = ps_x + longest_map + box + box + box + max_dur_str_w
				local timer_x_start = curr_x - box - max_dur_str_w
				local timers_x = timer_x_start + max_dur_str_w // 2
				local cols_x = {}
				local map_name_x = ps_x + box
			
				draw_border(ps_x, ps_y, ps_w, ps_h, "orange_shadow", true, true, true, true, true, true, true, true, 	true)
				fill_with_hfx_sprite_clipped(ps_x, ps_y, ps_w, ps_h, 1768)
				
				-- DrawBox(ps_x+4, ps_y, 30, ps_top, 4)
				-- DrawBox(ps_x+8, end_y - ps_bot, 30, ps_bot, 5)
				-- DrawBox(ps_x+12, ps_y + ps_top, 30, ps_zone_h, 3)
				
				for i = 1, 12 do
					local spr = 1808 + i + ((i > 8) and 396 or 0)
					local _x = curr_x + (i-1)*ps_horizontal_element
					cols_x[i] = _x
					
					LbDraw_ScaledSprite(_x, curr_y, get_sprite(0, spr), box, box)
				end
			
				local max_mg_showing = floor(ps_zone_h / ps_horizontal_element)
				
				if past_played_amt > max_mg_showing then
					PopSetFont(11, 0)
					local str = "Hold | / { to scroll"
					local str_w = string_width(str)
					LbDraw_Text(mouseX - str_w // 2, y + 4 + bottom_btns_size, str, 0)
					PopSetFont(ps_font, 0)
				end

				if framer % 8 == 0 then
					local speed = l_holding and 1 or pmisc.r_mouse_pressing and -1 or 0
					pmisc.hovering_past_mg = pmisc.hovering_past_mg + speed
				end
				
				local offset = pmisc.hovering_past_mg
				offset = clamp(offset, -(past_played_amt - max_mg_showing), 0)
				pmisc.hovering_past_mg = offset
				local bottom_mg_idx = past_played_amt + offset
				local top_mg_idx = math.max(1, bottom_mg_idx - max_mg_showing + 1)
				--LOG(top_mg_idx .. " " .. bottom_mg_idx .. " " .. offset)
				curr_y = curr_y + ps_horizontal_element
			
				for k = top_mg_idx, bottom_mg_idx do
					--zebra visual
					if k % 2 == 0 then
						DrawBox(ps_x, curr_y - half_box, ps_w, ps_horizontal_element, 1)
						fill_with_hfx_sprite_clipped(ps_x, curr_y - half_box, ps_w, ps_horizontal_element, 1769, LB_DRAW_FLAG_GLASS)
					end
					
					local past_mg = gmisc.past_played_minigames[k]
					local map_name = past_mg.map_name
					local pts_system = past_mg.pts_system
					
					local map_str = "#" .. tostring(k) .. ": " .. map_name
					local map_stats = past_mg.stats
					local map_participants = past_mg.teams
					
					LbDraw_Text(map_name_x, curr_y, map_str, 0)
					
					local duration = past_mg.cached_duration_str
					local dur_w = string_width(duration)
					local dur_x = timers_x - dur_w // 2
					DrawBox(timer_x_start-1, curr_y-1, max_dur_str_w+2, box+2, 9)
					DrawBox(timer_x_start, curr_y, max_dur_str_w, box, 10)
					LbDraw_Text(dur_x, curr_y, duration, 0)
					
					for _, participant in ipairs(past_mg.teams) do
						local uid = participant.uid
						local position = participant.position
						local place_sprite = 2204 + position
						local _ps_x = cols_x[uid]--curr_x + (uid-1)*ps_horizontal_element
						local awarded = participant.awarded
						local awarded_prefix = "+"
						
						LbDraw_ScaledSprite(_ps_x, curr_y, get_sprite(0, place_sprite), box, box)
						
						if awarded == 0 then
							PopSetFont(4, 0)
						else
							if awarded < 0 then
								awarded_prefix = ""
								PopSetFont(11, 0)
							else
								PopSetFont(10, 0)
							end
						end
						
						local ps__str = awarded_prefix .. "" .. awarded
						local str_w = string_width(ps__str)
						
						LbDraw_Text(_ps_x + box - mf(ps__str, 2), curr_y + box - half_box, ps__str, 0)
					end
					
					curr_y = curr_y + box*2
					PopSetFont(ps_font, 0)
				end
			
				curr_y = end_y - ps_bot
				
				local full_time = secondsToClock(ghub.total_game_time, false)
				LbDraw_Text(timers_x - string_width(full_time) // 2, curr_y, full_time, 0)
				
				local total_str = "* Total *"
				LbDraw_Text(map_name_x, curr_y, total_str, 0)
				PopSetFont(4, 0)
				local total_center = curr_x + half_box
				local total_center_y = curr_y + half_box - mf(CharHeight2(), 2)
				local did_award_manual_points = ghub.did_award_manual_points
				local manual_pts_tbl = ghub.awarded_manual_points
				
				DrawBox(curr_x-1, curr_y-1, 23*box+2, box+2, 0)
				DrawBox(curr_x, curr_y, 23*box, box, 1)
				
				if did_award_manual_points then
					PopSetFont(8, 0)
					local awarded_str = "*Manually awarded Points ---->"
					LbDraw_Text(map_name_x, curr_y + box + 2, awarded_str, 0)
					PopSetFont(4, 0)
				end
				
				for i = 1, 12 do
					local total_pts = tostring(uid_to_table_ptr(i).points)
					local t_w = string_width(total_pts)
					local t_x = total_center - mf(t_w, 2) + (i-1)*ps_horizontal_element
					LbDraw_Text(t_x, total_center_y, total_pts, 0)
					
					local manual_awarded = manual_pts_tbl[i]
					
					if manual_awarded > 0 then
						PopSetFont(8, 0)
						local mpts = "(" .. manual_awarded .. ")"
						t_x = total_center - mf(string_width(mpts), 2) + (i-1)*ps_horizontal_element
						LbDraw_Text(t_x, curr_y + box + 2, mpts, 0)
						PopSetFont(4, 0)
					end
				end
			end
		end
	end

	--tooltips
	if tt then
		local tt_width_divider = W >= 800 and 3 or 5
		PopSetFont(3, 0)
		local fonth = CharHeight2()
		local gap = 4
		local thickness = 3
		local tt_w = mf(W, tt_width_divider) + gap*2
		local tt_h_text, tt_w = type_text_in_area(tt, 0, 0, tt_w, fonth, false)
		local tt_h = tt_h_text + gap*2
		local tt_x = math.max(mouseX - 12 - tt_w, 4 + thickness)
		local tt_y = math.min(mouseY - 4, H - tt_h - 4 - thickness)
		DrawBox(tt_x, tt_y, tt_w, tt_h, 12)
		draw_border(tt_x, tt_y, tt_w, tt_h, "orange_shadow", true, true, true, true, true, true, true, true, true)
		type_text_in_area(tt, tt_x + gap, tt_y + gap, tt_w, fonth, true)
	end
	
	if not hovering_past_mg then
		pmisc.hovering_past_mg = 0
	end
	
	if not zone then
		reset_zone_hovering()
	end

	if not hover then
		reset_mouse_btns()
	end
end

function draw_player_card(x, y, card_tbl, player, hover)
	local dragging_this = (dragging_card() == player)
	if dragging_this then return end
	local _hover = hover
	local card_w, card_h = card_tbl.w, card_tbl.h
	local eye_x, eye_y, eye_size = x + card_tbl.eye_x, y + card_tbl.eye_y, card_tbl.eye_size
	local nick_x, nick_y, nick_max_w = eye_x + card_tbl.nick_x, y + card_tbl.nick_y, card_tbl.nick_max_w
	local icon_x, icon_y, icon_size = x + card_tbl.icon_x, y + card_tbl.icon_y, card_tbl.icon_size
	local score_x, score_y, score_w, score_h = x + card_tbl.score_x, y + card_tbl.score_y, card_tbl.score_w, card_tbl.score_h
	local rdy_x, rdy_y, rdy_size = x + card_tbl.rdy_x, y + card_tbl.rdy_y, card_tbl.rdy_size
	local shaman_icon_offset = pmisc.shaman_frame
	
	local beta = btn(player > 3)
	local player_tbl = gplayers[player + 1]
	local is_human = (player_tbl.mode == TRIBE_HUMAN)
	local is_ready = player_tbl.ready
	local hovering_AI_remove_icon = false
	local is_spec = player_tbl.spectator
	if is_spec then LbDraw_SetFlagsOn(LB_DRAW_FLAG_GLASS) end
	local bg_fill = ENUM_PLAYER_COLORED_CARDS_BG_START + player + 31*btn(cursor_inside(x, y, card_w, card_h))
	
	if not _hover then
		_hover = try_to_hover(eye_x, eye_y, eye_size, eye_size, SECTION_CARD_EYE, player)
	end
	
	if not _hover then
		_hover = try_to_hover(rdy_x, rdy_y, rdy_size, rdy_size, SECTION_CARD_READY, player)
		if _hover then hovering_AI_remove_icon = true end
	end
	
	if not _hover then
		_hover = try_to_hover(x, y, card_w, card_h, SECTION_CARD, player)
	end
	
	fill_with_hfx_sprite_clipped(x, y, card_w, card_h, bg_fill, nil)
	if player == playernum then
		LbDraw_ScaledSprite(x, y, get_sprite(0, pmisc.card_border), card_w, card_h)
	end
	LbDraw_SetFlagsOff(LB_DRAW_FLAG_GLASS)
	DrawBox(eye_x, eye_y, eye_size, eye_size, 1)
	DrawBox(eye_x+1, eye_y+1, eye_size-2, eye_size-2, 0)
	if is_spec then
		LbDraw_ScaledSprite(eye_x, eye_y, get_sprite(0, 359), eye_size, eye_size)
	end
	local nick = player_tbl.full_name
	local nick_fit = fit_text_in_line_no_ellipsis(nick, nick_max_w)
	LbDraw_Text(nick_x, nick_y, nick_fit, 0)
	DrawBox(icon_x, icon_y, icon_size, icon_size, 1)
	DrawBox(icon_x+1, icon_y+1, icon_size-2, icon_size-2, 0)
	local sprite, pts = player_tbl.sprite + shaman_icon_offset, tostring(player_tbl.points)
	LbDraw_ScaledSprite(icon_x+1, icon_y+1, get_sprite(1 + beta, sprite), icon_size-2, icon_size-2)
	if player == TRIBE_BLUE then
		local crown_h = icon_size // 3
		LbDraw_ScaledSprite(icon_x+1, icon_y+1 - crown_h // 2, get_sprite(0, GAME_MANAGER_SPRITE), icon_size-2, crown_h)
	end
	DrawBox(score_x, score_y, score_w, score_h, 1)
	DrawBox(score_x+1, score_y+1, score_w-2, score_h-2, 0)
	PopSetFont(11, 0)
	LbDraw_Text(score_x+2, score_y+1, pts, 0)
	set_font(FONT_SMALL)
	sprite = not is_human and (AI_DIFF_ICON_START + player_tbl.difficulty*2 + btn(hovering_AI_remove_icon)*1) or 1791 + btn(is_ready)
	LbDraw_ScaledSprite(rdy_x, rdy_y, get_sprite(0, sprite), rdy_size, rdy_size)
	
	return _hover
end

function draw_player_card_short(x, y, card_tbl, player, hover, dragging)
	local dragging_this = (dragging_card() == player)
	if not dragging and dragging_this then return end
	
	local _x, _y = not dragging and x or mouseX, not dragging and y or mouseY
	local _hover = hover
	local score_x, score_y, score_w, score_h = x + card_tbl.score_x, y + card_tbl.score_y, card_tbl.score_w, card_tbl.score_h
	local dragging_score_extra_w = 4 + score_w
	local card_w, card_h = not dragging and card_tbl.w or card_tbl.w + dragging_score_extra_w, card_tbl.h
	local nick_x, nick_y, nick_max_w = _x + card_tbl.nick_x, _y + card_tbl.nick_y, card_tbl.nick_max_w
	local icon_x, icon_y, icon_size = _x + card_tbl.icon_x, _y + card_tbl.icon_y, card_tbl.icon_size
	local rdy_x, rdy_y, rdy_size = dragging and _x + card_tbl.rdy_x or _x + card_tbl.rdy_x - dragging_score_extra_w, _y + card_tbl.rdy_y, card_tbl.rdy_size
	hovering_AI_remove_icon = false
	local shaman_icon_offset = pmisc.shaman_frame
	
	
	local beta = btn(player > 3)
	local player_tbl = gplayers[player + 1]
	local is_human = (player_tbl.mode == TRIBE_HUMAN)
	local is_ready = player_tbl.ready
	local bg_fill = ENUM_PLAYER_COLORED_CARDS_BG_START + player + 31*btn(cursor_inside(_x, _y, card_w, card_h))
	
	if not dragging then
		if not _hover then
			_hover = try_to_hover(rdy_x, rdy_y, rdy_size, rdy_size, SECTION_CARD_READY, player)
			if _hover then hovering_AI_remove_icon = true end
		end
		
		if not _hover then
			_hover = try_to_hover(_x, _y, card_w, card_h, SECTION_CARD, player)
		end
	else
		DrawBox(_x-1, _y-1, card_w+2, card_h+2, 0)
	end
	
	fill_with_hfx_sprite_clipped(_x, _y, card_w, card_h, bg_fill, nil)
	if not dragging and player == playernum then
		LbDraw_ScaledSprite(x, y, get_sprite(0, pmisc.card_border), card_w, card_h)
	end
	local nick = player_tbl.full_name
	local nick_fit = fit_text_in_line_no_ellipsis(nick, nick_max_w)
	LbDraw_Text(nick_x, nick_y, nick_fit, 0)
	DrawBox(icon_x, icon_y, icon_size, icon_size, 1)
	DrawBox(icon_x+1, icon_y+1, icon_size-2, icon_size-2, 0)
	local sprite = player_tbl.sprite + shaman_icon_offset
	LbDraw_ScaledSprite(icon_x+1, icon_y+1, get_sprite(1 + beta, sprite), icon_size-2, icon_size-2)
	if player == TRIBE_BLUE then
		local crown_h = icon_size // 3
		LbDraw_ScaledSprite(icon_x+1, icon_y+1 - crown_h // 2, get_sprite(0, GAME_MANAGER_SPRITE), icon_size-2, crown_h)
	end
	if dragging then
		PopSetFont(11, 0)
		local pts = tostring(player_tbl.points)
		DrawBox(score_x, score_y, score_w, score_h, 1)
		DrawBox(score_x+1, score_y+1, score_w-2, score_h-2, 0)
		LbDraw_Text(score_x+2, score_y+1, pts, 0)
		set_font(FONT_SMALL)
	end
	sprite = not is_human and (AI_DIFF_ICON_START + player_tbl.difficulty*2 + btn(hovering_AI_remove_icon)*1) or 1791 + btn(is_ready)
	LbDraw_ScaledSprite(rdy_x, rdy_y, get_sprite(0, sprite), rdy_size, rdy_size)
	
	return _hover
end

function try_to_change_changelog(left)
	if left then
		pmisc.patchnotes_id = math.max(pmisc.patchnotes_id -1, 1)
	else
		pmisc.patchnotes_id = math.min(pmisc.patchnotes_id + 1, #patch_notes)
	end
end

function draw_patchnotes()
	if not (state == STATE_HUB) then return end
	local ver = pmisc.patchnotes_id
	local notes = patch_notes[ver]
	if not notes then return end
	
	local draw
	local w, h, guiW = W, H, GUIW
	local box = w <= 800 and w // 70 or w // 96
	local o = 0
	
	if not hovering then
		if not hovering_ingame then
			if cursor_inside(guiW + 2, 2, box+2, box+2) then
				o = math.max(1, box // 6)
				draw = true
				hovering = SECTION_VIEW_CHANGELOG
				hovering_id = 1
			end
		end
	end
	
	DrawBox(guiW + 2, 2, box+2, box+2, 0)
	DrawBox(guiW + 3, 3, box, box, 1)
	LbDraw_ScaledSprite(guiW + 3 - o, 3 - o, get_sprite(0, 40), box + o*2, box + o*2)
	
	if draw then
		local number, date, text = notes[1], notes[2], notes[3]
		local use_version = (ver > 1) and "version " or ""
		text = "<f9> " .. use_version .. number .. " <br> <f4> " .. date .. " <br> <f3> " .. text
		text = text .. " <p> <br> <f8> | or { to view other patch notes"
		local font = 3
		local fontH = CharHeight2()
		PopSetFont(3, 0)
		local thickness = 3
		local a, b = mouseX + 16 + thickness, mouseY + 16 + thickness
		local gap = 4
		local totalH, maxW = process_text(a, b, w <= 800 and w // 3 or w // 4, text, false, font, fontH, gap)
		draw_border(a, b, maxW, totalH, "orange_shadow", true, true, true, true, true, true, true, true, 	true)
		fill_with_hfx_sprite_clipped(a, b, maxW, totalH, ver > 1 and GUI_COLOR_I or GUI_COLOR_J)
		process_text(a, b, maxW, text, true, font, fontH, gap)
	else
		pmisc.patchnotes_id = #patch_notes
	end
end

function process_text(x, y, max_w, text, do_write, mainFont, mainFontH, gap)
	--keep the gap a small number.
	local x, y = x + gap, y + gap
    local limit_w = x + max_w + gap*2
	local paragraph_x = x + 4
    local currX = paragraph_x
    local currY = y
    local line_h = CharHeight2()
    local line = 1
    local max_used_w = 0

    for word in text:gmatch("%S+") do
		-- Font size change
		local fsize = word:match("^<f(%d+)>$")
		if fsize then
			currentFontID = tonumber(fsize)
			PopSetFont(currentFontID, 0)
			line_h = math.max(CharHeight2(), line_h)
		-- Paragraph
		elseif word == "<p>" then
			line = line + 2
			currX = paragraph_x
			currY = currY + line_h + line_h // 2
			line_h = CharHeight2()
		-- Line break
		elseif word == "<br>" then
			line = line + 1
			currX = paragraph_x
			currY = currY + line_h
			line_h = CharHeight2()
		-- Normal word
		else
			local w = word .. " "
			local w_width = string_width(w)
			if currX + w_width > limit_w then
				line = line + 1
				currX = x
				currY = currY + line_h
				line_h = CharHeight2()
			end
			if do_write then
				LbDraw_Text(currX, currY, w, 0)
			end
			currX = currX + w_width
			max_used_w = math.max(max_used_w, currX - x)
		end
	end

	PopSetFont(mainFont, 0)

    local total_height = currY - mouseY
    return total_height, max_used_w
end

-- host imgui helper

function open_host_imgui()
	pmaster.imgui_open = not pmaster.imgui_open
end

function OnImGuiFrame()
	if not im_host then return end
	
    if pmaster.imgui_open then
        imgui.Begin('Host Helper Guide & Cheats:', nil, ImGuiWindowFlags_AlwaysAutoResize)

		imgui.TextUnformatted("TAB:  opens/closes this menu.")
		imgui.NextColumn()
		imgui.TextUnformatted("")
		imgui.NextColumn()
		imgui.TextUnformatted("IN MENU:")
		imgui.NextColumn()
		imgui.TextUnformatted("Left_CTRL + ( (1-8) or (A-D) ):  awards 1 point to target tribe (blue-orange) or team (A-D).")
		imgui.NextColumn()
		imgui.TextUnformatted("Left_Shift + ( (1-8) or (A-D) ):  awards 10 points to target tribe (blue-orange) or team (A-D).")
		imgui.NextColumn()
		imgui.TextUnformatted("")
		imgui.NextColumn()
		imgui.TextUnformatted("IN GAME:")
		imgui.NextColumn()
		imgui.TextUnformatted("Left_Shift + F11:  ends the current mini-game prematurely. Points are still awarded.")
		imgui.NextColumn()
		imgui.TextUnformatted("Left_Shift + F12:  ends the current mini-game prematurely. No points are awarded.")
        
        imgui.End()
    end
end

-- INGAME DRAWING

local zoom_shaman_str = multiplayer and "zoom to shaman upon respawn" or "zoom + select shaman upon respawn"

personal_settings = {
	{ id = HOVERING_PERSONAL_SETTING_SCORE_SIZE, 		name = "smaller scores size", 					var = function() return psettings.scores_small_font end, 		action = function() psettings.scores_small_font = not psettings.scores_small_font game_draw_cache.scoreboard = nil end },
	{ id = HOVERING_PERSONAL_SETTING_RESPAWN_ZOOM, 		name = zoom_shaman_str, 						var = function() return psettings.zoom_on_respawn end, 			action = function() psettings.zoom_on_respawn = not psettings.zoom_on_respawn end },
	{ id = HOVERING_PERSONAL_SETTING_OBJECTIVES_HIDE, 	name = "only show objectives when hovering", 	var = function() return psettings.objectives_hide end, 			action = function() psettings.objectives_hide = not psettings.objectives_hide end },
	{ id = HOVERING_PERSONAL_SETTING_DISABLE_EMOJIS, 	name = "disable emojis", 						var = function() return psettings.disable_emojis end, 			action = function() psettings.disable_emojis = not psettings.disable_emojis end },
}
personal_settings_longest_str = "only show objectives when hovering mm"

function render_mg_personal_setttings()
	local low_w = (W <= 800)
	local box = W // (low_w and 48 or 64)
	local x, y = W - 4 - box, 4
	local o = 2
	local hovering_cog = cursor_inside(x, y, box, box)
	local hovering_dropdown = false

	if hovering_cog then
		o = 1
		hovering_ingame = HOVERING_PERSONAL_SETTINGS_BTN
	end
	
	DrawBox(x, y, box, box, 1)
	DrawBox(x+1, y+1, box-2, box-2, 0)
	LbDraw_ScaledSprite(x + o, y + o, get_sprite(0, 1850), box - o*2, box - o*2)

	if hovering_cog or hovering_ingame == HOVERING_PERSONAL_SETTINGS_BTN then
		reset_emoji_wheel()
		PopSetFont(low_w and 4 or 3, 0)
		local longest_name = string_width(personal_settings_longest_str)
		local _o = 4
		local _thickness = 4
		local _w, _h = box + _o*3 + longest_name, #personal_settings*(box + _o) + _o
		local _x, _y = x - _thickness - _w, y
		local fill = GUI_COLOR_J
		hovering_dropdown = cursor_inside(_x-_thickness, _y-_thickness, _w + _thickness*2 + 1, _h + _thickness*2 + 1)
		
		if hovering_dropdown then
			fill = fill + 1
		end

		draw_border(_x, _y, _w, _h, "creme", true, true, true, true, true, true, true, true, 	true)
		fill_with_hfx_sprite_clipped(_x, _y, _w, _h, fill)
		
		local spr = get_sprite(0, 84)
		local curr_x = _x + _o
		local curr_y = _y + _o
		local h_id = false
		
		for _, setting in ipairs(personal_settings) do
			DrawBox(curr_x, curr_y, box, box, 0)
			LbDraw_Text(curr_x + _o + box, curr_y, setting.name, 0)
			
			if setting.var() then
				LbDraw_ScaledSprite(curr_x+1, curr_y+1, spr, box-2, box-2)
			end
			
			if not hovering_cog and not h_id and cursor_inside(curr_x, curr_y, box, box) then
				h_id = true
				hovering_ingame_id = setting.id
			end
			
			curr_y = curr_y + _o + box
		end
	end
	
	if not hovering_cog and not hovering_dropdown then
		reset_ingame_cursors()
	end
	
	return hovering_cog or hovering_dropdown
end

function ready_set_traffic_light()
	local box = W // 24
	local total_w = box * READY_SET_COUNTDOWN
	local x = (W + GUIW) // 2 - total_w // 2
	local y = H // 6 - box // 2
	local timer = gmisc.ready_set_timer
	
	DrawBox(x-2, y-2, total_w+4, box+4, 10)
	
	for i = 1, READY_SET_COUNTDOWN do
		local spr = 1791 + btn(timer < (i-1) * 12)
		LbDraw_ScaledSprite(x + box*(i-1), y, get_sprite(0, spr), box, box)
	end
end

function render_mg_scoreboard()
	local font2 = 1 --4(we use 0 now cuz 4 too tiny)
	local o = 3
	local team_spr = get_sprite(0, 680)
	local tbl = game_draw_cache.scoreboard
	
	if not tbl then
		game_draw_cache.scoreboard = {}
		local scoreboard_cache = game_draw_cache.scoreboard
		local font1 = 3 + btn(psettings.scores_small_font)
		PopSetFont(font1, 0)
		scoreboard_cache.font1 = font1
		local box = CharHeight2()
		scoreboard_cache.box = box
		local x = W - o - box - o
		scoreboard_cache.x = x
		local participants_cache = {}
		
		for _, p_team in ipairs(_participants_by_teams) do
			local is_team = p_team.is_team
			local name, score, color = p_team.name, function() return tostring(p_team.points) end, is_team and 0 or p_team.color
			local name_w = string_width(name)
			participants_cache[p_team.uid] = { is_team = is_team, players = p_team.players, name = name, score = score, color = color, name_w = name_w, x = x, name_x = x - o - name_w, score_x = x + box + o }
		end
		
		scoreboard_cache.participants_cache = participants_cache
		return
	end
	--participant_by_team.players = { {tribe = player, color = color, name = name, ai_diff = is_ai and ptbl.difficulty or nil} }
	local hover = false
	local font1 = tbl.font1
	PopSetFont(font1, 0)
	local zero_str = string_width("0")
	local score_w = ggameplay.highest_score_decimals * zero_str + o + zero_str--string_width("-")
	local box = tbl.box
	local x, y = tbl.x - score_w, H - o - (#_participants_by_teams)*(box + o) + o
	local participants_cache = tbl.participants_cache

	for _, participant_team in ipairs(_participants_by_teams) do
		local team = participants_cache[participant_team.uid]
		DrawBox(x, y, box, box, 1)
		DrawBox(x+1, y+1, box-2, box-2, team.color)
		
		LbDraw_Text(team.name_x - score_w, y, team.name, 0)
		LbDraw_Text(team.score_x - score_w, y, team.score(), 0)
		
		if team.is_team then
			LbDraw_ScaledSprite(x+2, y+2, team_spr, box-4, box-4)
				
			if not hover then
				if cursor_inside(x, y, box, box) then
					hover = true
					local amt_players = #team.players
					PopSetFont(font2, 0) --font2 (just too tiny)
					local char_height = CharHeight2()
					local max = string_width("m")*12 + char_height*2+4
					local h = amt_players * (char_height + 1) + 1
					local thickness = 4
					local _x, _y = x - thickness*2 - max, y - thickness*2 - h
					draw_border(_x, _y, max, h, "orange_shadow", true, true, true, true, true, true, true, true, 	true)
					DrawBox(_x, _y, max, h, 1)
					
					for kk, player in ipairs(team.players) do
						local tribe = player.tribe
						local name = player.name
						local diff = player.ai_diff
						
						DrawBox(_x+1, _y+1, char_height, char_height, 0)
						DrawBox(_x+2, _y+2, char_height-2, char_height-2, player.color)
						if diff then
							LbDraw_ScaledSprite(_x+max-char_height, _y+1, get_sprite(0, 2538 + diff), char_height, char_height)
						end
						LbDraw_Text(_x+1 + char_height + 1, _y+1, name, 0)
						
						_y = _y + char_height + 1
					end
					
					PopSetFont(font1, 0)
				end
			end
		else
			local ai_diff = team.players[1].ai_diff
			if ai_diff then
				if not hover and cursor_inside(x, y, box, box) then
					hover = true
					LbDraw_ScaledSprite(x, y, get_sprite(0, 2538 + ai_diff), box, box)
				end
			end
		end
		y = y + o + box
	end
end

function render_mg_objectives()
	if hovering_ingame == HOVERING_PERSONAL_SETTINGS_BTN then
		--if W <= 800 then
			return
		--end
	end

	local tbl = game_draw_cache.map_objectives
	local f1, f2 = 4, 3
	local h1, h2 = 10, 19
	
	if not tbl then
		local objectives_tbl = ghub.map_objectives
		game_draw_cache.map_objectives = {}
		local map_objectives = game_draw_cache.map_objectives
		local sprites = {1940, 175, 2563}
		PopSetFont(f1, 0)
		local objs_tt = { "the game will end when %s passes.", "the game will end if someone reaches %s points.", "the game will end after %s rounds." }
		
		for i = MAP_OBJECTIVE_TIMER, MAX_MAP_OBJECTIVES do
			if objectives_tbl[i].active then
				local objective = {}
				local value = objectives_tbl[i].value
				objective.curr_value_ptr = i == MAP_OBJECTIVE_TIMER and function() return ggameplay.map_curr_duration end or i == MAP_OBJECTIVE_ROUNDS and function() return ggameplay.map_curr_round end or function() return ggameplay.highest_score end
				objective.value_integer = i == MAP_OBJECTIVE_TIMER and value * 60 or value
				objective.value = i == MAP_OBJECTIVE_TIMER and secondsToClock(value*60) or tostring(value)
				objective.value_w2 = string_width(objective.value) // 2
				objective.spr = sprites[i]
				objective.tt = string.format(objs_tt[i], objective.value)
				objective.curr_value = function() return ggameplay.cached_curr_values_strings[i] end
				objective.raw_value = value
				objective.uid = i
				table.insert(map_objectives, objective)
			end
		end
	
		return
	end
	
	local hover = false
	local box = h1 + h2 + 2
	local total_h = box + 4
	if psettings.objectives_hide and mouseY > total_h then return end

	PopSetFont(f2, 0)
	local values_w = string_width("77:77")
	local centered_values = values_w // 2
	local objectives_amt = #tbl
	local gap_w = W // 96
	local each_w = box + gap_w*3 + values_w
	local each_w_center = each_w // 2
	local total_w = each_w * objectives_amt
	local x = (GUIW + W) // 2 - (total_w // 2)
	local curr_x = x
	local curr_y = 0
	local str, str_w2
	local blink = framer % 64 < 32

	for k, objective in ipairs(tbl) do
		local curr, max = objective.curr_value(), objective.value
		local curr_int, max_int = objective.curr_value_ptr(), objective.value_integer
		local diff = math.abs(max_int - curr_int)
		local close_to_goal = diff <= (0.2 * max_int) --blinks when 20% close to the objective
		--LOG(curr_int .. " " .. max_int .. " " .. tostring(close_to_goal))
		
		curr_x = x + (k-1)*each_w
		local centered_x = curr_x + each_w_center
		local border = (close_to_goal and blink) and "orange_red" or "gray"
		draw_border(curr_x, 0, each_w, total_h, border, false, true, true, true, false, false, true, true, 	true)
		fill_with_hfx_sprite_clipped(curr_x, 0, each_w, total_h, GUI_COLOR_K, nil)
		
		if not hover then
			if cursor_inside(curr_x, 0, each_w, total_h) then
				PopSetFont(5, 0)
				local tt = objective.tt
				local tt_w = string_width(tt)
				local _ttx = math.min(W - tt_w - 2, mouseX)
				DrawBox(_ttx-1, mouseY + 36-1, tt_w+2, CharHeight2()+2, 1)
				LbDraw_Text(_ttx, mouseY + 36, tt, 0)
			end
		end
		
		curr_x = curr_x + gap_w
		curr_y = curr_y + 2
		LbDraw_ScaledSprite(curr_x, curr_y, get_sprite(0, objective.spr), box, box)
		curr_x = curr_x + box + gap_w + centered_values
		PopSetFont(f1, 0)
		LbDraw_Text(curr_x - string_width(max) // 2, curr_y, max, 0)
		PopSetFont(f2, 0)
		str = curr
		str_w2 = string_width(str) // 2
		curr_x = curr_x - str_w2
		curr_y = curr_y + h1 + 2
		LbDraw_Text(curr_x, curr_y, str, 0)
		curr_y = 2
	end
end

function DrawBoxOutline(x,y,w,h,clr)
	DrawBox(x, y, w, 1, clr)
	DrawBox(x, y, 1, h, clr)
	DrawBox(x, y+h, w, 1, clr)
	DrawBox(x+w, y, 1, h, clr)
end

-- EMOJI WHEEL

players_active_emojis = {{curr = nil, count = 0, cdr = 0},{curr = nil, count = 0, cdr = 0},{curr = nil, count = 0, cdr = 0},{curr = nil, count = 0, cdr = 0},
						 {curr = nil, count = 0, cdr = 0},{curr = nil, count = 0, cdr = 0},{curr = nil, count = 0, cdr = 0},{curr = nil, count = 0, cdr = 0}}
	
_emojis = {
			{ s = 2099, amt = 3, framer = 1, snd="y_move_player.wav", img = 2099 }, --lmao
			{ s = 2102, amt = 18, framer = 1, snd="y_move_player.wav", img = 2104 }, --pepe wave
			{ s = 2084, amt = 15, framer = 1, snd="y_move_player.wav", img = 2084 }, --thumbs down
			{ s = 2183, amt = 1, framer = 1, snd="y_move_player.wav", img = 2183 },  --ashby
			{ s = 2175, amt = 7, framer = 1, snd="y_move_player.wav", img = 2175 }, --rage anger
			{ s = 2120, amt = 25, framer = 1, snd="y_move_player.wav", img = 2128 }, --clown
			{ s = 2182, amt = 1, framer = 1, snd="y_move_player.wav", img = 2182 }, --ez
			{ s = 2145, amt = 30, framer = 1, snd="y_move_player.wav", img = 2166 }, --crying
			{ s = 2036, amt = 48, framer = 1, snd="y_move_player.wav", img = 2042 }, --thumbs up
}

_emojis_order = { 2099,2104,2084,2183,2175,2128,2182,2166,2042 }
_make_fun_emojis = {1, 2, 7, 9}
_disappointed_emojis = {3, 5, 6, 8}

function is_tribe_using_emoji(tribe)
	return players_active_emojis[tribe+1].curr ~= nil
end

function create_emote(pn, id)
	if not gmisc.emojis_enabled then return end
	
	local id = id or rndb(1, #_emojis_order) --still do it before the local return else it desyncs
	
	--if psettings.disable_emojis then return end
	local player_emojis = players_active_emojis[pn+1]
	
	if player_emojis.cdr == 0 then
		if nilS(pn) then
			local emoji = _emojis[id]
			if not emoji then return end
			
			local c3d = getShaman(pn).Pos.D3
			local t = createThing(T_EFFECT, 10, pn, c3d, false, false)
			--EnableFlag(t.Flags3, TF3_LOCAL) --DON'T, desyncs
			local start = emoji.s
			local framer = emoji.framer
			local max = start + emoji.amt - 1
			
			if psettings.disable_emojis then
				start, max = 1770, 1770
			end
			
			set_thing_draw_info(t, TDI_SPRITE_F1_D1, start)
			t.u.Effect.Duration = -1
			t.Pos.D3.Ypos = c3d.Ypos + 256 + 32
			
			--if emoji.snd then
				--queue_custom_sound_event(t, emoji.snd, 127) --currently bugged sound engine in 1.5. it's playing all over the world, not just near the thing (or doesnt play even near thing)
			--end
			
			player_emojis.count = player_emojis.count + 1
			if player_emojis.count >= 2 then player_emojis.cdr = EMOJI_SPAM_COOLDOWN end
			local curr = player_emojis.curr
			if curr then
				local _t = curr.T
				_t.u.Effect.Duration = 1
				set_thing_draw_info(_t, TDI_SPRITE_F1_D1, 1770)
				delete_thing_type_safe(_t)
			end
			
			player_emojis.curr = { T=t, ID=id, FRAMER=framer, MIN=start, MAX=max, CURR=start, FADE=turn+12*4 }
		end
	end
end

function decrease_emoji_antispam_count_and_timer()
	for i = 1, 8 do
		local pl_emj = players_active_emojis[i]
		
		pl_emj.count = math.max(0, pl_emj.count - 1)
		pl_emj.cdr = math.max(0, pl_emj.cdr - 1)
	end
end

function process_emojis(everySecond1)
	for k, v in ipairs(players_active_emojis) do
		local emoji = v.curr
		local owner = k - 1
		
		if emoji ~= nil then
			local thing = emoji.T
			
			if thing then
				if nilS(owner) then
					local c3d = getShaman(owner).Pos.D3
					
					thing.Pos.D3 = c3d
					thing.Pos.D3.Ypos = c3d.Ypos + 128 + 32 + 128
					
					--if everyTurns(emoji.FRAMER) then
						local curr_frame = emoji.CURR
						
						curr_frame = curr_frame + 1
						if curr_frame > emoji.MAX then
							curr_frame = emoji.MIN
						end
						
						emoji.CURR = curr_frame
						set_thing_draw_info(thing, TDI_SPRITE_F1_D1, curr_frame)
					--end
					
					if turn >= emoji.FADE then
						thing.u.Effect.Duration = 1
						set_thing_draw_info(thing, TDI_SPRITE_F1_D1, 1770)
						delete_thing_type_safe(thing)
						v.curr = nil
					end
				else
					thing.u.Effect.Duration = 1
					set_thing_draw_info(thing, TDI_SPRITE_F1_D1, 1770)
					delete_thing_type_safe(thing)
					v.curr = nil
				end
			end
		end
	end
	
	if everySecond1 then
		decrease_emoji_antispam_count_and_timer()
	end
end

function destroy_all_emojis()
	for pn = 1, 8 do
		local pl_emj = players_active_emojis[pn]
		
		if pl_emj.curr then
			local t = pl_emj.curr.T
			
			if t then
				t.u.Effect.Duration = 1
				set_thing_draw_info(t, TDI_SPRITE_F1_D1, 1770)
				delete_thing_type_safe(t)
			end
		end
		
		pl_emj.curr = nil
		pl_emj.count = 0
		pl_emj.cdr = 0
	end
end

function reset_emoji_wheel()
	player.emojis_wheel = { showing=false, hovering = false, cache={} }
	pemojis = player.emojis_wheel
end

function create_emoji_wheel()
	if not gmisc.emojis_enabled then return end
	
	if not pemojis.showing then
		local w,h,guiW = ScreenWidth(), ScreenHeight(), GFGetGuiWidth()
		local mouseX, mouseY = get_mouse_x(), get_mouse_y()
		local radius = mf(w, 8)
		
		if mouseX + radius >= w then return end
		if mouseY + radius >= h then return end
		
		pemojis.showing = true
		pemojis.cache = { x = mouseX, y = mouseY, radius = radius }
	end	
end

function draw_emoji_wheel()
	if not pemojis.showing then return end
	
	local w = W
	local cache = pemojis.cache
	local x, y, radius = cache.x, cache.y, cache.radius
	
	--DrawBox(x, y, radius, radius, 3)
	LbDraw_CircleOutline(x, y, radius, 0)
	LbDraw_CircleOutline(x, y, radius+1, 0)
	LbDraw_CircleOutline(x, y, radius+2, 0)
	
	-- Calculate distance between mouse and circle center
	local dx = mouseX - x
	local dy = mouseY - y
	local distance = math.sqrt(dx * dx + dy * dy)

	-- Check if mouse is outside the circle
	--if distance > radius then
		--return nil  -- Mouse is outside the circle
	--end

	-- Calculate the angle using math.atan
	local angleDeg
	if dx == 0 then
		-- Avoid division by zero; handle vertical lines explicitly
		if dy > 0 then
			angleDeg = 90
		else
			angleDeg = 270
		end
	else
		-- Calculate the angle in radians
		local angleRad = math.atan(dy / dx)
		angleDeg = math.deg(angleRad)  -- Convert to degrees

		-- Adjust angle based on quadrant
		if dx < 0 then
			angleDeg = angleDeg + 180  -- Quadrants 2 and 3
		elseif dy < 0 then
			angleDeg = angleDeg + 360  -- Quadrant 4
		end
	end

	-- Normalize angle to be within [0, 360)
	angleDeg = angleDeg % 360
	
	local section = math.floor(angleDeg / 45) + 1

	-- draw emotes

	local imageOffset = math.floor(radius * 0.8)
	local image_size = mf(w, 28)
	local half_size = mf(image_size, 2)
	local hover = false
	local clr = TbColour.new()
	clr.Index = 0
	local circle_filled_size = mf(image_size, 1.4)
	
	for _section = 1, 8 do
		-- Calculate the angle in radians for each section
		local angle = math.rad((_section - 1) * 45)  -- Start at 0° for section 1, 45° for section 2, etc.

		-- Calculate the x, y position offset from the circle center
		local posX = math.floor(x + imageOffset * math.cos(angle))
		local posY = math.floor(y + imageOffset * math.sin(angle))
		
		-- Adjust for image center
		local centeredPosX = posX - half_size
		local centeredPosY = posY - half_size
		
		LbDraw_SetFlagsOff(LB_DRAW_FLAG_GLASS)
		
		LbDraw_CircleFilled(posX, posY, circle_filled_size, 0)
		
		--lines
		LbDraw_Line(x, y, mouseX, mouseY, clr)
		
		LbDraw_SetFlagsOn(LB_DRAW_FLAG_GLASS)
		
		if not hover then
			if cursor_inside(centeredPosX - half_size, centeredPosY - half_size, image_size*2, image_size*2, mouseX, mouseY) then
				hover = true
				pemojis.hovering = _section
				LbDraw_SetFlagsOff(LB_DRAW_FLAG_GLASS)
			end
		end
		
		LbDraw_ScaledSprite(centeredPosX, centeredPosY, get_sprite(0, _emojis_order[_section]), image_size, image_size)
		
		LbDraw_SetFlagsOn(LB_DRAW_FLAG_GLASS)
	end
	
	if not hover then
		if cursor_inside(x - half_size, y - half_size, image_size*2, image_size*2, mouseX, mouseY) then
			hover = true
			pemojis.hovering = 9
			LbDraw_SetFlagsOff(LB_DRAW_FLAG_GLASS)
		end
	end
	
	LbDraw_CircleFilled(x, y, circle_filled_size, 0)
	
	LbDraw_ScaledSprite(x - half_size, y - half_size, get_sprite(0, _emojis_order[9]), image_size, image_size)
	
	if not hover then pemojis.hovering = false end
end

function correct_emoji_game_state()
	if (state < STATE_READY_SET) or (state > STATE_FINAL_RESULTS) then return false end
	if (state == STATE_FINAL_RESULTS) and gmisc.before_results_timer <= 0 then return false end
	return true
end

function onkeydown_wheel()
	if not correct_emoji_game_state() then return end
	if psettings.disable_emojis then return end
	
	if not pemojis.showing then
		if nilS(playernum) then
			create_emoji_wheel()
		end
	end
end

function onkeyup_wheel()
	if not correct_emoji_game_state() then return end
	
	if pemojis.showing then
		if pemojis.hovering ~= false then
			if not multiplayer then
				create_emote(playernum, pemojis.hovering)
			else
				send_packet_now("a", tostring(pemojis.hovering))
			end
		end
		
		reset_emoji_wheel()
	end
end

function on_turn_AI_emojis()
	if everySeconds(8) then
		for tribe = 0, 7 do
			local ai = player_is_ai(tribe)
			
			if ai then
				if nilS(tribe) then
					if chance(1) then
						create_emote(tribe, rndb(1, 9))
					
						return
					end
				end
			end
		end
	end
end

-- BLOOD / EFFECTS

function create_blood(c3d, id)
	local blood_entries = { 
			{1952, 8}, {1960, 10}, {1970, 8}, {1978, 16}, {2002, 8}, {2010, 8}, {2018, 4}, {2022, 8}, --bloods
			{2030, 6}, --white circular smoke
			{2200, 5}, --dust or stone particles
		}
		
	local curr = blood_entries[id][1]
	local max = curr + blood_entries[id][2]
	local thing = create_thing_cache(T_EFFECT, 10, SAFE_NEUTRAL, c3d)
	if not thing then return end
	set_thing_draw_info(thing, TDI_SPRITE_F1_D1, curr)
	thing.u.Effect.Duration = -1
	
	table.insert(ggameplay.bloods, { THING = thing, C3D = c3d, CURR = curr, MAX = max })
end

function process_bloods()
	local amt = #ggameplay.bloods
	
	for i = amt, 1, -1 do
		local v = ggameplay.bloods[i]
		if v.THING then
			local curr, max = v.CURR, v.MAX
			
			if curr < max then
				v.CURR = curr + 1
				set_thing_draw_info(v.THING, TDI_SPRITE_F1_D1, curr)
			end
			
			if curr == max then
				local ef = v.THING
				ef.u.Effect.Duration = 1
				ef.DrawInfo.Flags = EnableFlag(ef.DrawInfo.Flags, DF_THING_NO_DRAW)
				delete_thing_type_safe(ef)
				table.remove(ggameplay.bloods, i)
			end
		end
	end
end

function destroy_bloods()
	for _, blood in ipairs(ggameplay.bloods) do
		delete_thing_type_safe(blood.THING)
	end
	ggameplay.bloods = {}
end

-- GENERAL DRAWING

function clip_region(x, y, w, h)
	local r = TbRect.new()
	r.Left = x
	r.Right = x + w
	r.Top = y
	r.Bottom = y + h
	LbDraw_SetClipRect(r)
end

function stop_clipping()
	LbDraw_ReleaseClipRect()
end

sprites_real_size = {
								--fill
								[1731] = {x = 32, y = 32},
								[1732] = {x = 32, y = 32},
								[1733] = {x = 32, y = 32},
								[1734] = {x = 32, y = 32},
								[1735] = {x = 32, y = 32},
								[1736] = {x = 32, y = 32},
								[1737] = {x = 32, y = 32},
								[1738] = {x = 32, y = 32},
								[1739] = {x = 32, y = 32},
								[1740] = {x = 32, y = 32},
								[1741] = {x = 32, y = 32},
								
								[491] = {x = 8, y = 8},
								
								[1762] = {x = 32, y = 32}, --darker color cards
								[1763] = {x = 32, y = 32},
								[1764] = {x = 32, y = 32},
								[1765] = {x = 32, y = 32},
								[1766] = {x = 32, y = 32},
								[1767] = {x = 32, y = 32},
								[1768] = {x = 32, y = 32},
								[1769] = {x = 32, y = 32},
								
								[1793] = {x = 32, y = 32}, --hovering color cards
								[1794] = {x = 32, y = 32},
								[1795] = {x = 32, y = 32},
								[1796] = {x = 32, y = 32},
								[1797] = {x = 32, y = 32},
								[1798] = {x = 32, y = 32},
								[1799] = {x = 32, y = 32},
								[1800] = {x = 32, y = 32},
								[1825] = {x = 32, y = 32}, --locked
								
								[2567] = {x = 32, y = 32},
								[2568] = {x = 32, y = 32},
								
								--borders
								["yellow"] = { w = 8, h = 8, border = {843, 862, 845, 864}, corners = {812, 813, 823, 806} },
								["gray"] = { w = 4, h = 4, border = {101, 102, 103, 104}, corners = {97, 98, 99, 100} },
								["orange_shadow"] = { w = 4, h = 4, border = {156, 160, 162, 158}, corners = {155, 157, 161, 159} },
								["creme"] = { w = 8, h = 8, border = {514, 515, 516, 517}, corners = {510, 511, 512, 513} },
								["white"] = { w = 8, h = 8, border = {523, 524, 525, 526}, corners = {519, 520, 521, 522} },
								["white2"] = { w = 8, h = 8, border = {532, 533, 534, 535}, corners = {528, 529, 530, 531} },
								["gold_brown"] = { w = 8, h = 8, border = {561, 562, 563, 564}, corners = {557, 558, 559, 560} },
								["thin_brown"] = { w = 8, h = 8, border = {569, 570, 571, 572}, corners = {565, 566, 567, 568} },
								["thin_brown_light"] = { w = 8, h = 8, border = {577, 578, 579, 580}, corners = {573, 574, 575, 576} },
								["thin_brown_red"] = { w = 8, h = 8, border = {585, 586, 587, 588}, corners = {581, 582, 583, 584} },
								
								["orange_red"] = { w = 4, h = 4, border = {2485, 2486, 2487, 2488}, corners = {2489, 2490, 2491, 2492} },
}

function fill_with_hfx_sprite_clipped(x, y, w, h, spr, flag)
	clip_region(x, y, w, h)
	if flag then LbDraw_SetFlagsOn(flag) end
	
	local sprite = get_sprite(0, spr)
	local spr_size = sprites_real_size[spr]
	local spr_w, spr_h = spr_size.x, spr_size.y
	local amtX, amtY = math.ceil(w / spr_w), math.ceil(h / spr_h)
	local currX, currY = x, y
	
	for j = 1, amtY do
		local currX = x
		for i = 1, amtX do
			LbDraw_ScaledSprite(currX, currY, sprite, spr_w, spr_h)
			currX = currX + spr_w
		end
		currY = currY + spr_h
	end
	
	if flag then LbDraw_SetFlagsOff(flag) end
	stop_clipping()
end

function draw_border(x, y, w, h, spr, TOP, BOT, LEFT, RIGHT, CORNER_A, CORNER_B, CORNER_C, CORNER_D, halfSize)
	local tbl = sprites_real_size[spr]
	local border_tbl = tbl.border
	local sprite_top, sprite_bot = get_sprite(0, border_tbl[1]), get_sprite(0, border_tbl[2])
	local sprite_left, sprite_right = get_sprite(0, border_tbl[3]), get_sprite(0, border_tbl[4])
	local spr_size, spr_size_2 = tbl.w, tbl.h
	local each_w, each_h = tbl.w, tbl.h
	
	if halfSize then
		spr_size, spr_size_2 = math.max(mf(spr_size, 2), 2), math.max(mf(spr_size_2, 2), 2)
		each_w, each_h = math.max(mf(each_w, 2), 2), math.max(mf(each_h, 2), 2)
	end
	
	local thickness = math.min(each_w, each_h)
	local each_w_2, each_h_2 = spr_size, spr_size_2
	local amtX, amtY = math.max(mf(w, each_w), 1), math.max(mf(h, each_h), 1)
	local amtX_2, amtY_2 = math.max(mf(w, each_w_2), 1), math.max(mf(h, each_h_2), 1)
	local restX, restY = w - (each_w * amtX), h - (each_h * amtY)
	local restX_2, restY_2 = w - (each_w_2 * amtX_2), h - (each_h_2 * amtY_2)
	
	if TOP or BOT then
		local _y = y - each_h
		
		if TOP then
			for i = 0, amtX - 1 do
				local width = each_w + (restX)*btn(i == amtX-1)
				local _x = x + i*each_w
				
				LbDraw_ScaledSprite(_x, _y, sprite_top, width, each_h)
			end
		end
		
		if BOT then
			_y = _y + each_h + h
		
			for i = 0, amtX - 1 do
				local width = each_w + (restX)*btn(i == amtX-1)
				local _x = x + i*each_w
				
				LbDraw_ScaledSprite(_x, _y, sprite_bot, width, each_h)
			end
		end
	end

	if LEFT or RIGHT then
		local _x = x - each_w_2
		
		if LEFT then
			for i = 0, amtY_2 - 1 do
				local height = each_h_2 + (restY_2)*btn(i == amtY_2-1)
				local _y = y + i*each_h_2
				
				LbDraw_ScaledSprite(_x, _y, sprite_left, each_w_2, height)
			end
		end
		
		if RIGHT then
			_x = _x + each_w_2 + w
		
			for i = 0, amtY_2 - 1 do
				local height = each_h_2 + (restY_2)*btn(i == amtY_2-1)
				local _y = y + i*each_h_2
				
				LbDraw_ScaledSprite(_x, _y, sprite_right, each_w_2, height)
			end
		end
	end
	
	local corner_tbl = sprites_real_size[spr].corners
	
	if CORNER_A then LbDraw_ScaledSprite(x - thickness, y - thickness, get_sprite(0, corner_tbl[1]), thickness, thickness) end
	if CORNER_B then LbDraw_ScaledSprite(x + w, y - thickness, get_sprite(0, corner_tbl[2]), thickness, thickness) end
	if CORNER_C then LbDraw_ScaledSprite(x - thickness, y + h, get_sprite(0, corner_tbl[3]), thickness, thickness) end
	if CORNER_D then LbDraw_ScaledSprite(x + w, y + h, get_sprite(0, corner_tbl[4]), thickness, thickness) end
end

function draw_map_rules(str, x, y, w, h)
	local parag_o = 4
	local limit_w = x + w
	local currX = x + parag_o
	local currY = y
	local font_h = CharHeight2()
	local maxY = y -- track bottom-most Y reached

	for word in string.gmatch(str, "%S+") do
		local first_char = word:sub(1, 1)
		local _w = word
		
		if first_char == "<" then
			if word == "<br>" then
				currX = x + parag_o
				currY = currY + font_h
			elseif word == "<p>" then
				currX = x + parag_o
				currY = currY + font_h*2
			end
			
			_w = ""
		else
			_w = _w .. " "
		end
		
		local w_width = string_width(_w)
		
		if currX + w_width > limit_w then
			currX = x
			currY = currY + font_h
		end
		
		LbDraw_Text(currX, currY, _w, 0)
		currX = currX + w_width
		
		if currY > maxY then
            maxY = currY
        end
	end
	
	local content_height = (maxY - y) + font_h
    return content_height
end

function fit_text_in_line(text, maxW)
    if string_width(text) <= maxW then return text end

    local curr_w = 0
    local last_i = 0

    for i = 1, #text do
        local next_char_w = string_width(text:sub(i, i))
        if curr_w + next_char_w > maxW then
            break
        end
        curr_w = curr_w + next_char_w
        last_i = i
    end

    while last_i > 0 and curr_w + string_width("...") > maxW do
        curr_w = curr_w - string_width(text:sub(last_i, last_i))
        last_i = last_i - 1
    end

    if last_i == 0 then
        return string_width("...") <= maxW and "..." or ""
    end

    return text:sub(1, last_i) .. "..."
end

function fit_text_in_line_no_ellipsis(text, maxW)
    if string_width(text) <= maxW then
        return text
    end

    local curr_w = 0
    local last_i = 0

    for i = 1, #text do
        local next_char_w = string_width(text:sub(i, i))
        if curr_w + next_char_w > maxW then
            break
        end
        curr_w = curr_w + next_char_w
        last_i = i
    end

    return text:sub(1, last_i)
end

function shaman_framer()
	if not (framer % 6 == 0) then return end

	local curr = pmisc.shaman_frame
	if curr < 3 then
		pmisc.shaman_frame = curr + 1
	else
		pmisc.shaman_frame = 0
	end
end

local map_desc_fonts = { 4, 5, 8, 3 }
function change_map_rules_font(left)
	local idx = pmisc.rules_font_idx
	if left then
		idx = idx + 1
		if idx > #map_desc_fonts then idx = 1 end
	else
		idx = idx - 1
		if idx < 1 then idx = #map_desc_fonts end
	end
	
	pmisc.rules_font_idx = idx
	pmisc.rules_font = map_desc_fonts[idx]
	reset_map_rules_offset()
end

function DrawPercentBar(x, y, w, h, percent, colorBar, colorFill, border, outline)
	local barpercent = math.min(w, math.floor((w * percent) / 100))
	
	if outline then
		DrawBox(x-1, y-1, w+2, h+2, type(outline) == "number" and outline or 1)
	end
	
	DrawBox(x, y, w, h, colorBar)
	DrawBox(x, y, barpercent, h, colorFill)
	
	if border ~= false then
		draw_border(x, y, w, h, border, true, true, true, true, true, true, true, true, 	true)
	end
end

function play_sprite(x, y, w, h, start, frames, speed)
	local spr = start + ((floor(framer / speed)) % frames)
	LbDraw_ScaledSprite(x, y, get_sprite(1, spr), w, h)
end

function type_text_in_area(text, x, y, max_desc_w, font_h, write_it)
	local line = 1
	local currX = x
	local max_x = currX
	local limitX = x + max_desc_w
	local currY = y
	local fontH = font_h
	
	for word in string.gmatch(text, "%S+") do
		local _w = word .. " "
		local w_width = string_width(_w)
		
		if currX + w_width > limitX then
			currX = x
			currY = currY + fontH
			line = line + 1
		end
		
		if write_it then
			LbDraw_Text(currX, currY, _w, 0)
		end
		
		currX = currX + w_width
		max_x = math.max(max_x, currX)
	end
	
	if not write_it then
		return line * fontH, max_x
	end
end

-- ======================================================================================================================================== --
-- ========================================================== FORGE ======================================================================= --
-- ======================================================================================================================================== --

forging_messages = {
						{"look at the land go...", "forging this amazing map...", "building bridges...", "generating land at the worst possible location...", "forging land...", "this is a 3d printer :)"},
						{"slowly placing objects...", "slowly, so it doesn't crash...", "placing objects at the worst possible location...", "pray this doesn't throw an error...", "grab a drink meanwhile..."},
						{"almost there...", "finishing touches...", "generating the last objects...", "hang in there...", "aaaand...", "are you ready?", "okaaaaay let's go!"}
}

function draw_forge_progress()
	PopSetFont(3, 0)
	local str = "- forging the world -"
	local box_w, box_h = W // 4, H // 64
	local half_box_w = box_w // 2
	local box_x, box_y = (W + GUIW) // 2 - half_box_w, H - (box_h * 2)
	local str_x = box_x + half_box_w - string_width(str) // 2
	local percent = gforge.percent

	DrawPercentBar(box_x, box_y, box_w, box_h, percent, 1, 5, "orange_shadow")
	LbDraw_Text(str_x, box_y - 4 - CharHeight2() - 4 - 12, str, 0)
	local spr_size = floor(box_h*1.5)
	play_sprite(box_x - spr_size - 1, box_y - (spr_size - box_h), spr_size, spr_size, 438, 8, 8)

	PopSetFont(4, 0)
	str = gforge.forging_str
	str_x = box_x + half_box_w - string_width(str) // 2
	LbDraw_Text(str_x, box_y - 4 - 12, str, 0)

	PopSetFont(1, 0)
	str = percent .. "%"
	str_x = box_x + half_box_w - string_width(str) // 2
	LbDraw_Text(str_x, box_y + box_h // 2 - CharHeight2() // 2, str, 0)
	
	local bs = W // 50
	local x, y = GUIW + 4, H - 4 - bs
	local o = 0
	if cursor_inside(x, y, bs, bs) then
		pmisc.follow_forge = true
		o = 2
	else
		pmisc.follow_forge = false
	end
	
	DrawBox(x, y, bs, bs, 0)
	DrawBox(x+1, y+1, bs-2, bs-2, 1)
	LbDraw_ScaledSprite(x+1 + o, y+1 + o, get_sprite(0, 553), bs-2 - o*2, bs-2 - o*2)
end

function update_forging_string()
	local curr_timer = gforge.forging_str_timer
	gforge.forging_str_timer = curr_timer + 1
	
	if curr_timer + 1 > 12*3 then
		gforge.forging_str_timer = 0
		local mode = gforge.enum
		local idx = mode >= FORGING_DECORATIONS and 3 or mode
		local tbl = forging_messages[idx]
		gforge.forging_str = tbl[rndb(1, #tbl)]
	end
end

function init_forging()
	ggameplay.mg_obj_list = {}
	for thing_type = 1, NUM_THING_TYPES do
		ggameplay.mg_obj_list[thing_type] = {}
	end

	gforge.init = true
	gforge.ptr = map_list[ghub.selected_map]
	gforge.total_things = 0
	gforge.amounts = {}
	gforge.remaining = 0
	gforge.percent = 0
	gforge.idx = 0
	gforge.enum = FORGING_LAND
	local fstr_tbl = forging_messages[FORGING_LAND]
	gforge.forging_str = fstr_tbl[rndb(1, #fstr_tbl)]
	gforge.forging_str_timer = 0
	f_count_things_amt()
end

function process_forging()
	local remaining = gforge.remaining - 1
	gforge.remaining = remaining
	--gforge.percent = floor((gforge.remaining / gforge.total_things) * 100) --countdown
	gforge.percent = math.min(100, 100 - floor((gforge.remaining / gforge.total_things) * 100)) --countup
	forge_methods[gforge.enum]()
	update_forging_string()
end

function f_finish()
	f_shake_land_bug()
	f_prepare_mg_tables()
	f_spawn_shamans()
	show_panel()
	gmisc.ready_set_timer = 12 * READY_SET_COUNTDOWN
	set_state(STATE_READY_SET)
end

function f_shake_land_bug()
	--this updates land and makes unit be able to walk
	local location = ghub.map_info_ptr.marker_lb
	local pos = coord_to_c3d(location[1], location[2])
	
	--createThing(T_VEHICLE, M_VEHICLE_AIRSHIP_1, SAFE_NEUTRAL, pos, false, false)
	createThing(T_EFFECT, M_EFFECT_RAISE_LAND, SAFE_NEUTRAL, pos, false, false)
	createThing(T_EFFECT, M_EFFECT_EROSION, SAFE_NEUTRAL, pos, false, false)
end

function fill_allies_enemies_tbl(tribe, team)
	local tbl = {}
	local allies, enemies = {}, {} --should allies include self?
	
	for k, gui_team in ipairs(teams_ptrs) do
		if k == 1 then
			for _, player in ipairs(gui_team.players) do
				local is_ally = (player == tribe)
				tbl[player] = is_ally
				table.insert(is_ally and allies or enemies, player)
			end
		else
			local is_ally = (team == gui_team)
			for _, player in ipairs(gui_team.players) do
				tbl[player] = is_ally
				table.insert(is_ally and allies or enemies, player)
			end
		end
	end
	
	return tbl, allies, enemies
end

function f_prepare_mg_tables()
	ggameplay.cached_curr_values_strings = {"00:00", "0", "0"}
	local map_info = ghub.map_info_ptr
	local reinc_timer = map_info.reinc_timer or 12*3
	ggameplay.map_respawn_timer = reinc_timer
	set_everyone_enemy()
	mg_turn = 0
	ggameplay.mg_effects_list = {}
	ggameplay.mg_created_land_locations = {}
	ggameplay.mg_unwalkable_zones = {}
	ggameplay.active_powerups = {}
	ggameplay.highest_score_decimals = 1
	ggameplay.clear_drop_infos = not map_info.drop_infos
	ggameplay.participants_by_teams = {}
	ggameplay.participants = {}
	ggameplay.AI_participants = {}
	reset_mg_player_vars()
	ggameplay.mg_vars = {}
	
	for k, gui_team in ipairs(teams_ptrs) do
		local solo = (k == 1)
		
		if solo then
			for _, player in ipairs(gui_team.players) do
				local ptbl = gplayers[player+1]
				if not ptbl.spectator then
					local color = ptbl.color
					local name = ptbl.full_name
					local is_ai = (ptbl.mode == TRIBE_AI)
					local participant_by_team = {}
					table.insert(ggameplay.participants_by_teams, participant_by_team)
					participant_by_team.uid = player + 1
					participant_by_team.name = name
					participant_by_team.points = 0
					participant_by_team.color = color
					participant_by_team.is_team = false
					participant_by_team.global_table = gplayers[player+1]
					participant_by_team.active_playing = true
					participant_by_team.players = { {tribe = player, color = color, name = name, ai_diff = is_ai and ptbl.difficulty or nil} }
					local participant = {}
					participant.active = true
					participant.tribe = player
					participant.ai = is_ai
					participant.diff = ptbl.difficulty
					participant.name = name
					participant.color = color
					participant.g_color = ptbl.g_color
					participant.reinc = reinc_timer
					participant.can_reinc = true
					participant.dead_lock = true
					participant.ldb = SAFE_NEUTRAL
					participant.ldb_cdr = -1
					participant.ally_or_enemy, participant.ally_list, participant.enemy_list = fill_allies_enemies_tbl(player, gui_team)
					participant.team_ptr = participant_by_team
					table.insert(ggameplay.participants, participant)
					ggameplay.tribes_to_participants_ptrs[player] = participant
					
					if is_ai then
						table.insert(ggameplay.AI_participants, { ptr = participant})
					end
				end
			end
		else
			if #gui_team.players > 0 then
				local participant_by_team = {}
				table.insert(ggameplay.participants_by_teams, participant_by_team)
				participant_by_team.uid = k + 7
				participant_by_team.name = gui_team.default_name
				participant_by_team.points = 0
				participant_by_team.players = {}
				participant_by_team.is_team = true
				participant_by_team.global_table = gui_team
				participant_by_team.active_playing = true
				
				for _, player in ipairs(gui_team.players) do
					local ptbl = gplayers[player+1]
					local color = ptbl.color
					local is_ai = (ptbl.mode == TRIBE_AI)
					local participant = {}
					participant.active = true
					participant.tribe = player
					participant.ai = is_ai
					participant.diff = ptbl.difficulty
					participant.name = ptbl.full_name
					table.insert(participant_by_team.players, {tribe = player, color = color, name = ptbl.full_name, ai_diff = is_ai and ptbl.difficulty or nil})
					participant.color = color
					participant.g_color = ptbl.g_color
					participant.reinc = reinc_timer
					participant.can_reinc = true
					participant.dead_lock = true
					participant.ldb = SAFE_NEUTRAL
					participant.ldb_cdr = -1
					participant.ally_or_enemy, participant.ally_list, participant.enemy_list = fill_allies_enemies_tbl(player, gui_team)
					participant.team_ptr = participant_by_team
					table.insert(ggameplay.participants, participant)
					ggameplay.tribes_to_participants_ptrs[player] = participant
					
					for _, player2 in ipairs(gui_team.players) do
						if player ~= player2 then
							set_players_allied(player, player2)
						end
					end
					
					if is_ai then
						table.insert(ggameplay.AI_participants, { ptr = participant})
					end
				end
			end
		end
	end
	
	_participants = ggameplay.participants
	_participants_by_teams = ggameplay.participants_by_teams
	_ai_participants = ggameplay.AI_participants
	_tribes_to_participants_ptrs = ggameplay.tribes_to_participants_ptrs
	active_powerups = ggameplay.active_powerups
	mg_vars = ggameplay.mg_vars
	update_pointers()
end

function f_spawn_shamans()
	local tbl = ghub.map_info_ptr
	local ztbl = tbl.zoom
	local stbl = map_list[ghub.selected_map].shaman_positions
	
	if #ztbl > 0 then
		local x, z, angle = ztbl[1] or 0, ztbl[2] or 0, ztbl[3] or 0
		set_myplayer_camera_new_postion(coord_to_c2d(x, z), angle)
	end
	
	if #stbl > 0 then
		for k, player in ipairs(_participants) do
			local tribe = player.tribe
			spawn_shaman_at_map_position(tribe, false, tbl.shield_respawn, true)
		end
	end
	
	tmi[TMI_PERSON_MEDICINE_MAN].BaseSpeed = 0
end

function spawn_shaman_at_map_position(pn, reinc_sound, shield, first_spawn, custom_shield_timer)
	local map = ghub.selected_map
	local positions = map_list[map].shaman_positions
	if not positions then return end
	
	local idx = pn*2 + 1
	local c3d = (mg_vars and (mg_vars.custom_reinc_location and mg_vars.custom_reinc_location(pn))) or (positions and coord_to_c3d(positions[idx], positions[idx+1]))
	if not c3d then return end
	
	local shaman = createThing(T_PERSON, M_PERSON_MEDICINE_MAN, pn, c3d, false, false)
	
	if reinc_sound then
		queue_sound_event(shaman, SND_EVENT_SHAMAN_RETURN, 0)
	end

	if shield then
		local duration = custom_shield_timer or 4
		giveShield(shaman, duration)
	end
	
	if pn == playernum then
		if not first_spawn and psettings.zoom_on_respawn then
			local angle = 0
			local optional_angle = ghub.map_info_ptr.optional_shaman_zoom_angle
			if optional_angle then
				angle = type(optional_angle) == "function" and optional_angle(pn) or optional_angle
			end
			ZoomThing(shaman, angle)
			if not multiplayer then
				try_to_select_thing(shaman, pn)
			end
		end
	end
	
	if first_spawn then
		shaman.Pos.D3.Ypos = math.min(3000, shaman.Pos.D3.Ypos + 2045)
		--set_unit_flag(s, TF_STATE_LOCKED, 1, true)
	end
	
	shaman.Flags = EnableFlag(shaman.Flags, TF_RESET_STATE)
end

function f_count_things_amt()
	local map_ptr = gforge.ptr
	local land_count = 0
	local obj_count = 0
	local decorations_count = not map_ptr.decorations and 1 or #map_ptr.decorations // 4
	local markers_count = 1--btn(#map_ptr.markers > 0)
	local tbl = map_ptr.land
	for i = 0, 255 do
		if tbl[i] then
			land_count = land_count + 1
		end
	end
	tbl = map_ptr.objects
	if tbl then
		for i = 0, 255 do
			if tbl[i] then
				obj_count = obj_count + 1
			end
		end
	end
	land_count = math.max(land_count, 1)
	obj_count = math.max(obj_count, 1)

	gforge.amounts = {land_count, obj_count, decorations_count, markers_count}
	local total_amount = land_count + obj_count + decorations_count + markers_count
	-- if things would take greater than 72 (6 seconds) to forge, double the speed
	local speed = total_amount <= 72 and FORGE_LAND_OBJ_PLACEMENT_SPEED_SLOW or FORGE_LAND_OBJ_PLACEMENT_SPEED_FAST

	gforge.forging_speed = speed
	gforge.total_things = math.ceil(land_count / speed) + math.ceil(obj_count / speed) + math.ceil(decorations_count / speed) + markers_count
	
	if gforge.total_things == 0 then
		f_finish()
	else
		gforge.remaining = gforge.total_things
	end
end

forge_methods = {
	[FORGING_LAND] = function() 			f_forge_land() end,
	[FORGING_OBJECTS] = function() 			f_forge_objects() end,
	[FORGING_DECORATIONS] = function() 		f_forge_decorations() end,
	[FORGING_MARKERS] = function() 			f_forge_markers() end,
}

function set_forging_stage(state, idx_reset_value)
	gforge.enum = state
	gforge.idx = idx_reset_value
end

function f_forge_markers()
	local markers_tbl = _gnsi.ThisLevelHeader.Markers
	local default_pos = coord_to_map_idx(0, 0)
	
	for i = 0, 255 do
		markers_tbl[i] = default_pos
	end

	if gforge.amounts[FORGING_MARKERS] ~= 0 then
		local tbl = gforge.ptr.markers
		local slot = 0
		
        for i = 1, #tbl, 2 do
            local x, z = tbl[i], tbl[i+1]
            markers_tbl[slot] = coord_to_map_idx(x, z)
			--local ef = createThing(T_EFFECT, M_EFFECT_FIREBALL, SAFE_NEUTRAL, coord_to_c3d(x, z), false, false) ef.u.Effect.Duration = -1
            slot = slot + 1
        end
    end
	
	f_finish()
end

function f_forge_decorations()
    local tbl = gforge.ptr.decorations
	if not tbl or #tbl == 0 then set_forging_stage(FORGING_MARKERS, 0) return end
    local idx = gforge.idx
    local limit = 0
    local last
	local zoom

    local s = idx
    while s <= #tbl do
        local x = tbl[s]
        local z = tbl[s+1]
		local c3d = coord_to_c3d(x, z)
		local island_stuff = _decorations[tbl[s+2]]
		local M, L = island_stuff[1], island_stuff[2]
		local angle = tbl[s+3] * 256
		zoom = {x,z}
        
		local t = create_thing_cache(T_SCENERY, M, SAFE_NEUTRAL, c3d)
		set_thing_draw_info(t, TDI_OBJECT_GENERIC, L)
		t.AngleXZ = angle

        last = s + 4

        limit = limit + 1
        if limit >= gforge.forging_speed then
            break
        end
    end
	
	if zoom and pmisc.follow_forge then
		ZOOM_TO(zoom[1], zoom[2], 0)
	end

    gforge.idx = last
    if gforge.idx > #tbl then
		set_forging_stage(FORGING_MARKERS, 0)
		return
    end
end

function f_forge_objects()
	local idx = gforge.idx
	local tbl = gforge.ptr.objects
	if not tbl then set_forging_stage(FORGING_DECORATIONS, 1) return end
	local limit = 0
	local speed = gforge.forging_speed
	local last
	local zoom
	
	for j = idx, 255 do
		local x = j
		local subtbl = tbl[x]

		if not subtbl then
			if j < 255 then
				goto next_iteration
			else
				set_forging_stage(FORGING_DECORATIONS, 1)
				return
			end
		end

		local s = 1
		for _ = 1, #subtbl / 5 do
			local z = subtbl[s]
			local _type = subtbl[s+1]
			local _model = subtbl[s+2]
			local _owner = subtbl[s+3]
			local _angle = subtbl[s+4]
			local c3d = coord_to_c3d(x, z)
			zoom = {x,z}
			
			local t = create_thing_cache(_type, _model, _owner, c3d)
			t.AngleXZ = _angle
			s = s + 5
		end
		
		last = j
		
		limit = limit + 1
		if limit >= speed then break end
		::next_iteration::
	end
	
	if zoom and pmisc.follow_forge then
		ZOOM_TO(zoom[1], zoom[2], 0)
	end
	
	gforge.idx = last + speed
	if gforge.idx > 255 then
		set_forging_stage(FORGING_DECORATIONS, 1)
		return
	end
end

function f_forge_land()
	local idx = gforge.idx
	local tbl = gforge.ptr.land
	if not tbl then set_forging_stage(FORGING_OBJECTS, 0) return end
	local limit = 1
	local speed = gforge.forging_speed
	local last
	local zoom
	
	for j = idx, 255 do
		local x = j
		local subtbl = tbl[x]

		if not subtbl then
			if j < 255 then
				goto next_iteration
			else
				set_forging_stage(FORGING_OBJECTS, 0)
				return
			end
		end

		local s = 1
		for _ = 1, #subtbl / 2 do
			local z = subtbl[s]
			local y = subtbl[s+1]
			make_land_on_square(x, z, y)
			zoom = {x,z}
			s = s + 2
			
			--on first tick, zoom to start of land creation
			if gforge.remaining == gforge.total_things - 1 then
				ZOOM_TO(x, z, 0)
			end
		end
		
		last = j
		
		limit = limit + 1
		if limit >= speed then break end
		::next_iteration::
	end
	
	if zoom and pmisc.follow_forge then
		ZOOM_TO(zoom[1], zoom[2], 0)
	end
	
	gforge.idx = last + speed
	if gforge.idx > 255 then
		set_forging_stage(FORGING_OBJECTS, 0)
		return
	end
end

function make_land_on_square(x, z, y)
	local m = MapPosXZ.new()
	m.XZ.X = x
	m.XZ.Z = z
	
	SearchMapCells(SQUARE, 0, 0, 0, m.Pos, function(me)
		me.Alt = y
	return true end)
	
	set_square_map_params(m.Pos, 1 + 1, TRUE)
end

function process_deforging()
	if not gforge.unforging then return end

	local curr = gforge.unforging_enum
	
	if curr == UNFORGING_OBJECTS then
		local found = false
		
		for thing_type = 1, NUM_THING_TYPES do
			local tbl = ggameplay.mg_obj_list[thing_type]
			
			if tbl then
				local remaining = #tbl
				
				if remaining > 0 then
					found = true
					local max_to_delete = math.min(16, remaining)
					
					for i = max_to_delete, 1, -1 do
						local obj = tbl[i]
						if obj then
							if obj.Model == M_SCENERY_HEAD then
								--if you delete, it probably it doesnt seem to clear the mapcell flags
								obj.State = S_SCENERY_GROUNDED
							else
								delete_thing_type(obj)
							end
						end
						table.remove(tbl, i)
					end
					break
				end
			end
		end
		
		if not found then
			kill_all_shamans()
			for t_type = T_PERSON, T_EFFECT do
				ProcessGlobalTypeList(t_type, function(t)
					delete_thing_type_safe(t)
				return true end)
			end
			gforge.unforging_enum = UNFORGING_LAND
			gforge.idx = 0
			gforge.ptr = map_list[ghub.previous_map]
		end
	elseif curr == UNFORGING_LAND then
		local idx = gforge.idx
		local tbl = gforge.ptr.land
		if not tbl then end_deforging() return end
		local limit = 1
		local speed = gforge.forging_speed
		local last
		
		for j = idx, 255 do
			local x = j
			local subtbl = tbl[x]

			if not subtbl then
				if j < 255 then
					goto next_iteration
				else
					end_deforging()
					return
				end
			end

			local s = 1
			for _ = 1, #subtbl / 2 do
				local z = subtbl[s]
				local y = subtbl[s+1]
				--make_land_on_square(x, z, y)
				local m = MapPosXZ.new()
				m.XZ.X = x
				m.XZ.Z = z
				sink_land_idx_rad(m.Pos, 1)
				s = s + 2
			end
			
			last = j
			
			limit = limit + 1
			if limit >= speed then break end
			::next_iteration::
		end
		
		gforge.idx = last + speed
		if gforge.idx > 255 then
			end_deforging()
			return
		end
	end
end

function init_deforging()
	gforge.unforging = true
	gforge.unforging_enum = UNFORGING_OBJECTS
	gforge.init = false
end

function end_deforging()
	sink_land_created_inside_mg() --the 5 start game countdown seconds will be used to gracefully end this effects
	gforge.unforging = false
	
	if gforge.unforging_done_notificate then
		gforge.unforging_done_notificate = false
		add_msg_to_hub_log(51, HUB_LOG_SYSTEM_MSG_PTR)
		queue_custom_sound_event(nil, "y_unforge_ready.wav", 127)
	end
	--LOG("END deforging")
end

-- ======================================================================================================================================== --
-- ========================================================== SOUND ======================================================================= --
-- ======================================================================================================================================== --

sound_event_triggers = {
							[SND_EVENT_INFO_DROP] = function(t_thing, flags) return TRUE end,
							[SND_EVENT_INFO_BOUNCE] = function(t_thing, flags) return TRUE end,
							
							[SND_EVENT_SHAM_VOLCANO] = function(t_thing, flags) local map = ghub.selected_map if map == MAP_BOAT_RACE_I or map == MAP_BOAT_RACE_II or map == MAP_BOAT_RACE_III then return TRUE else return FALSE end end,
							[SND_EVENT_ESHAM_VOLCANO] = function(t_thing, flags) local map = ghub.selected_map if map == MAP_BOAT_RACE_I or map == MAP_BOAT_RACE_II or map == MAP_BOAT_RACE_III then return TRUE else return FALSE end end,
							[M_SPELL_BLAST] = function(t_thing, flags) local map = ghub.selected_map if map == MAP_NAVAL_WARFARE then return TRUE else return FALSE end end, 
							[SND_EVENT_SHOT_FIREBALL] = function(t_thing, flags) if ghub.selected_map == MAP_NAVAL_WARFARE then return TRUE else return FALSE end end,
							[SND_EVENT_SHAM_INSECT] = function(t_thing, flags) local map = ghub.selected_map if map == MAP_NAVAL_WARFARE or map == MAP_SUN_AND_MOON or map == MAP_CORROSION then return TRUE else return FALSE end end,
							[SND_EVENT_ESHAM_INSECT] = function(t_thing, flags) local map = ghub.selected_map if map == MAP_NAVAL_WARFARE or map == MAP_SUN_AND_MOON or map == MAP_CORROSION then return TRUE else return FALSE end end,
							[SND_EVENT_SHAM_CONVERT] = function(t_thing, flags) local map = ghub.selected_map if map == MAP_NAVAL_WARFARE or map == MAP_TECTONIC or map == MAP_JUMPERS or map == MAP_PAINTBALL then return TRUE else return FALSE end end,
							[SND_EVENT_ESHAM_CONVERT] = function(t_thing, flags) local map = ghub.selected_map if map == MAP_NAVAL_WARFARE or map == MAP_TECTONIC or map == MAP_JUMPERS or map == MAP_PAINTBALL then return TRUE else return FALSE end end,
							[SND_EVENT_SHAM_SHIELD] = function(t_thing, flags) local map = ghub.selected_map if map == MAP_NAVAL_WARFARE or map == MAP_TECTONIC then return TRUE else return FALSE end end,
							[SND_EVENT_ESHAM_SHIELD] = function(t_thing, flags) local map = ghub.selected_map if map == MAP_NAVAL_WARFARE or map == MAP_TECTONIC then return TRUE else return FALSE end end,
							[SND_EVENT_SHAM_LGHTNG] = function(t_thing, flags) local map = ghub.selected_map if map == MAP_PAINTBALL then return TRUE else return FALSE end end,
							[SND_EVENT_ESHAM_LGHTNG] = function(t_thing, flags) local map = ghub.selected_map if map == MAP_PAINTBALL then return TRUE else return FALSE end end,
							[SND_EVENT_SHAMAN_RETURN] = function(t_thing, flags) local map = ghub.selected_map if map == MAP_ICE_ARENA then return TRUE else return FALSE end end,
							--[SND_EVENT_SHAMDIE_SWIRL] = function(t_thing, flags) local map = ghub.selected_map if map == MAP_ICE_ARENA then return TRUE else return FALSE end end,
							[SND_EVENT_SHAMKILL_SWIRL] = function(t_thing, flags) local map = ghub.selected_map if map == MAP_ICE_ARENA then return TRUE else return FALSE end end,
}

hub_sounds = { "yyy_s_start", "yyy_s_leroy", "yyy_s_wololo", "yyy_s_spin", "yyy_s_unfair", "yyy_s_ok", "yyy_s_no", "yyy_s_yes", "yyy_s_welcome" }

function mute_sounds(clicker)
	local muted = pmisc.sounds_muted
	--add_msg_to_hub_log(36 + btn(muted), clicker, nil) --it's now local xD
	
	pmisc.sounds_muted = not muted
	queue_custom_sound_event(nil, "y_swap_soundboard.wav", 127)
end

function can_press_hub_snd()
	return pmisc.sounds_cdr == 0
end

function play_sound(idx, clicker)
	local muted = pmisc.sounds_muted
	
	if playernum == clicker then
		if not muted then
			pmisc.sounds_cdr = 12 * HUB_SOUND_CDR_SECONDS
		end
	end

	if muted then return end
	
	local wav = hub_sounds[#hub_sounds - idx + 1] .. ".wav"
	queue_custom_sound_event(nil, wav, 127)
end

function process_sounds_cdr()
	pmisc.sounds_cdr = math.max(pmisc.sounds_cdr - 1, 0)
end

-- ======================================================================================================================================== --
-- ========================================================== PACKETS ===================================================================== --
-- ======================================================================================================================================== --

on_packet_fire_functions = {
	["a"] = function(from, str) --use emoji
				if not correct_emoji_game_state() then return end
				create_emote(from, tonumber(str))
			end,
	["b"] = function(from, str) --onmouseup (ingame)
				if not (state == STATE_PLAYING) then return end
				if mg_turn == 0 then return end
				
				local OnMouseUpMultiplayer = scripts_ptr.OnMouseUpMultiplayer
				if OnMouseUpMultiplayer then
					OnMouseUpMultiplayer(from, str)
				end
			end,
	["c"] = function(from, str) --onkey (ingame)
				if not (state == STATE_PLAYING) then return end
				if mg_turn == 0 then return end
				
				local mg_onkeyMultiplayer = scripts_ptr.OnKeyMultiplayer
				if mg_onkeyMultiplayer then
					mg_onkeyMultiplayer(from, str)
				end
			end,
	["d"] = function(from, str) --host press spacebar (after game results)
				if not (state == STATE_FINAL_RESULTS) then return end
				if not from == TRIBE_BLUE then return end
				
				local key = decode_base62(str)
				if key == LB_KEY_SPACE then
					leave_final_results_to_hub()
				end
			end,
	["e"] = function(from, str) --host ended game prematurely (one of 2 modes)
				if not (state == STATE_PLAYING) then return end
				if not from == TRIBE_BLUE then return end
				if mg_turn == 0 then return end
				
				end_game(tonumber(str))
			end,
	["f"] = function(from, str) --host manually award pts
				if not (state == STATE_HUB) then return end
				local shift = decode_boolean(str:sub(1,1))
				local ctrl = decode_boolean(str:sub(2,2))
				local key = decode_base62(str:sub(3))
				
				manual_awarding_pts_multiplayer(from, shift, ctrl, key)
			end,
	["g"] = function(from, str) --change map direction up/down
				if not (state == STATE_HUB) then return end
				if not from == TRIBE_BLUE then return end
				
				change_map(TRIBE_BLUE, tonumber(str))
			end,
	["h"] = function(from, str) --change map from searchbar
				if not (state == STATE_HUB) then return end
				if not from == TRIBE_BLUE then return end
				
				change_map(TRIBE_BLUE, nil, tonumber(str))
			end,
	["i"] = function(from, str) --release dragging card
				if not (state == STATE_HUB) then return end
				
				try_to_apply_dragging_card_multiplayer(tonumber(str:sub(1,1)), tonumber(str:sub(2)), from)
			end,
	["j"] = function(from, str) --section locks
				if not (state == STATE_HUB) then return end
				if not from == TRIBE_BLUE then return end
				
				lock_or_unlock_section(tonumber(str), from)
			end,
	["k"] = function(from, str) --swap spectator
				if not (state == STATE_HUB) then return end
				
				swap_spectator(tonumber(str), from)
			end,
	["l"] = function(from, str) --ready up/AI diff change
				if not (state == STATE_HUB) then return end
				
				swap_ready(tonumber(str:sub(2)), from, decode_boolean(str:sub(1,1)))
			end,
	["m"] = function(from, str) --add AI to hub
				if not (state == STATE_HUB) then return end
				if not from == TRIBE_BLUE then return end
				
				insert_AI_to_hub()
			end,
	["n"] = function(from, str) --swap advanced option
				if not (state == STATE_HUB) then return end
				if not from == TRIBE_BLUE then return end
				
				swap_advanced_option(tonumber(str), from)
			end,
	["o"] = function(from, str) --swap map objective
				if not (state == STATE_HUB) then return end
				if not from == TRIBE_BLUE then return end
				
				swap_map_objective(tonumber(str), from)
			end,
	["p"] = function(from, str) --modify objective value (+ or -)
				if not (state == STATE_HUB) then return end
				if not from == TRIBE_BLUE then return end
				
				modify_objective_value(tonumber(str:sub(1,1)), decode_boolean(str:sub(2)), from)
			end,
	["q"] = function(from, str) --bot middle buttons
				if not (state == STATE_HUB) then return end
				if not from == TRIBE_BLUE then return end
				
				hub_mid_btns(tonumber(str:sub(2)), decode_boolean(str:sub(1,1)), from)
			end,
	["r"] = function(from, str) --start game /cancel start
				if not (state == STATE_HUB) then return end
				if not from == TRIBE_BLUE then return end
				
				start_game(from)
			end,
	["s"] = function(from, str) --remove ai card
				if not (state == STATE_HUB) then return end
				if not from == TRIBE_BLUE then return end
				
				remove_ai_card(from, tonumber(str))
			end,
	["t"] = function(from, str) --play sound
				if not (state == STATE_HUB) then return end
				
				play_sound(tonumber(str), from)
			end,				
}


button_list = {

	[SECTION_LOCKS] = function(id, left, clicker) 						if left and im_host then if multiplayer then send_packet_now("j", tostring(id)) else lock_or_unlock_section(id, clicker) end end end,
	[SECTION_CARD_EYE] = function(id, left, clicker) 					if left and ((id == clicker) or im_host) then if multiplayer then send_packet_now("k", tostring(id)) else swap_spectator(id, clicker) end end end,
	[SECTION_CARD_READY] = function(id, left, clicker) 					if ((id == clicker) or im_host) then if multiplayer then send_packet_now("l", encode_boolean(left)..tostring(id)) else swap_ready(id, clicker, left) end end end,
	[SECTION_ADD_AI] = function(id, left, clicker) 						if left and im_host then if multiplayer then send_packet_now("m") else insert_AI_to_hub() end end end,
	[SECTION_ADVANCED_OPTIONS_BTNS] = function(id, left, clicker) 		if left and im_host then if multiplayer then send_packet_now("n", tostring(id)) else swap_advanced_option(id, clicker) end end end,
	[SECTION_MAP_OBJECTIVES] = function(id, left, clicker) 				if left and im_host then if multiplayer then send_packet_now("o", tostring(id)) else swap_map_objective(id, clicker) end end end,
	[SECTION_MAP_OBJ_BTN_MINUS] = function(id, left, clicker) 			if left and im_host then if multiplayer then send_packet_now("p", tostring(id)..encode_boolean(false)) else modify_objective_value(id, false, clicker) end end end,
	[SECTION_MAP_OBJ_BTN_PLUS] = function(id, left, clicker) 			if left and im_host then if multiplayer then send_packet_now("p", tostring(id)..encode_boolean(true)) else modify_objective_value(id, true, clicker) end end end,
	[SECTION_CHANGE_MAP] = function(id, left, clicker) 					if left and im_host then if multiplayer then send_packet_now("g", tostring(id)) else change_map(clicker, id) end end end,
	[SECTION_SEARCH_MAP_OPTION] = function(id, left, clicker) 			if left and im_host then if multiplayer then send_packet_now("h", tostring(map_cache_id_to_real_id(id))) else change_map(clicker, nil, map_cache_id_to_real_id(id)) end end end,
	[SECTION_BOT_MIDDLE_BUTTONS] = function(id, left, clicker) 			if im_host then if multiplayer then send_packet_now("q", encode_boolean(left)..tostring(id)) else hub_mid_btns(id, left, clicker) end end end,
	[SECTION_START_GAME] = function(id, left, clicker) 					if left and im_host then if multiplayer then send_packet_now("r") else start_game(clicker) end end end,
	[SECTION_CARD] = function(id, left, clicker) 						if not left and im_host and not dragging_card() then if multiplayer then send_packet_now("s", tostring(id)) else remove_ai_card(clicker, id) end end end,
	
	-- local
	
	[SECTION_SEARCH_MAP] = function(id, left, clicker) 					if left and im_host then start_map_searching() end end,
	[SECTION_VOLUME_BUTTONS] = function(id, left, clicker) 				if left then if id == 1 then mute_sounds(clicker) else if can_press_hub_snd() then if multiplayer then if not pmisc.sounds_muted then send_packet_now("t", tostring(id-1)) end else play_sound(id - 1, playernum) end end end end end, --mute sounds is local
	[SECTION_MAP_DESCRIPTION_BTNS] = function(id, left, clicker) 		if id == 1 then change_map_rules_font(left) end end,
	[SECTION_VIEW_CHANGELOG] = function(id, left, clicker) 				if id == 1 then try_to_change_changelog(left) end end,
}



function send_packet_now(id, data)
	local _data = id .. (data or "")
	--LOG("send packet with data: " .. _data)
	
	if multiplayer then
		Send(1, _data)
	else
		if debug then
			--mimic multiplayer packet offline
			OnPacket(0, 255, _data)
		end
	end
end

function OnPacket(from, packet_type, data)
	local str = data
	local str_ptr = string.sub(str, 1, 1)
	str = str:sub(2)
	
	local packet_fire_function = on_packet_fire_functions[str_ptr]
	if packet_fire_function then
		--LOG("on packet from: " .. from .. " , with data: " .. str)
		packet_fire_function(from, str)
	end
end

--encode/decode helpers

function encode_boolean(bool)
	return bool and "1" or "0"
end
function decode_boolean(str_value)
	return ntb(tonumber(str_value))
end

local BASE_62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
-- Encode a value (0-255) into 2 base-62 characters
function encode_base62(value)
    local quotient = floor(value / 62)
    local remainder = value % 62
    return BASE_62:sub(quotient + 1, quotient + 1) .. BASE_62:sub(remainder + 1, remainder + 1)
end
-- Decode 2 base-62 characters back to a number (0-255)
-- function decode_base62(encoded)
    -- return (BASE_62:find(encoded:sub(1, 1)) - 1) * 62 +
           -- (BASE_62:find(encoded:sub(2, 2)) - 1)
-- end
--with guard checks
function decode_base62(encoded)
    if type(encoded) ~= "string" or #encoded ~= 2 then
        return nil
    end

    local a = BASE_62:find(encoded:sub(1, 1), 1, true)
    local b = BASE_62:find(encoded:sub(2, 2), 1, true)

    if not a or not b then
        return nil
    end

    return (a - 1) * 62 + (b - 1)
end