Skip to content

Instantly share code, notes, and snippets.

@aduermael
Last active January 8, 2023 20:46
Show Gist options
  • Save aduermael/71af6f61de83b28c6d4aebc50307bbdf to your computer and use it in GitHub Desktop.
Save aduermael/71af6f61de83b28c6d4aebc50307bbdf to your computer and use it in GitHub Desktop.
Ludum-Dare-52
--[[
EAT YOUR FRUITS
Made this game in 48h for Ludum Dare 52 (https://ldjam.com/events/ludum-dare/52/eat-your-fruits).
- [X] players should collide with tree, /!\ though, shake scale should push back
- [X] fix punch (trigger far from a tree, then move to touch a tree)
- [x] auto-destroy fruits if not collected
- [x] set max number of collected fruits
- [x] spawn points
- [x] remove apple from other players when nbFruits == 0
- [x] map
- [x] die when falling off from map
- [ ] consider deaths to compute score (not only kills)
- [ ] improve random positions for trees and spawn points
ISSUES:
- Implemeted triggers using Objects with collision boxes + Physics = false,
best workaround I know, but not free of glitches (fruits moving players when falling on them).
Cubzh physics engine v2 will provide a proper way to implement triggers.
]]--
Config = {
Map = "aduermael.ld52_arena",
Items = {"aduermael.apple"}
}
-- Increasing gravity, giving more weight to all objects.
Config.ConstantAcceleration.Y = -300
local ROUND_DURATION = 90 -- seconds
local PRE_ROUND_DURATION = 1
local END_ROUND_DURATION = 7
local PROJECTILE_VELOCITY = 400
local PROJECTILE_LIFE = 5.0 -- auto destroyed if not touching anything
local MAX_PROJECTILES = 10 -- maximum number of fruits a player can hold
local FRUIT_LIFE_ON_GROUND = 4.0 -- seconds
local SPAWNPOINT_SEARCH_BOX_SIZE = 5 -- multiplied by map scale
local SPAWN_HEIGHT = 2 -- in "map blocks" (multiplied by map scale)
local MAP_SCALE = 9
local FIRST_PERSON_FOV = 80
Client.OnStart = function()
-- Dev.DisplayColliders = true
-- CONSTANTS
kTreeCollisionGroups = CollisionGroups(4)
kPunchCollisionGroups = CollisionGroups(5)
kFruitCollisionGroups = CollisionGroups(6)
kFruitTriggerCollisionGroups = CollisionGroups(7)
kOtherPlayersCollisionGroups = CollisionGroups(8)
-- VARS
roundEndTime = 0 -- client side cache for timer
-- AMBIENCE
ambience:sunny()
Map.Scale = MAP_SCALE
Camera.FOV = FIRST_PERSON_FOV
-- MODULES
explode = require("explode")
particles = require("particles")
multi = require("multi")
multi.teleportTriggerDistance = 100
initGameShapes()
ui:init()
initSounds()
-- PARTICLES
-- explosionEmitter is used to spawn all explosions
-- other local emitters are used, created when needed
local config = {
velocity = function()
--return Number3(math.random() * 200 - 100, math.random() * 140, math.random() * 200 - 100)
return Number3(((math.random() * 2) - 1) * 50, 20 + math.random(100), ((math.random() * 2) - 1) * 50)
end,
life = function() return 1 end,
scale = function()
return 0.5 + math.random() * 0.5
end,
}
explosionEmitter = particles:newEmitter(config)
explosionEmitter:SetParent(World)
projectileEmitterConfig = {
acceleration = function()
return -Config.ConstantAcceleration * 0.95
end,
velocity = function()
return Number3(((math.random() * 2) - 1) * 10, ((math.random() * 2) - 1) * 10, ((math.random() * 2) - 1) * 10)
end,
scale = function()
return 0.2 + math.random() * 0.1
end,
life = function() return 0.3 end
}
-- SYNC
multi:registerPlayerAction("collect", function(sender, data)
if not sender then return end
collect(sender, data.fid)
end)
multi:registerPlayerAction("punch", function(sender, data)
if not sender then return end
sender:SwingRight()
end)
multi:registerPlayerAction("fruit", function(sender, data)
if not sender then return end
local pos = Number3(data.p[1], data.p[2], data.p[3])
spawnFruit(nil, sender, data.fid, pos)
end)
multi:registerPlayerAction("shake", function(sender, data)
if not sender then return end
local tree = _trees[math.floor(data.tid)]
if tree ~= nil then
shake(tree, sender)
end
end)
multi:registerPlayerAction("shoot", function(sender, data)
if not sender then return end
sender:SwingRight()
-- local author = Players[math.floor(data.a)]
local pos = Number3(data.p[1], data.p[2], data.p[3])
local dir = Number3(data.d[1], data.d[2], data.d[3])
shoot(sender, pos, dir, data.pid)
end)
multi:registerPlayerAction("hit", function(sender, data)
local source = data.s
local target = data.t
local hp = data.hp
hitPlayer(source, target, hp)
end)
gsm.clientLobbyOnStart = function()
UI.Crosshair = true
victoryPodium:stop()
firstPerson()
print("Waiting for one more player.")
respawn(Player)
end
gsm.clientPreRoundOnStart = function()
UI.Crosshair = true
victoryPodium:stop()
firstPerson()
for _,p in ipairs(gsm.playersInRound) do
respawn(p)
end
end
gsm.clientRoundOnStart = function()
firstPerson()
for _,p in ipairs(gsm.playersInRound) do
p.nbKills = 0
setNbFruits(p, 0)
-- respawn(p)
end
ui:updateScores()
roundEndTime = Time.UnixMilli() + ROUND_DURATION * 1000
end
gsm.clientRoundTick = function()
-- anything to do here?
end
gsm.clientEndRoundOnStart = function()
UI.Crosshair = false
thirdPerson()
Player.canMove = false
local sortedPlayers = {}
for _,v in ipairs(gsm.playersInRound) do
table.insert(sortedPlayers,v)
pcall(function()
v.Motion = Number3(0,0,0)
v.nbKills = v.nbKills or 0
end)
end
table.sort(sortedPlayers, function(a, b)
return a.nbKills > b.nbKills
end)
-- remove what the player may be holding
if Player.equipped ~= nil then
Player.equipped.Tick = nil
Player.equipped:RemoveFromParent()
Player:EquipRightHand(nil)
end
if Player.equippedHand ~= nil then -- hack for first person mode
Player.equippedHand.Tick = nil
Player.equippedHand:RemoveFromParent()
Player.equippedHand = nil
end
victoryPodium:teleportPlayers(sortedPlayers)
end
gsm.clientRoundPlayersUpdate = function()
ui:updateScores()
end
-- timer required, otherwise, Map scale not ready for ray casts
Timer(0.2, function()
placeMapElements()
-- again, timer required, otherwise trees aren't really there
Timer(0.2, function()
placeSpawnPoints()
end)
end)
end
-- CLIENT FUNCTIONS
if Client ~= nil then
function setNbFruits(player, n)
local tmp = player.nbFruits
if n > MAX_PROJECTILES then n = MAX_PROJECTILES end
if n < 0 then n = 0 end
player.nbFruits = n
if player.nbFruits == 0 then
equip(player, nil)
elseif tmp == nil or tmp == 0 then
equip(player,Shape(appleModel))
end
if player == Player then
ui.nbFruitsLabel.Text = "" .. player.nbFruits
ui:refresh()
end
end
function shake(tree, author)
if author == Player then
multi:playerAction("shake", {
tid= tree.id,
})
end
local o = tree.shapes
o.dt = 0.0
if o.scale == nil then
o.scale = o.Scale:Copy()
end
local speed = 40
local duration = 0.4
local span = 0.7
if o.Tick == nil then
o.dt = 0.0
o.shakeEnd = duration
o.Tick = function(o, dt)
o.dt = o.dt + dt
local scaleDiff = (1 + math.sin(o.dt * speed)) * 0.05
o.Scale = o.scale + {scaleDiff, scaleDiff, scaleDiff}
if o.dt >= duration then
o.Scale = o.scale
o.Tick = nil
end
end
else
local remaining = o.shakeEnd - o.dt
if remaining < duration then
o.shakeEnd = o.shakeEnd + (duration - remaining)
end
end
end
-- drop player above the map, initializing multiplayer sync if needed
dropPlayer = function(player)
if not player then return end
if player:GetParent() == nil then
player.canMove = true -- will be used later
player.nbFruits = 0
World:AddChild(player)
multi:initPlayer(player)
if player == Player then
player.Head:AddChild(AudioListener)
local punchBox = Object()
player:AddChild(punchBox)
punchBox.Physics = false
local boxSize = 7
local boxHalfSize = boxSize * 0.5
punchBox.CollisionBox.Min = {-boxHalfSize, -boxHalfSize, -boxHalfSize}
punchBox.CollisionBox.Max = {boxHalfSize, boxHalfSize, boxHalfSize}
punchBox.LocalPosition = {0,2,15}
punchBox.CollisionGroups = kPunchCollisionGroups
punchBox.CollidesWithGroups = {} -- kTreeCollisionGroups
player.punchBox = punchBox
player.punchBox.OnCollision = function(o1, o2)
o1.CollidesWithGroups = {}
shake(o2.tree, player)
spawnFruit(o2.tree.leaves, player)
playPunchTree()
end
else
player.CollisionGroups = kOtherPlayersCollisionGroups
end
end
setNbFruits(player, 0)
if player == Player then
player.OnCollision = function(o1, o2)
if o2.type == "apple" then
o2.apple:RemoveFromParent()
playReload()
collect(player, o2.apple.id)
end
end
end
local o = player.parentBox or player
o.Velocity = { 0, 0, 0 }
local mapCenter = Number3(Map.Width * 0.5, Map.Height + 10, Map.Depth * 0.5) * Map.Scale
if _spawnPoints == nil or #_spawnPoints == 0 then
o.Position = mapCenter
o.Rotation = { 0, 0, 0 }
else
local spawnPoint = _spawnPoints[math.random(1,#_spawnPoints)]
o.Position = spawnPoint
local diff = mapCenter - spawnPoint
diff.Y = 0
diff:Normalize()
o.Rotation = {0, 0, 0}
o.Forward = diff
end
end
equip = function(player, shape)
if player.equipped ~= nil then
player.equipped.Tick = nil
player.equipped:RemoveFromParent()
player:EquipRightHand(nil)
end
if player.equippedHand ~= nil then -- hack for first person mode
player.equippedHand.Tick = nil
player.equippedHand:RemoveFromParent()
player.equippedHand = nil
end
player.equipped = shape
if player == Player then
if firstPersonMode then
if shape == nil then
local hand = Player.RightHand:Copy()
Camera:AddChild(hand)
player.equippedHand = hand
hand.Pivot = Number3(hand.Width, hand.Height, hand.Depth) * 0.5
hand.LocalPosition = {7,-5,5}
hand.LocalRotation = {-math.pi * 0.2,math.pi * 1.5,0}
hand.dt = 0.0
hand.Tick = function(o, dt)
o.dt = o.dt + dt
hand.LocalPosition.Y = -5 + math.sin(o.dt * 3) * 0.3
end
else
Camera:AddChild(shape)
shape.Pivot = Number3(shape.Width, shape.Height, shape.Depth) * 0.5
shape.LocalPosition = {7,-5,7}
shape.LocalRotation = {0.4,0,0}
shape.dt = 0.0
shape.Tick = function(o, dt)
o.dt = o.dt + dt
shape.LocalPosition.Y = -5 + math.sin(o.dt * 3) * 0.3
end
end
else
player:EquipRightHand(shape)
end
else
player:EquipRightHand(shape)
end
end
thirdPerson = function()
if not firstPersonMode then return end
firstPersonMode = false
Camera:SetModeThirdPerson(Player)
Player.Body.IsHidden = false
end
-- Camera:SetModeFirstPerson is currently broken. (since avatars have hair)
-- Using this for now, but SetModeFirstPerson will certainly be fixed at some point.
firstPerson = function()
if firstPersonMode then return end
firstPersonMode = true
Camera:SetModeFree()
Camera:SetParent(Player)
Camera.LocalPosition = Number3(0,25,2)
Camera.LocalRotation = Number3(0,0,0)
Player.Body.IsHidden = true
end
-- places trees, rocks, grass, spawn points...
function placeMapElements()
-- using seed for now to avoid syncing trees generation,
-- but it would be nice to get rid of this and place trees
-- differently for each round.
-- math.randomseed(19820203)
-- math.randomseed(19871102)
-- math.randomseed(20020315)
-- math.randomseed(20160505)
math.randomseed(20170601)
local ray = Ray(Number3(0,0,0), Number3(0,-1,0))
local impact
for x=3,Map.Width-2 do
for z=3,Map.Depth-2 do
ray.Origin = Number3(x-0.5,Map.Height + 10,z-0.5) * Map.Scale
impact = ray:Cast(Map)
if impact ~= nil then
if impact.Block.Color.R == 170 then
local r = math.random()
if r > 0.87 then
local p = ray.Origin + ray.Direction * impact.Distance
if r > 0.98 then
local tree = createTree()
World:AddChild(tree)
tree.Position = p
elseif r > 0.93 then
placeGrass(p)
else
placeSmallRock(p)
end
end
end
end
end
end
end
function placeSpawnPoints()
if not _spawnPoints then
_spawnPoints = {}
end
local ray = Ray(Number3(0,0,0), Number3(0,-1,0))
local impact
local boxSize = Map.Scale.Y * SPAWNPOINT_SEARCH_BOX_SIZE
local boxHalfSize = boxSize * 0.5
local minDelta = Number3(-boxHalfSize,0,-boxHalfSize)
local maxDelta = Number3(boxHalfSize,boxSize,boxHalfSize)
local box = Box({0,0,0}, {1,1,1})
local dir = Number3(0,-1,0)
local boxImpact
local n = 0
for x=3,Map.Width-2 do
for z=3,Map.Depth-2 do
local origin = Number3(x-0.5,Map.Height + 20,z-0.5) * Map.Scale
box.Min = origin + minDelta
box.Max = origin + maxDelta
ray.Origin = origin
boxImpact = box:Cast(dir, 10000, Map.CollisionGroups + kTreeCollisionGroups)
impact = ray:Cast(Map)
if impact.Block ~= nil and boxImpact ~= nil then
if impact.Distance == boxImpact.Distance then
-- n = n + 1
local p = ray.Origin + ray.Direction * impact.Distance + {0, SPAWN_HEIGHT * Map.Scale.Y, 0}
table.insert(_spawnPoints, p)
end
end
end
end
-- print("found " .. n .. " spawn points")
end
end
Client.Tick = function(dt)
multi:tick(dt)
walkTick(dt)
-- Detect if player is falling,
-- drop it above the map when it happens.
if Player.Position.Y < -300 then
if gsm.state ~= gsm.States.Round then
respawn(Player)
Player:TextBubble("💀 Oops!")
else
hitPlayer(Player.ID, Player.ID, 100)
multi:playerAction("hit", {s = Player.ID, t = Player.ID, hp = 100})
end
asDeathByFalling:Play()
-- quick way to avoid calling that condition in loop
Player.Position.Y = 100000
end
if roundEndTime > 0 then
local time = math.floor((roundEndTime - Time.UnixMilli()) / 1000)
if time < 0 then time = 0 end
local nbSeconds = string.format("%02d", time % 60)
local nbMinutes = string.format("%d", math.floor(time / 60))
ui.timerLabel.Text = nbMinutes..":"..nbSeconds
end
end
Client.AnalogPad = function(dx, dy)
if not Player.canMove then
Player.Motion = Number3(0,0,0)
return
end
Player.LocalRotation.Y = Player.LocalRotation.Y + dx * 0.005
Camera.LocalRotation.X = Camera.LocalRotation.X + -dy * 0.005
if dpadX ~= nil and dpadY ~= nil then
Player.Motion = (Player.Forward * dpadY + Player.Right * dpadX) * 100
end
end
Client.DirectionalPad = function(x, y)
if not Player.canMove then
Player.Motion = Number3(0,0,0)
dpadX = 0
dpadY = 0
return
end
-- storing globals here for AnalogPad
-- to update Player.Motion
dpadX = x dpadY = y
Player.Motion = (Player.Forward * y + Player.Right * x) * 100
end
-- jump function, triggered with Action1
Client.Action1 = function()
if Player.IsOnGround and Player.canMove then
Player.Velocity.Y = 100
playJump()
end
end
Client.Action2 = function()
if gsm.state ~= gsm.States.Round and gsm.state ~= gsm.States.Lobby then return end
if not Player.canMove then return end
if Player.nbFruits ~= nil and Player.nbFruits > 0 then
if not Player.nextProjectileID then Player.nextProjectileID = 1 end
local projectileID = Player.nextProjectileID
Player.nextProjectileID = Player.nextProjectileID + 1
multi:playerAction("shoot", {
a= Player.ID,
p={ Camera.Position.X,Camera.Position.Y,Camera.Position.Z },
d={ Camera.Forward.X,Camera.Forward.Y,Camera.Forward.Z },
pid=projectileID
})
shoot(Player, Camera.Position:Copy(), Camera.Forward:Copy(), projectileID)
else
punch(Player)
end
end
Client.Action3 = function()
punch(Player)
end
function punch(player)
if player == Player then
player.punchBox.CollidesWithGroups = kTreeCollisionGroups
Timer(0.1, function()
player.punchBox.CollidesWithGroups = {}
end)
if firstPersonMode then
local hand = player.equippedHand or player.equipped
if not hand.punching then
hand.tick = hand.Tick
hand.pos = hand.LocalPosition:Copy()
hand.punching = true
end
hand.punchDT = 0.0
hand.punchDelta = Number3(0,-2,3)
hand.Tick = function(o, dt)
local done = false
local punchDuration = 0.2
o.punchDT = o.punchDT + dt
if o.punchDT >= punchDuration then
o.punchDT = punchDuration
done = true
end
local progress = o.punchDT / punchDuration
local mov = math.sin(progress*math.pi*2-math.pi*0.5) + 1
o.LocalPosition = o.pos + (o.punchDelta * mov)
if done then
o.Tick = o.tick
o.punching = false
end
end
else
player:SwingRight()
end
multi:playerAction("punch", {
a= Player.ID,
})
else
player:SwingRight()
end
end
function collect(author, fruitID)
setNbFruits(author, author.nbFruits + 1)
local fruit = _fruits[fruitID]
if fruit ~= nil then
fruit:RemoveFromParent()
_fruits[fruitID] = nil
end
if author == Player then
-- inform others that fruit has been collected
multi:playerAction("collect", {
fid= fruitID,
})
end
end
function shoot(author, pos, dir, projectileID)
if projectileID == nil then error("projectile should have an ID") end
if author == Player and author.nbFruits == 0 then return end -- can't shoot without fruits
setNbFruits(author, author.nbFruits - 1)
author.asThrow.Position = pos
author.asThrow:Stop()
author.asThrow.Pitch = 1 + ((math.random() * 2) - 1) * 0.1
author.asThrow:Play()
local projectile = Shape(appleModel)
projectile.id = projectileID
if not author.projectiles then author.projectiles = {} end
author.projectiles[projectile.id] = projectile
projectile.life = PROJECTILE_LIFE
World:AddChild(projectile)
projectile.CollisionGroups = {}
projectile.CollidesWithGroups = {}
-- adding little delay to active collisions
-- otherwise, explosion is triggered when there's a wall
-- in your back ^^'
Timer(0.02, function()
projectile.CollidesWithGroups = Map.CollisionGroups + kOtherPlayersCollisionGroups + kTreeCollisionGroups
end)
projectile.Physics = true
projectile.Position = pos
projectile.Forward = dir
projectile.Acceleration = -Config.ConstantAcceleration
projectile.Velocity = dir * PROJECTILE_VELOCITY
projectile.Friction = 1
projectile.Bounciness = 0.8
projectile.emitter = particles:newEmitter(projectileEmitterConfig)
projectile.emitter:SetParent(projectile)
projectile.emitter.Tick = function(o, dt)
o:spawn(2)
end
projectile.rot = Number3(0,0,0)
projectile.rotX = (math.random() * 6.0) - 3.0
projectile.rotY = (math.random() * 10.0) - 5.0
projectile.Tick = function(o, dt)
o.rot.X = o.rot.X + dt * o.rotX
o.rot.Y = o.rot.Y + dt * o.rotY
o.Rotation = o.rot
o.life = o.life - dt
if o.life <= 0 then
o:OnCollision(Map)
end
end
-- o1: projectile
-- o2: object projectile enters in collision with
projectile.OnCollision = function(o1, o2)
if author == Player then
if o2.CollisionGroups == kTreeCollisionGroups then
shake(o2.tree, author)
spawnFruit(o2.tree.leaves, author)
end
end
author.projectiles[o1.id] = nil
o1.Tick = nil
o1.emitter.Tick = nil
author.asExplode.Position = o1.Position
if author.asThrow:GetParent() == o1 then
author.asThrow:Stop()
end
author.asExplode:Stop()
author.asExplode:Play()
explosionEmitter.Position = o1.Position
explosionEmitter:spawn(20)
o1:RemoveFromParent()
local pos = o1.Position:Copy()
local sphere1 = {center = pos, radius = 15}
local sphere2 = {center = pos, radius = 25}
if Player == author then
for _,p in pairs(Players) do
if p ~= Player and p.hp > 0 then
-- NOTE: this is weird that the box has to be offseted
-- prior to local to world conversion.
-- It's not clear in docs if the collision box is expressed
-- in local coorfinates, but they're definitely not world coords.
-- Let's clarify that.
local box = p.CollisionBox:Copy()
box.Min = {-box.Max.X * 0.5, 0, -box.Max.Z * 0.5}
box.Max = {box.Max.X * 0.5, box.Max.Y, box.Max.Z * 0.5}
box.Min = p:PositionLocalToWorld(box.Min)
box.Max = p:PositionLocalToWorld(box.Max)
if sphereCollidesWithBox(sphere1, box) then
multi:playerAction("hit", {s = Player.ID, t = p.ID, hp = 100})
hitPlayer(Player.ID, p.ID, 100)
elseif sphereCollidesWithBox(sphere2, box) then
multi:playerAction("hit", {s = Player.ID, t = p.ID, hp = 50})
hitPlayer(Player.ID, p.ID, 50)
end
end
end
end
end
end
function killAndRespawn(player)
if not player then return end
player.IsHidden = true
if player == Player then
player.canMove = false
end
Timer(2, function()
if gsm.state ~= gsm.States.Round then return end
-- Avoid error when player just left
pcall(function()
respawn(player)
end)
end)
end
function respawn(player)
if not player then return end
if player == Player then
dropPlayer(player)
player.canMove = true
end
-- Wait to avoid showing character when respawning
Timer(0.5, function()
if gsm.state ~= gsm.States.Round then return end
-- Avoid error when player just left
pcall(function()
player.IsHidden = false
player.hp = 100
end)
end)
end
function hitPlayer(source, target, hp)
if gsm.state ~= gsm.States.Round then return end
local sourcePlayer
local targetPlayer
for k,p in pairs(Players) do
if k == source then
sourcePlayer = p
end
if k == target then
targetPlayer = p
end
end
if sourcePlayer == nil then return end
if targetPlayer == nil then return end
targetPlayer.hp = targetPlayer.hp - hp
if targetPlayer.hp <= 0 then
if targetPlayer == Player then
local e = Event()
e.action = "killed"
e.t = target
e.s = source
e:SendTo(Server)
end
explode:shapes(targetPlayer.Body)
killAndRespawn(targetPlayer)
print(targetPlayer.Username.." 💀 by "..sourcePlayer.Username)
if sourcePlayer == Player then
asPingKill:Play()
end
end
end
Client.OnPlayerJoin = function(player)
print(player.Username.." joined the game.")
dropPlayer(player)
player.hp = 100
player.kills = 0
player.deaths = 0
local asThrow = AudioSource()
asThrow.Sound = "whooshes_small_4"
asThrow.Volume = 0.3
asThrow.Radius = 600
asThrow.Spatialized = true
asThrow:SetParent(World)
player.asThrow = asThrow
local asExplode = AudioSource()
asExplode.Sound = "drumkick_1" -- "small_explosion_3"
asExplode.Volume = 0.75
asExplode.Radius = 300
asExplode.Spatialized = true
asExplode:SetParent(World)
player.asExplode = asExplode
if player == Player then
Player.Body.IsHidden = true
gsm:clientSyncState()
end
end
Client.OnPlayerLeave = function(player)
print(player.Username.." just left the game.")
multi:removePlayer(player)
end
Client.DidReceiveEvent = function(event)
if gsm:clientHandleEvent(event) then return end
multi:receive(event)
if event.action == "nbKills" then
local source = Players[math.floor(event.p)]
if not source then return end
source.nbKills = event.nb
ui:updateScores()
end
end
Screen.DidResize = function(width, height)
uikit:fitScreen()
ui:refresh()
end
-- SERVER
Server.OnStart = function()
gsm.minPlayersToStart = DEBUG == true and 1 or 2
gsm.playerCanJoinDuringRound = true
gsm.durationPreRound = PRE_ROUND_DURATION
gsm.durationRound = ROUND_DURATION
gsm.durationEndRound = END_ROUND_DURATION
gsm.serverLobbyOnStart = function() end
gsm.serverRoundOnStart = function()
for _,p in ipairs(gsm.playersInRound) do
p.nbKills = 0
end
end
-- Timer to wait all players when restarting the server
Timer(3, function()
if gsm.state == gsm.States.Lobby then
gsm:serverSetGameState(gsm.States.Lobby)
end
end)
end
Server.OnPlayerJoin = function(p)
gsm:serverOnPlayerJoin(p)
end
Server.OnPlayerLeave = function(p)
gsm:serverOnPlayerLeave(p)
end
Server.DidReceiveEvent = function(e)
if gsm:serverHandleEvent(e) then return end
if e.action == "killed" then
local source = Players[math.floor(e.s)]
local target = Players[math.floor(e.t)]
source.nbKills = source.nbKills or 0
if source == target then -- self kill
-- TODO: nbDeaths
else
source.nbKills = source.nbKills + 1
end
local e = Event()
e.action = "nbKills"
e.p = source.ID
e.nb = source.nbKills
e:SendTo(Players)
if source.nbKills >= 10 then
gsm:serverSetGameState(gsm.States.EndRound)
end
end
end
-- UI
ui = {
padding = 8,
smallPadding = 4,
players = {}, -- labels for each connected player, indexed by ID
orderedPlayers = {}, -- ordered labels
}
ui.init = function(self)
victoryPodium:init()
uikit = require("uikit")
uikit:init()
Pointer:Hide()
UI.Crosshair = true
local topLeft = uikit:createNode()
topLeft:setParent(uikit.rootFrame)
self.topLeft = topLeft
local bg = uikit:createFrame(Color(0,0,0,0.5))
bg:setParent(topLeft)
topLeft.LocalPosition.Z = -1 -- nessary due to uikit issues (temporary)
topLeft.bg = bg
local topRight = uikit:createNode()
topRight:setParent(uikit.rootFrame)
self.topRight = topRight
bg = uikit:createFrame(Color(0,0,0,0.5))
bg:setParent(topRight)
topRight.LocalPosition.Z = -1 -- nessary due to uikit issues (temporary)
topRight.bg = bg
local bottomRight = uikit:createNode()
bottomRight:setParent(uikit.rootFrame)
self.bottomRight = bottomRight
bg = uikit:createFrame(Color(0,0,0,0.5))
bg:setParent(bottomRight)
-- nessary due to uikit issues (temporary)
bottomRight.LocalPosition.Z = 250
bg.LocalPosition.Z = -1
bottomRight.bg = bg
self.nbFruitsLabel = uikit:createText("" .. 0, Color.White)
self.nbFruitsLabel:setParent(bottomRight)
local apple = Shape(appleModel)
self.fruitsIcon = uikit:createShape(apple)
self.fruitsIcon:setParent(bottomRight)
-- NOTE: -2 to exclude apple's stem
apple.Pivot = Number3(apple.Width, apple.Height - 2, apple.Depth) * 0.5
apple.rot = Number3(0.1,0,0)
apple.dt = 0.0
apple.scale = apple.Scale:Copy()
apple.Tick = function(o, dt)
o.rot.Y = o.rot.Y + dt
o.Rotation = o.rot
o.dt = o.dt + dt
o.Scale = apple.scale + apple.scale * math.sin(o.dt * 10) * 0.1
end
self.fruitsIcon.LocalPosition.Z = -1
self.timerLabel = uikit:createText("0:00", Color.White)
self.timerLabel:setParent(topRight)
ui:refresh()
end
ui.refresh = function(self)
-- BOTTOM RIGHT
self.nbFruitsLabel.LocalPosition.X = -self.nbFruitsLabel.Width - self.padding
self.nbFruitsLabel.LocalPosition.Y = self.padding
self.fruitsIcon.LocalPosition.X = self.nbFruitsLabel.LocalPosition.X - self.fruitsIcon.Width * 0.5 - self.padding * 2
self.fruitsIcon.LocalPosition.Y = self.fruitsIcon.shape.Pivot.Y * uikit.kShapeScale + self.padding
self.bottomRight.LocalPosition.X = Screen.Width - self.padding
self.bottomRight.LocalPosition.Y = self.padding
self.bottomRight.bg.Width = self.nbFruitsLabel.Width + self.padding * 2 + 40
self.bottomRight.bg.Height = self.nbFruitsLabel.Height + self.padding * 2
self.bottomRight.bg.LocalPosition.X = -self.bottomRight.bg.Width
-- TOP RIGHT
self.timerLabel.LocalPosition.X = -self.timerLabel.Width - self.padding
self.timerLabel.LocalPosition.Y = -self.timerLabel.Height - self.padding
self.topRight.LocalPosition.X = Screen.Width - self.padding
self.topRight.LocalPosition.Y = Screen.Height - self.padding
self.topRight.bg.Width = self.timerLabel.Width + self.padding * 2
self.topRight.bg.Height = self.timerLabel.Height + self.padding * 2
self.topRight.bg.LocalPosition.X = -self.topRight.bg.Width
self.topRight.bg.LocalPosition.Y = -self.topRight.bg.Height
-- TOP LEFT
self.topLeft.LocalPosition.X = self.padding
self.topLeft.LocalPosition.Y = Screen.Height - self.padding
self:updateScores()
end
ui.updateScores = function(self)
local startX = self.padding
local startY = -self.padding
local toRemove = {}
local toAdd = {}
local found
for id, _ in pairs(self.players) do
found = false
for _, p in ipairs(gsm.playersInRound) do
if id == p.ID then found = true break end
end
if found == false then
table.insert(toRemove, id)
end
end
-- REMOVE
for _, id in ipairs(toRemove) do
self.players[id].label:remove()
self.players[id].kills:remove()
self.players[id].separator:remove()
self.players[id].deaths:remove()
self.players[id] = nil
end
-- add newcomers
local entry
for _, p in ipairs(gsm.playersInRound) do
entry = self.players[p.ID]
if entry == nil then
entry = {}
entry.nbKills = p.nbKills or 0
entry.ID = p.ID
entry.label = uikit:createText(p.Username, Color.White)
entry.label:setParent(self.topLeft)
entry.kills = uikit:createText("" .. entry.nbKills, Color.Green)
entry.kills:setParent(self.topLeft)
entry.separator = uikit:createText("|", Color.White)
entry.separator:setParent(self.topLeft)
entry.deaths = uikit:createText("" .. 0, Color.Red)
entry.deaths:setParent(self.topLeft)
self.players[entry.ID] = entry
end
-- update
if p.nbKills ~= nil and entry.nbKills ~= p.nbKills then
entry.nbKills = p.nbKills
entry.kills.Text = "" .. entry.nbKills
end
end
local ordered = {}
for _,entry in pairs(self.players) do
entry.nbKills = entry.nbKills or 0
table.insert(ordered,entry)
end
if #ordered == 0 then return end
table.sort(ordered, function(a, b)
return a.nbKills > b.nbKills
end)
local previous
local maxEdge = 0
for i, e in ipairs(ordered) do
e.label.LocalPosition.X = startX
if previous ~= nil then
e.label.LocalPosition.Y = previous.LocalPosition.Y - e.label.Height - self.padding
else
e.label.LocalPosition.Y = -e.label.Height + startY
end
e.kills.LocalPosition.Y = e.label.LocalPosition.Y
e.separator.LocalPosition.Y = e.label.LocalPosition.Y
e.deaths.LocalPosition.Y = e.label.LocalPosition.Y
e.kills.LocalPosition.X = e.label.LocalPosition.X + e.label.Width + self.padding
e.separator.LocalPosition.X = e.kills.LocalPosition.X + e.kills.Width + self.smallPadding
e.deaths.LocalPosition.X = e.separator.LocalPosition.X + e.separator.Width + self.smallPadding
local edge = e.deaths.LocalPosition.X + e.deaths.Width
if edge > maxEdge then maxEdge = edge end
previous = e.label
end
self.topLeft.bg.Width = maxEdge + self.padding
self.topLeft.bg.LocalPosition.Y = previous.LocalPosition.Y - self.padding
self.topLeft.bg.Height = -self.topLeft.bg.LocalPosition.Y
end
-- Inits/builds shapes used by the game.
function initGameShapes()
-- tree trunk colors
local c1 = Color(100,100,100)
local c2 = Color(80,80,80)
local c3 = Color(150,150,150) -- inside
-- leaves colors
local c4 = Color(100,150,100)
local c5 = Color(80,100,80)
local trunkPartSize = 10
local trunkPartSizeMinusOne = trunkPartSize - 1
local _trunkPart = MutableShape()
for x=0,trunkPartSizeMinusOne do
for z=0,trunkPartSizeMinusOne do
for y=0,trunkPartSizeMinusOne do
if x == 0 or z == 0 or x == trunkPartSizeMinusOne or z == trunkPartSizeMinusOne then
if math.random() > 0.8 then
_trunkPart:AddBlock(c2, x,y,z)
else
_trunkPart:AddBlock(c1, x,y,z)
end
elseif y == 0 or y == trunkPartSizeMinusOne then
_trunkPart:AddBlock(c3, x,y,z)
end
end
end
end
_trunkPart.Pivot = {trunkPartSize * 0.5, 0, trunkPartSize * 0.5}
trunkPart = Shape(_trunkPart)
trunkPart.CollisionGroups = kTreeCollisionGroups
local leavesPartSize = 10
local leavesPartSizeMinusOne = leavesPartSize - 1
local _leavesPart = MutableShape()
for x=0,leavesPartSizeMinusOne do
for z=0,leavesPartSizeMinusOne do
for y=0,leavesPartSizeMinusOne do
if x == 0 or x == leavesPartSizeMinusOne
or z == 0 or z == leavesPartSizeMinusOne
or y == 0 or y == leavesPartSizeMinusOne then
if math.random() > 0.8 then
_leavesPart:AddBlock(c5, x,y,z)
else
_leavesPart:AddBlock(c4, x,y,z)
end
end
end
end
end
_leavesPart.Pivot = {leavesPartSize * 0.5, 0, leavesPartSize * 0.5}
leavesPart = Shape(_leavesPart)
leavesPart.CollisionGroups = kTreeCollisionGroups
createTree = function()
if not _trees then -- create index for trees
_trees = {}
_treesNextIndex = 1
end
local tree = Object()
tree.id = _treesNextIndex
_treesNextIndex = _treesNextIndex + 1
_trees[tree.id] = tree
tree.shapes = Object()
tree.colliders = Object()
tree:AddChild(tree.shapes)
tree:AddChild(tree.colliders)
local parts = math.random(3,4)
local part
local previousPart
local collider
local previousCollider
-- trunk
for i = 1,parts do
part = Shape(trunkPart)
part.tree = tree
local thickness = 1 - (i - 1) / 15
part.Scale = {thickness, 1.0 + math.random() * 0.5, thickness}
if previousPart ~= nil then
previousPart:AddChild(part)
part.LocalPosition.Y = trunkPartSize - 1
else
tree.shapes:AddChild(part)
end
part.LocalRotation = {math.random() * 0.2, math.random(0,3) * math.pi, 0}
-- set collider
-- Using separate colliders because we scale tree parts
-- and don't want the box to be affected by what's only
-- supposed to be a visual effect.
collider = Object()
-- againg, offsetting boxes to compensate probable engine issue
-- + adding little scale margin
local box = part.CollisionBox:Copy()
box.Min = Number3(-box.Max.X * 0.5, 0, -box.Max.Z * 0.5) * 1.05
box.Max = Number3(box.Max.X * 0.5, box.Max.Y, box.Max.Z * 0.5) * 1.05
collider.CollisionBox = box
collider.CollidesWithGroups = Player.CollisionGroups
if previousCollider ~= nil then
previousCollider:AddChild(collider)
else
tree.colliders:AddChild(collider)
end
collider.LocalPosition = part.LocalPosition
collider.LocalRotation = part.LocalRotation
collider.Scale = part.Scale
previousCollider = collider
previousPart = part
end
parts = math.random(2,3)
-- leaves
for i = 1,parts do
part = Shape(leavesPart)
part.tree = tree
local thickness = (5 - i) * (1 + math.random() * 0.3) * 0.8
previousPart:AddChild(part)
part.LocalPosition.Y = trunkPartSize - 1
part.Scale = {thickness, 1.0 + math.random() * 0.5, thickness}
part.Scale = part.Scale / previousPart.LossyScale
part.LocalRotation = {math.random() * 0.2, math.random(0,3) * math.pi, 0}
previousPart = part
-- fruits will fall from there
if i == 1 then
part.Shadow = true
tree.leaves = part
end
end
return tree
end
-- Fruits
appleModel = Shape(Items.aduermael.apple)
-- if & pos are nil when the fruit is generated locally
spawnFruit = function(leaves, author, id, pos)
-- create index for fruits
-- fruit ids are built combining player ID + increment
if not _fruits then
_fruits = {}
_fruitsNextIndex = 1
end
local apple = Shape(appleModel)
if author == Player then
if id ~= nil then error("id should be nil when spawning local fruit") end
id = "" .. Player.ID .. ":".. _fruitsNextIndex
_fruitsNextIndex = _fruitsNextIndex + 1
end
apple.id = id
_fruits[apple.id] = apple
local trigger = Object()
trigger.type = "apple"
trigger.apple = apple
trigger.CollisionBox = apple.CollisionBox:Copy()
trigger.Physics = false
trigger.CollisionGroups = {}
trigger.CollidesWithGroups = Player.CollisionGroups
apple:AddChild(trigger)
trigger.LocalPosition = -apple.Pivot
if pos ~= nil then
-- use provided spawn position
World:AddChild(apple)
apple.Position = pos
else
leaves:AddChild(apple)
-- pick random position within first level of tree leaves
-- not too close from trunk
local halfW = leaves.Width * 0.5
local dFromCenter = 2.5
local d = Number3(0,0,1)
d:Rotate({0,math.random() * math.pi * 2, 0})
d = d * (dFromCenter + math.random() * (halfW - dFromCenter))
apple.LocalPosition.Y = -2
apple.LocalPosition.X = d.X
apple.LocalPosition.Z = d.Z
World:AddChild(apple, true) -- keep world position
end
apple.Rotation = {0,0,0}
apple.Physics = true
apple.CollisionGroups = {}
apple.CollidesWithGroups = Map.CollisionGroups
local fruitID = apple.id
Timer(FRUIT_LIFE_ON_GROUND, function()
local fruit = _fruits[fruitID]
if fruit ~= nil then
fruit:RemoveChildren() -- removes trigger
fruit.dt = 0.0
local duration = 0.3
fruit.Tick = function(o,dt)
local done = false
o.dt = o.dt + dt
if o.dt > duration then o.dt = duration done = true end
local progress = o.dt / duration
o.Scale = 1 - progress * progress
if done then
o.Tick = nil
o:RemoveFromParent()
end
end
_fruits[fruitID] = nil
end
end)
if author == Player then
multi:playerAction("fruit", {
fid= apple.id,
p={ apple.Position.X,apple.Position.Y,apple.Position.Z },
})
end
end
end
-- SOUNDS
function initSounds()
local asPunchTree1 = AudioSource()
asPunchTree1.Sound = "wood_impact_1"
asPunchTree1.Volume = 0.5
asPunchTree1.Spatialized = false
local asPunchTree2 = AudioSource()
asPunchTree2.Sound = "wood_impact_3"
asPunchTree2.Volume = 0.5
asPunchTree2.Spatialized = false
asPunchTree3 = AudioSource()
asPunchTree3.Sound = "wood_impact_4"
asPunchTree3.Volume = 0.5
asPunchTree3.Spatialized = false
local asPunchTree4 = AudioSource()
asPunchTree4.Sound = "wood_impact_5"
asPunchTree4.Volume = 0.5
asPunchTree4.Spatialized = false
asPunchTree = { asPunchTree1, asPunchTree2, asPunchTree3, asPunchTree4 }
function playPunchTree()
local s = asPunchTree[math.random(1,#asPunchTree)]
s:Stop()
s:Play()
end
asReload1 = AudioSource()
asReload1.Sound = "gun_reload_1"
asReload1.Volume = 0.075
asReload1.Spatialized = false
asReload2 = AudioSource()
asReload2.Sound = "gun_reload_2"
asReload2.Volume = 0.075
asReload2.Spatialized = false
asReload3 = AudioSource()
asReload3.Sound = "gun_reload_3"
asReload3.Volume = 0.075
asReload3.Spatialized = false
asReload = { asReload1, asReload2, asReload3 }
function playReload()
local s = asReload[math.random(1,#asReload)]
s:Stop()
s:Play()
end
asJump1 = AudioSource()
asJump1.Sound = "hurtscream_1"
asJump1.Volume = 0.075
asJump1.Spatialized = false
asJump2 = AudioSource()
asJump2.Sound = "hurtscream_2"
asJump2.Volume = 0.075
asJump2.Spatialized = false
asJump3 = AudioSource()
asJump3.Sound = "hurtscream_3"
asJump3.Volume = 0.075
asJump3.Spatialized = false
asJumps = { asJump1, asJump2, asJump3 }
function playJump()
local s = asJumps[math.random(1,#asJumps)]
s:Stop()
s:Play()
end
asDeathByFalling = AudioSource()
asDeathByFalling.Sound = "deathscream_3"
asDeathByFalling.Volume = 0.075
asDeathByFalling.Spatialized = false
asPingKill = AudioSource()
asPingKill.Sound = "metal_clanging_3"
asPingKill.Volume = 0.65
asPingKill.Spatialized = false
asPingKill.Pitch = 2
function walkAudioPlay(shape, asset, key)
local audio
if key == nil then
audio = shape.audio
else audio = shape[key] end
audio:Stop()
audio.Sound = asset
audio.Spatialized = true
audio.Volume = audio.vol -- proxy attribute
audio:Play()
end
Player.walk = 0
local audio = AudioSource()
audio.vol = 0.2
Player.step = audio
Player:AddChild(audio)
function walkTick(dt)
Player.walk = Player.walk + dt
if Player.IsOnGround and (Player.Motion.SquaredLength > 0.01) then
if Player.walk > 0.3 then
Player.walk = 0
if Player.BlockUnderneath == nil then return end
local fileNum = math.random(5)
local c = Player.BlockUnderneath.Color
local r = c.R
local g = c.G
local b = c.B
-- quick way to define what sound to play
-- changing the map would break that...
if r == g and r == b then
walkAudioPlay(Player, "walk_concrete_"..fileNum, "step")
elseif r == 170 or r == 209 then
walkAudioPlay(Player, "walk_grass_"..fileNum, "step")
else
walkAudioPlay(Player, "walk_wood_"..fileNum, "step")
end
end
end
end
end
-- AMBIENCE
-- NOTE: this should be turned into a module,
-- would make it easier for other games to use it.
ambience = {}
ambience._common = function(self)
TimeCycle.On = false
Time.Current = Time.Noon
end
ambience.sunny = function(self)
self:_common()
Fog.Near = 150
Fog.Far = 300
local m = TimeCycle.Marks.Noon
m.SkyColor = Color(29, 118, 213)
m.HorizonColor = Color(95, 168, 236)
m.AbyssColor = Color(31, 81, 143)
m.AmbientLightColor = Color(80,120,100)
Clouds.Altitude = 30
sun = Light()
sun.On = true
--sun.Color = Color(179,143,32)
sun.Color = Color(99,75,0)
sun.Type = LightType.Directional
World:AddChild(sun)
sun.Rotation = {math.pi * 0.3, math.pi * 0.5, 0}
end
-- GAME STATE MANAGER
-- This is a work in progress module by @caillef,
-- pasted here because not available directly on Cubzh yet.
local gameStateManager = {}
local gameStateManagerMetatable = {
__index = {
States = {
Lobby = 1,
PreRound = 2,
Round = 3,
EndRound = 4
},
StateNames = {
"Lobby",
"PreRound",
"Round",
"EndRound"
},
Events = {
GameState = "gs",
SyncState = "st",
PlayersInRound = "pr"
},
state = 1,
playerCanJoinDuringRound = true,
_isClientInit = false,
_isServerInit = false,
_clientInit = function(self)
self.object = Object()
self.object:SetParent(World)
self.object.Tick = function(dt)
self:_clientUpdate(dt)
end
self._isClientInit = true
end,
playersInRound = {},
_clientUpdate = function(self, dt)
if not self._isClientInit then self:_clientInit() end
local state = self.state
local tickFunctionName = "client"..self.StateNames[state].."Tick"
local tickFunction = self[tickFunctionName]
if tickFunction then
tickFunction()
end
end,
_clientSetGameState = function(self,newState)
if not self._isClientInit then self:_clientInit() end
self.prevState = self.state
self.state = newState
local startFunctionName = "client"..self.StateNames[newState].."OnStart"
local startFunction = self[startFunctionName]
if startFunction then
startFunction()
end
end,
clientHandleEvent = function(self, e)
if not self._isClientInit then self:_clientInit() end
if e.action == self.Events.GameState then
self:_clientSetGameState(e.state)
return true
elseif e.action == self.Events.PlayersInRound then
local list = JSON:Decode(e.list)
local playersInRound = {}
for _,id in ipairs(list) do
table.insert(playersInRound, Players[math.floor(id)])
end
self.playersInRound = playersInRound
if self.clientRoundPlayersUpdate then
self:clientRoundPlayersUpdate(playersInRound)
end
return true
end
return false
end,
clientSyncState = function(self)
local e = Event()
e.action = gameStateManager.Events.SyncState
e:SendTo(Server)
end,
_serverInit = function(self)
self.object = Object()
self.object:SetParent(World)
self.object.Tick = function(dt)
self:_serverUpdate(dt)
end
self._isServerInit = true
end,
_serverUpdate = function(self, dt)
if not self._isServerInit then self:_serverInit() end
local state = self.state
local tickFunctionName = "server"..self.StateNames[state].."Tick"
local tickFunction = self[tickFunctionName]
if tickFunction then
tickFunction()
end
if state == self.States.Lobby then
if self.minPlayersToStart and #Players >= self.minPlayersToStart then
self:serverSetGameState(self.States.PreRound)
end
end
end,
_serverUpdatePlayersInRound = function(self)
local playersId = {}
for _,p in pairs(Players) do
local alreadyIn = false
for _,p2 in ipairs(self.playersInRound) do
if p2 == p then alreadyIn = true end
end
if not alreadyIn then
table.insert(self.playersInRound, p)
end
table.insert(playersId, p.ID)
p.nbKills = p.nbKills or 0
end
-- sync players in round with clients
local e = Event()
e.action = self.Events.PlayersInRound
e.list = JSON:Encode(playersId)
e:SendTo(Players)
end,
serverSetGameState = function(self,newState)
if not self._isServerInit then self:_serverInit() end
if gsm._serverPhaseTimer then
gsm._serverPhaseTimer:Cancel()
end
if newState == self.state then return end
self.prevState = self.state
self.state = newState
local stateName = self.StateNames[newState]
local startFunctionName = "server"..stateName.."OnStart"
local startFunction = self[startFunctionName]
if startFunction then
startFunction()
end
if self["duration"..stateName] ~= nil then
gsm.stateEndAt = Time.UnixMilli() + self["duration"..stateName] * 1000
gsm._serverPhaseTimer = Timer(self["duration"..stateName], function()
local nextState
if newState == self.States.EndRound then
nextState = self.States.PreRound
else
nextState = newState + 1
end
self:serverSetGameState(nextState)
end)
end
if newState == self.States.Lobby then
self.playersInRound = {}
elseif newState == self.States.PreRound then
self:_serverUpdatePlayersInRound()
if self.minPlayersToStart and #self.playersInRound < self.minPlayersToStart then
self:serverSetGameState(self.States.Lobby)
end
elseif newState == self.States.Round then
self:_serverUpdatePlayersInRound()
elseif newState == self.States.EndRound then
end
-- sync with players
local e = Event()
e.action = self.Events.GameState
e.state = newState
e:SendTo(Players)
end,
serverHandleEvent = function(self, e)
if not self._isServerInit then self:_serverInit() end
if e.action == self.Events.SyncState then
local ev = Event()
ev.action = self.Events.GameState
ev.state = self.state
ev:SendTo(e.Sender)
return true
end
return false
end,
serverOnPlayerJoin = function(self, player)
if gsm.playerCanJoinDuringRound then
self:_serverUpdatePlayersInRound()
end
Timer(1, function()
for _,p in ipairs(self.playersInRound) do
local e = Event()
e.action = "nbKills"
e.p = p.ID
e.nb = p.nbKills or 0
e:SendTo(player)
end
if gsm.state == gsm.States.Round then
local e = Event()
e.action = "roundEndAt"
e.t = gsm.stateEndAt
e:SendTo(player)
end
end)
end,
serverOnPlayerLeave = function(self, player)
for k,p in ipairs(self.playersInRound) do
if player == p then
table.remove(self.playersInRound,k)
self:_serverUpdatePlayersInRound()
end
end
if self.minPlayersToStart and #self.playersInRound < self.minPlayersToStart then
self:serverSetGameState(self.States.Lobby)
end
end
}
}
setmetatable(gameStateManager, gameStateManagerMetatable)
gsm = gameStateManager
-- VICTORY PODIUM
-- This is a work in progress module by @caillef,
-- pasted here because not available directly on Cubzh yet.
victoryPodium = {}
local victoryPodiumMetatable = {
__index = {
_isInit = false,
podiumPosition = Number3(0,500,0),
_podium = nil,
init = function(self)
self._podium = Object()
self._podium:SetParent(World)
self._podium.Position = self.podiumPosition - Number3(0,10,0)
self._podium.IsHidden = true
local floor = MutableShape()
floor:AddBlock(Color.Black,0,0,0)
floor:SetParent(self._podium)
floor.CollidesWithGroups = Player.CollisionGroups
floor.Pivot = Number3(0.5,1,1)
floor.Scale.X = 200
floor.Scale.Z = 200
local wall = MutableShape()
wall:AddBlock(Color.Black,0,0,0)
wall:SetParent(self._podium)
wall.Pivot = Number3(0.5,0,0.5)
wall.Scale.X = 200
wall.Scale.Y = 200
local gold = MutableShape()
gold:AddBlock(Color.Yellow,0,0,0)
gold:SetParent(self._podium)
gold.CollidesWithGroups = Player.CollisionGroups
gold.Pivot = Number3(0.5,0,1)
gold.Scale.X = 15
gold.Scale.Y = 9
gold.Scale.Z = 15
gold.LocalPosition = Number3(0,0,0)
local silver = MutableShape()
silver:AddBlock(Color.Grey,0,0,0)
silver:SetParent(self._podium)
silver.CollidesWithGroups = Player.CollisionGroups
silver.Pivot = Number3(0.5,0,1)
silver.Scale.X = 15
silver.Scale.Y = 6
silver.Scale.Z = 15
silver.LocalPosition = Number3(15,0,0)
local bronze = MutableShape()
bronze:AddBlock(Color.Orange,0,0,0)
bronze:SetParent(self._podium)
bronze.CollidesWithGroups = Player.CollisionGroups
bronze.Pivot = Number3(0.5,0,1)
bronze.Scale.X = 15
bronze.Scale.Y = 3
bronze.Scale.Z = 15
bronze.LocalPosition = Number3(-15,0,0)
self._isInit = true
end,
stop = function(self)
if not self._lastWinners then return end
-- hide nameplate
for _,p in ipairs(self._lastWinners) do
pcall(function()
if p.nameplate then
p.nameplate.IsHidden = true
end
end)
end
end,
teleportPlayers = function(self, winners)
if not self._isInit then print("call victoryPodium:init() first") return end
self._podium.IsHidden = false
self._lastWinners = winners
Camera:SetModeFree()
Camera:SetParent(World)
Camera.Position = self.podiumPosition + Number3(0,10,-30)
Camera.Rotation = Number3(0.2,0,0)
pcall(function()
local p1 = Players[winners[1].ID]
p1.Position = self.podiumPosition + Number3(0,15,-7.5)
p1.Forward = Number3(0,0,-1)
p1.IsHidden = false
if not p1.nameplate then
p1.nameplate = Text()
p1.nameplate.Text = p1.Username
p1.nameplate:SetParent(p1.Head)
p1.nameplate.LocalRotation = Number3(0,math.pi,0)
p1.nameplate.LocalPosition = Number3(0,15,0)
end
p1.nameplate.IsHidden = false
end)
if #winners > 1 then
pcall(function()
local p2 = Players[winners[2].ID]
p2.Position = self.podiumPosition + Number3(15,15,-7.5)
p2.Forward = Number3(-0.4,0,-1)
p2.IsHidden = false
if not p2.nameplate then
p2.nameplate = Text()
p2.nameplate.Text = p2.Username
p2.nameplate:SetParent(p2.Head)
p2.nameplate.LocalRotation = Number3(0,math.pi,0)
p2.nameplate.LocalPosition = Number3(0,15,0)
end
p2.nameplate.IsHidden = false
end)
end
if #winners > 2 then
pcall(function()
local p3 = Players[winners[3].ID]
p3.Position = self.podiumPosition + Number3(-15,15,-7.5)
p3.Forward = Number3(0.4,0,-1)
p3.IsHidden = false
if not p3.nameplate then
p3.nameplate = Text()
p3.nameplate.Text = p3.Username
p3.nameplate:SetParent(p3.Head)
p3.nameplate.LocalRotation = Number3(0,math.pi,0)
p3.nameplate.LocalPosition = Number3(0,15,0)
end
p3.nameplate.IsHidden = false
end)
end
end
},
}
setmetatable(victoryPodium, victoryPodiumMetatable)
-- UTILS
-- sphere: {center, radius, sqRadius}
function sphereCollidesWithBox(sphere, box)
if sphere == nil then return false end
if sphere.radius == nil then return false end
if sphere.sqRadius == nil then sphere.sqRadius = sphere.radius * sphere.radius end
-- get box closest point to sphere center
local x = math.max(box.Min.X, math.min(sphere.center.X, box.Max.X));
local y = math.max(box.Min.Y, math.min(sphere.center.Y, box.Max.Y));
local z = math.max(box.Min.Z, math.min(sphere.center.Z, box.Max.Z));
-- see if closest point is within sphere
local sqDistance = (x - sphere.center.X) * (x - sphere.center.X) +
(y - sphere.center.Y) * (y - sphere.center.Y) +
(z - sphere.center.Z) * (z - sphere.center.Z)
return sqDistance < sphere.sqRadius;
end
-- generates & places grass at given position
placeGrass = function(pos)
if _bladeModel == nil then
_bladeModel = MutableShape()
_bladeModel:AddBlock(Color(52,131,63),0,0,0)
_bladeModel.Pivot = {0.5, 0, 0.5}
_bladeModel = Shape(_bladeModel)
end
local grass = Object()
for i = 1, math.random(1,4) do
local blade = Shape(_bladeModel)
blade.Physics = false
blade.CollisionGroups = nil
blade.CollidesWithGroups = nil
grass:AddChild(blade)
blade.Scale.Y = 2 + math.random() * 4
blade.LocalRotation = {math.random() * 0.9, math.random() * math.pi * 2, 0}
end
World:AddChild(grass)
grass.Position = pos
grass.Rotation = {0, math.random() * math.pi * 2, 0}
end
-- places small rock at given position
placeSmallRock = function(pos)
if _rockModel == nil then
_rockModel = MutableShape()
_rockModel:AddBlock(Color(170,170,170),0,0,0)
_rockModel.Pivot = {0.5, 0, 0.5}
end
local rock = Shape(_rockModel)
rock.Scale = {1 + math.random() * 3, 0.5 + math.random() * 1.5, 1 + math.random() * 3}
World:AddChild(rock)
rock.Position = pos
rock.Rotation = {0, math.random() * math.pi * 2, 0}
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment