From 79b9d303d56a5d2edf9b0065c442ecdd8726e1b4 Mon Sep 17 00:00:00 2001
From: Tristan
Date: Fri, 12 Apr 2024 10:37:10 +0200
Subject: [PATCH 1/2] WIP Cubzh module: talking NPCs + location update
---
cubzh/gigax/gigax.json | 5 +
cubzh/gigax/gigax.lua | 7 +
cubzh/map_gen.lua | 174 --
cubzh/scripts/hub_v3.lua | 1962 ++++++++++++++++++++++
cubzh/{cubzh.lua => scripts/imagine.lua} | 358 +---
cubzh/scripts/mod_test.lua | 484 ++++++
cubzh/scripts/mod_test_pathfinding.lua | 932 ++++++++++
cubzh/scripts/pathfinding.lua | 777 +++++++++
cubzh/scripts/procedural_maze.lua | 1089 ++++++++++++
9 files changed, 5304 insertions(+), 484 deletions(-)
create mode 100644 cubzh/gigax/gigax.json
create mode 100644 cubzh/gigax/gigax.lua
delete mode 100644 cubzh/map_gen.lua
create mode 100644 cubzh/scripts/hub_v3.lua
rename cubzh/{cubzh.lua => scripts/imagine.lua} (50%)
create mode 100644 cubzh/scripts/mod_test.lua
create mode 100644 cubzh/scripts/mod_test_pathfinding.lua
create mode 100644 cubzh/scripts/pathfinding.lua
create mode 100644 cubzh/scripts/procedural_maze.lua
diff --git a/cubzh/gigax/gigax.json b/cubzh/gigax/gigax.json
new file mode 100644
index 0000000..86d35e3
--- /dev/null
+++ b/cubzh/gigax/gigax.json
@@ -0,0 +1,5 @@
+{
+ "contributors": [
+ { "tristandeborde": 1.0 }
+ ]
+}
\ No newline at end of file
diff --git a/cubzh/gigax/gigax.lua b/cubzh/gigax/gigax.lua
new file mode 100644
index 0000000..17c0a55
--- /dev/null
+++ b/cubzh/gigax/gigax.lua
@@ -0,0 +1,7 @@
+--- This module allows you to integrate the Gigax library into your project.
+--- The library is used to generated realistic NPCs, quests, and more into your game.
+---@code
+--- gigax = require("github.com/gigaxgames/integrations/cubzh/gigax")
+
+mod = {}
+
diff --git a/cubzh/map_gen.lua b/cubzh/map_gen.lua
deleted file mode 100644
index 122ab3b..0000000
--- a/cubzh/map_gen.lua
+++ /dev/null
@@ -1,174 +0,0 @@
-Config = {
- Items = {
- "claire.torch",
- "voxels.gate"
- }
-}
-
-
--- The rest of the variables are unchanged
-local BROWN = Color(139, 69, 19)
-local CORRIDOR_LENGTH = 150
-local door = Shape(Items.voxels.gate)
-local CORRIDOR_WIDTH = door.Width
-local WALL_HEIGHT = 40
-local TORCH_SPACING = 40
-
--- Constants
-local NUMBER_OF_CORRIDORS = 2
-local corridors = {}
-local doors = {}
-local lastDoorIx = 2
-
-
-Client.OnStart = function()
- -- Add player to game
- World:AddChild(Player, true)
-
- -- Initialize the corridors and gates
- for i = 1, NUMBER_OF_CORRIDORS do
- corridors[i] = makeCorridor((i-1) * CORRIDOR_LENGTH, i)
- end
-
- -- Set up the UI
- ease = require "ease"
- ui = require "uikit"
- sfx = require "sfx"
- controls = require "controls"
- controls:setButtonIcon("action1", "⬆️")
-end
-
-Pointer.Click = function(pointerEvent)
- local impact = pointerEvent:CastRay(kMapAndItemsCollisionGroups)
- local object = impact.Object
-
- print(object.isDoor)
- if impact and impact.Distance < 500 then
- if object and object.isDoor then
- doorAction(object, "toggle")
- if object.number == lastDoorIx then -- Only if the last door is clicked
- -- Move the other corridor in front
- advanceCorridor()
- end
- end
- end
-end
-
-function advanceCorridor()
- local corridorToMoveIx = lastDoorIx % 2 + 1
- local corridorToMove = corridors[corridorToMoveIx]
- corridorToMove.Position = Number3(corridorToMove.Position.X + (CORRIDOR_LENGTH * 2), 0, 0)
- -- close the other door
- doorAction(doors[corridorToMoveIx], "close")
- -- Toggle the last door index for the next time
- lastDoorIx = corridorToMoveIx
-end
-
--- Function to create and set up the door
-function createDoorAtEndOfCorridor(offset, ix)
- local door = Shape(Items.voxels.gate)
- door.Position = Number3(offset + CORRIDOR_LENGTH, door.Height/2, door.Width)
- door.Rotation = Number3(0, math.pi / 2, 0) -- Adjusted to face the corridor
- door.Pivot = { 0, door.Height * 0.5, door.Depth * 0.5 }
- door.isDoor = true
- door.number = ix -- If you have more doors, increment this number for each
- door.closed = true
- door.rotClosed = math.pi / 2 -- The rotation when the door is closed
- doors[door.number] = door
- --door.collisionGroups = {DOOR_COLLISION_GROUP}
- World:AddChild(door)
- return door
-end
-
-function makeCorridor(offset, ix)
- print("Creating corridor " .. ix)
- local corridor = Object()
- World:AddChild(corridor)
- -- Add a door at the end of the corridor
- doors[ix] = createDoorAtEndOfCorridor(offset, ix)
- corridor:AddChild(doors [ix])
-
- -- Create floor
- local floor = MutableShape()
- for x = offset, offset + CORRIDOR_LENGTH - 1 do
- for z = 1, CORRIDOR_WIDTH - 2 do -- Subtracting 2 since we're building walls on both sides
- floor:AddBlock(BROWN, x, 0, z)
- end
- end
- corridor:AddChild(floor)
-
- -- Create walls
- local leftWall = MutableShape()
- local rightWall = MutableShape()
- for x = offset, offset + CORRIDOR_LENGTH - 1 do
- for y = 1, WALL_HEIGHT do
- -- Left wall
- leftWall:AddBlock(BROWN, x, y, 0)
-
- -- Right wall
- rightWall:AddBlock(BROWN, x, y, CORRIDOR_WIDTH - 1)
- end
- end
- corridor:AddChild(leftWall)
- corridor:AddChild(rightWall)
-
- -- Add torches alongside the walls, spaced evenly
- for x = offset, offset + CORRIDOR_LENGTH - 1, TORCH_SPACING do
- -- Left wall torches
- local leftTorch = addTorch(x, 10, 4) -- Position adjusted for the left wall
- -- Right wall torches
- local rightTorch = addTorch(x, 10, CORRIDOR_WIDTH - 5) -- Position adjusted for the right wall
- corridor:AddChild(leftTorch)
- corridor:AddChild(rightTorch)
- end
-
- return corridor
-end
-
-function addTorch(x, y, z)
- local torch = Shape(Items.claire.torch)
- torch.Position = Number3(x, y, z) -- Position adjusted for scale
- return torch
-end
-
--- door stuff
-
--- This function would be called when a monster is defeated
-function openDoor(doorNumber)
- local door = doors[doorNumber]
- if door and door.isDoor and door.closed then
- doorAction(door, "open")
- end
-end
-
--- Function to perform actions on doors
-function doorAction(object, action)
- print("Performing action on door nb " .. object.number .. ": " .. action)
- if action == "toggle" then
- object.closed = not object.closed
- elseif action == "close" then
- object.closed = true
- elseif action == "open" then
- object.closed = false
- end
-
- -- Play sound effects based on the action
- if object.closed then
- print("closing door")
- sfx("doorclose_1", object.Position, 0.5)
- else
- print("opening door")
- sfx("dooropen_1", object.Position, 0.5)
- end
-
- -- Set physics to trigger to allow for interaction, but not immediately solid
- object.Physics = PhysicsMode.Trigger
- object.colliders = 0
-
- -- Set the door's rotation based on whether it's open or closed
- if object.closed then
- object.Rotation = { 0, object.rotClosed, 0 }
- else
- object.Rotation = { 0, object.rotClosed + math.pi * 0.5, 0 }
- end
-end
\ No newline at end of file
diff --git a/cubzh/scripts/hub_v3.lua b/cubzh/scripts/hub_v3.lua
new file mode 100644
index 0000000..48c9cfc
--- /dev/null
+++ b/cubzh/scripts/hub_v3.lua
@@ -0,0 +1,1962 @@
+Dev.DisplayColliders = false
+local DEBUG_AMBIENCES = false
+local DEBUG_ITEMS = false
+local DEBUG_PET = false
+
+local SPAWN_POSITION = Number3(254, 80, 181) --315, 81, 138 --spawn point placed in world editor
+local SPAWN_ROTATION = Number3(0, math.pi * 0.08, 0)
+local TITLE_SCREEN_CAMERA_POSITION_IN_BLOCK = Number3(40, 20, 30)
+local MAP_SCALE = 6.0 -- var because could be overriden when loading map
+local GLIDER_BACKPACK = {
+ SCALE = 0.75,
+ ITEM_NAME = "voxels.glider_backpack",
+}
+
+local TIME_TO_AVATAR_CTA = 60 * 4 -- seconds
+local TIME_TO_FRIENDS_CTA = 60 * 7 -- seconds
+
+local TIME_CYCLE_DURATION = 480 -- 8 minutes
+local DAWN_DURATION = 0.05 -- percentages
+local DAY_DURATION = 0.55
+local DUSK_DURATION = 0.05
+local NIGHT_DURATION = 0.35
+
+local TIME_TO_MID_DAY = DAWN_DURATION + DAY_DURATION * 0.5
+local TIME_TO_NIGHTFALL = DAWN_DURATION + DAY_DURATION + DUSK_DURATION * 0.1
+local TIME_TO_DAYBREAK = DAWN_DURATION * 0.1
+local HOUR_HAND_OFFSET = -0.5 + 2 * TIME_TO_MID_DAY
+local MINUTE_HAND_OFFSET = 1
+
+local MAP_COLLISION_GROUPS = CollisionGroups(1)
+local MAP_COLLIDES_WITH_GROUPS = CollisionGroups()
+
+local PLAYER_COLLISION_GROUPS = CollisionGroups(2)
+local PLAYER_COLLIDES_WITH_GROUPS = CollisionGroups(1, 3, 4, 5) -- map + items + buildings + barriers
+
+local ITEM_COLLISION_GROUPS = CollisionGroups(3)
+local ITEM_COLLIDES_WITH_GROUPS = CollisionGroups(1, 3, 4) -- map + items + buildings
+
+local BUILDING_COLLISION_GROUPS = CollisionGroups(4)
+local BUILDING_COLLIDES_WITH_GROUPS = CollisionGroups()
+
+local BARRIER_COLLISION_GROUPS = CollisionGroups(5)
+local BARRIER_COLLIDES_WITH_GROUPS = CollisionGroups()
+
+local CAMERA_COLLIDES_WITH_GROUPS = CollisionGroups(1, 4) -- map + buildings
+
+local ITEM_BUILDING_AND_BARRIER_COLLISION_GROUPS = CollisionGroups(3, 4, 5)
+
+local DRAFT_COLLISION_GROUPS = CollisionGroups(6)
+
+local TRIGGER_AREA_SIZE = Number3(60, 30, 60)
+
+local LIGHT_COLOR = Color(244, 210, 87)
+-- local MAX_PLAYER_DISTANCE = 20
+-- local MAX_PLAYER_DISTANCE_SQR = MAX_PLAYER_DISTANCE * MAX_PLAYER_DISTANCE
+-- local TIME_TO_HATCH = 120
+
+Client.OnStart = function()
+ dialog = require("dialog")
+ dialog:setMaxWidth(400)
+ multi = require("multi")
+ textbubbles = require("textbubbles")
+ skills = require("object_skills")
+ controls = require("controls")
+ ambience = require("ambience")
+ collectible = require("collectible")
+ -- SFX & VFX
+ particles = require("particles")
+ walkSFX = require("walk_sfx")
+ sfx = require("sfx")
+ wingTrail = require("wingtrail")
+
+ require("social"):enablePlayerClickMenu()
+
+ -- HUD
+ textbubbles.displayPlayerChatBubbles = true
+ controls:setButtonIcon("action1", "⬆️")
+ dialog:setMaxWidth(400)
+
+ -- AMBIENCE
+ Clouds.Altitude = 60 * MAP_SCALE
+
+ if not DEBUG_AMBIENCES then
+ ambienceCycle = ambience:startCycle({
+ {
+ config = dawn,
+ duration = TIME_CYCLE_DURATION * DAWN_DURATION,
+ },
+ {
+ config = day,
+ duration = TIME_CYCLE_DURATION * DAY_DURATION,
+ fadeIn = TIME_CYCLE_DURATION * DAWN_DURATION * 0.5,
+ fadeOut = TIME_CYCLE_DURATION * DUSK_DURATION * 0.5,
+ },
+ {
+ config = dusk,
+ duration = TIME_CYCLE_DURATION * DUSK_DURATION,
+ },
+ {
+ config = night,
+ duration = TIME_CYCLE_DURATION * NIGHT_DURATION,
+ fadeIn = TIME_CYCLE_DURATION * DUSK_DURATION * 0.5,
+ fadeOut = TIME_CYCLE_DURATION * DAWN_DURATION * 0.5,
+ },
+ }, {
+ internalTick = false,
+ })
+ end
+
+ -- CONTROLS
+ -- Disabling controls until user is authenticated
+ Client.DirectionalPad = nil
+ Client.Action1 = nil
+ Client.Action1Release = nil
+ Pointer.Drag = nil
+
+ -- CAMERA
+ -- Set camera for pre-authentication state (rotating while title screen is shown)
+ cameraDefaultFOV = Camera.FOV
+ Camera:SetModeFree()
+ Camera.Position = TITLE_SCREEN_CAMERA_POSITION_IN_BLOCK * MAP_SCALE
+
+ Menu:OnAuthComplete(function()
+ Client.DirectionalPad = playerControls.directionalPad
+ Pointer.Drag = playerControls.pointerDrag
+ Client.Action1 = action1
+ Client.Action1Release = action1Release
+
+ initPlayer(Player)
+ dropPlayer(Player)
+
+ addCollectibles()
+ addTimers()
+
+ print(Player.Username .. " joined!")
+ end)
+
+ mapEffects()
+
+ -- SYNCED ACTIONS
+ multi:onAction("swingRight", function(sender)
+ sender:SwingRight()
+ end)
+
+ multi:onAction("equipGlider", function(sender)
+ local s = bundle.Shape(GLIDER_BACKPACK.ITEM_NAME)
+ s.Scale = GLIDER_BACKPACK.SCALE
+ sender:EquipBackpack(s)
+ end)
+ LocalEvent:Listen(LocalEvent.Name.LocalAvatarUpdate, function()
+ multi:action("updateAvatar")
+ require("api").getAvatar(Player.Username, function(_, _)
+ -- TODO: retry on error? maybe it should be done within getAvatar
+ end)
+ end)
+ multi:onAction("updateAvatar", function(sender)
+ avatar:get(sender.Username, sender.Avatar)
+ end)
+
+ LocalEvent:Listen(LocalEvent.Name.AvatarLoaded, function()
+ require("api").getAvatar(Player.Username, function(_, _)
+ -- TODO: retry on error? maybe it should be done within getAvatar
+ end)
+ end)
+
+ -- called when receiving information for distant object that are not linked
+ multi.linkRequest = function(name)
+ if _helpers.stringStartsWith(name, "p_") then
+ local playerID = math.floor(tonumber(_helpers.stringRemovePrefix(name, "p_")))
+ local p = Players[playerID]
+ if p ~= nil then
+ multi:unlink("g_" .. p.ID)
+
+ playerControls:walk(p)
+
+ multi:link(p, "p_" .. p.ID)
+ multi:link(p.Head, "ph_" .. p.ID)
+ if p.Parent == nil then
+ p:SetParent(World)
+ end
+ end
+ elseif _helpers.stringStartsWith(name, "g_") then -- glider
+ local playerID = math.floor(tonumber(_helpers.stringRemovePrefix(name, "g_")))
+ local p = Players[playerID]
+ if p ~= nil then
+ multi:unlink("p_" .. p.ID)
+ multi:unlink("ph_" .. p.ID)
+
+ local glider = playerControls:glide(p)
+
+ multi:link(glider, "g_" .. p.ID)
+ end
+ end
+ end
+end
+
+local SAVE_INTERVAL = 0.1
+local SAVE_AMOUNT = 10
+local savedPositions, savedRotations = {}, {}
+
+local unixMilli
+local currentTime
+
+local isNight = false
+local tInCycle
+local t = 0
+local t20 = 0
+local n
+Client.Tick = function(dt)
+ t20 = t20 + dt * 20
+
+ if not localPlayerShown then
+ t = t + dt -- * 0.2
+
+ local p = Number3(393, 36 + 120, 92)
+
+ p.Y = p.Y + (1 + math.sin(t * 0.6)) * 3
+
+ local rx = math.sin(t * 0.5) * math.rad(2)
+ local ry = math.rad(-22) + math.sin(t * 0.2) * math.rad(7)
+
+ Camera.Position = p
+ Camera.Rotation = Rotation(rx, ry, 0)
+ else
+ if Player.Position.Y < -200 then
+ dropPlayer(Player)
+ end
+ end
+
+ unixMilli = Time.UnixMilli() / 1000.0
+ currentTime = unixMilli % TIME_CYCLE_DURATION
+
+ tInCycle = currentTime / TIME_CYCLE_DURATION
+
+ if townhallHourHand ~= nil then
+ -- rotation 0 -> 9, -math.pi * 0.5 -> 12
+ -- mid-day -> 35% of TIME_CYCLE_DURATION
+ -- 0% of TIME_CYCLE_DURATION = -70% of 12h
+ -- Rotation(math.pi * (-0.5 + 2 * 0.7 - 2 * 2 * currentTime / TIME_CYCLE_DURATION), 0, 0)
+ townhallHourHand.LocalRotation = Rotation(math.pi * (HOUR_HAND_OFFSET - 4 * tInCycle), 0, 0)
+ end
+
+ if townhallMinuteHand ~= nil then
+ -- rotation 0 -> 12
+ -- Rotation(math.pi * (2 * 0.7 - 2 * 2 * 12 currentTime / TIME_CYCLE_DURATION), 0, 0)
+ townhallMinuteHand.LocalRotation = Rotation(math.pi * (MINUTE_HAND_OFFSET - 48 * tInCycle), 0, 0)
+ end
+
+ if isNight then
+ if tInCycle > TIME_TO_DAYBREAK and tInCycle < TIME_TO_NIGHTFALL then
+ isNight = false
+ LocalEvent:Send("Day")
+ end
+ else
+ if tInCycle > TIME_TO_NIGHTFALL or tInCycle < TIME_TO_DAYBREAK then
+ isNight = true
+ LocalEvent:Send("Night")
+ end
+ end
+
+ if not DEBUG_AMBIENCES then
+ ambienceCycle:setTime(currentTime)
+ end
+
+ if Player.pet ~= nil then
+ pet:followPlayer()
+ if pet.level < 0 and pet.remaining > 0 then
+ pet:updateHatchTimer()
+ end
+ end
+
+ n = (1 + math.sin(t20)) * 0.5 * 0.05
+ for _, speaker in ipairs(speakers) do
+ speaker.Scale = speaker.originalScale + n
+ end
+end
+
+Client.OnPlayerJoin = function(p)
+ if p == Player then
+ updateSync() -- Syncing for other players
+ return
+ end
+ initPlayer(p)
+ dropPlayer(p)
+ print(p.Username .. " joined!")
+end
+
+Client.OnPlayerLeave = function(p)
+ multi:unlink("g_" .. p.ID)
+ multi:unlink("ph_" .. p.ID)
+ multi:unlink("p_" .. p.ID)
+
+ if p ~= Player then
+ print(p.Username .. " just left!")
+ skills.removeStepClimbing(p)
+ walkSFX:unregister(p)
+ p:RemoveFromParent()
+ end
+end
+
+function setupBuilding(obj)
+ obj.CollisionGroups = BUILDING_COLLISION_GROUPS
+ obj.CollidesWithGroups = BUILDING_COLLIDES_WITH_GROUPS
+ obj.InnerTransparentFaces = false
+end
+
+speakers = {}
+Client.OnWorldObjectLoad = function(obj)
+ ease = require("ease")
+ hierarchyactions = require("hierarchyactions")
+ toast = require("ui_toast")
+ bundle = require("bundle")
+ avatar = require("avatar")
+
+ if obj.Name == "voxels.windmill" then
+ setupBuilding(obj)
+ obj.Wheel.Physics = PhysicsMode.Disabled
+ obj.Wheel.Tick = function(self, dt)
+ self:RotateLocal(-dt * 0.25, 0, 0)
+ end
+ elseif obj.Name == "voxels.drink_truck" then
+ hierarchyactions:applyToDescendants(obj, { includeRoot = true }, function(o)
+ setupBuilding(o)
+ end)
+ local lamps = obj:GetChild(8)
+ lamps.IsUnlit = false
+
+ local l = Light()
+ l:SetParent(lamps)
+ l.LocalPosition.X = 10
+ l.LocalPosition.Z = 30
+ l.Color = LIGHT_COLOR
+ l.Hardness = 0.7
+ l.On = false
+
+ LocalEvent:Listen("Night", function(_)
+ lamps.IsUnlit = true
+ l.On = true
+ end)
+ LocalEvent:Listen("Day", function(_)
+ lamps.IsUnlit = false
+ l.On = false
+ end)
+ elseif obj.Name == "voxels.home_1" then
+ setupBuilding(obj)
+ elseif obj.Name == "voxels.city_lamp" then
+ obj.Shadow = true
+ local light = obj:GetChild(1)
+ light.IsUnlit = false
+
+ local l = Light()
+ l:SetParent(light)
+ l.Color = LIGHT_COLOR
+ l.Hardness = 0.7
+ l.On = false
+
+ LocalEvent:Listen("Night", function(_)
+ light.IsUnlit = true
+ l.On = true
+ end)
+ LocalEvent:Listen("Day", function(_)
+ light.IsUnlit = false
+ l.On = false
+ end)
+ elseif obj.Name == "voxels.simple_lighthouse" then
+ setupBuilding(obj)
+ lightFire = obj:GetChild(1)
+ lightFire.IsUnlit = true
+ lightFire.IsHidden = true
+ lightRay = obj:GetChild(2)
+ lightRay.Physics = PhysicsMode.Disabled
+ lightRay.Scale.X = 10
+ lightRay.IsUnlit = true
+ lightRay.Palette[1].Color.A = 20
+ lightRay.IsHidden = true
+ -- LocalEvent:Listen("Night", function(_)
+ -- --lightFire.IsHidden = not data.isNight
+ -- --lightRay.IsHidden = not data.isNight
+ -- end)
+ elseif obj.Name == "voxels.townhall" then
+ setupBuilding(obj)
+
+ townhallHourHand = obj.Hour
+ townhallHourHand.Pivot = { 0.5, 0.5, 0.5 }
+
+ townhallMinuteHand = obj.Minute
+ townhallMinuteHand.Pivot = { 0.5, 0.5, 0.5 }
+ elseif obj.Name == "voxels.water_fountain" then
+ local w = obj:GetChild(1) -- water
+ w.Physics = PhysicsMode.Disabled
+ w.InnerTransparentFaces = false
+ local t1 = 0
+ w.Tick = function(self, dt)
+ t1 = t1 + dt
+ self.Scale.Y = 1 + (math.sin(t1) * 0.05)
+ end
+
+ local c = obj:GetChild(2) --floating cube
+ c.Physics = PhysicsMode.Disabled
+
+ local collider = c:Copy()
+ collider.IsHidden = true
+ collider.CollisionGroups = ITEM_COLLISION_GROUPS
+ collider.CollidesWithGroups = ITEM_COLLIDES_WITH_GROUPS
+ collider.Physics = PhysicsMode.Static
+ collider:SetParent(obj)
+ collider.LocalPosition = c.LocalPosition + { 0, 5, 0 }
+ collider.Rotation = Rotation(0, 0, 0)
+
+ local originY = c.LocalPosition.Y + 5
+ local t2 = 0
+ c.Tick = function(self, dt)
+ t2 = t2 + dt * 2
+ self.LocalPosition.Y = originY + 1 + math.sin(t2) * 0.5 * 4
+ self:RotateLocal(0, dt * 0.5, 0)
+ end
+ elseif obj.Name == "customavatar" then
+ obj = _helpers.replaceWithAvatar(obj, "claire")
+ obj.OnCollisionBegin = function(self, other)
+ if other ~= Player then
+ return
+ end
+ _helpers.lookAt(self.avatarContainer, other)
+ dialog:create(
+ "Hey! Edit your avatar in the Profile Menu, or use the changing room! 👕👖🥾",
+ self.avatar
+ )
+ Menu:HighlightProfile()
+ end
+ obj.OnCollisionEnd = function(self, other)
+ if other ~= Player then
+ return
+ end
+ _helpers.lookAt(self.avatarContainer, nil)
+ dialog:remove()
+ Menu:RemoveHighlight()
+ end
+ elseif obj.Name == "friend1" then
+ hierarchyactions:applyToDescendants(obj, { includeRoot = true }, function(o)
+ o.Shadow = true
+ end)
+ obj = _helpers.replaceWithAvatar(obj, "aduermael")
+ obj.OnCollisionBegin = function(self, other)
+ if other ~= Player then
+ return
+ end
+ _helpers.lookAt(self.avatarContainer, other)
+ end
+ obj.OnCollisionEnd = function(self, other)
+ if other ~= Player then
+ return
+ end
+ _helpers.lookAt(self.avatarContainer, nil)
+ end
+ elseif obj.Name == "friend2" then
+ hierarchyactions:applyToDescendants(obj, { includeRoot = true }, function(o)
+ o.Shadow = true
+ end)
+ obj = _helpers.replaceWithAvatar(obj, "gdevillele")
+ obj.OnCollisionBegin = function(self, other)
+ if other ~= Player then
+ return
+ end
+ _helpers.lookAt(self.avatarContainer, other)
+ dialog:create("Looking for friends? Add some through the Friends menu!", self.avatar)
+ Menu:HighlightFriends()
+ end
+ obj.OnCollisionEnd = function(self, other)
+ if other ~= Player then
+ return
+ end
+ _helpers.lookAt(self.avatarContainer, nil)
+ dialog:remove()
+ Menu:RemoveHighlight()
+ end
+ elseif obj.Name == "voxels.change_room" then
+ obj.trigger = _helpers.addTriggerArea(obj, obj.BoundingBox)
+ obj.trigger.OnCollisionBegin = function(self, other)
+ if other ~= Player then
+ return
+ end
+ self.toast = toast:create({
+ message = "Ready to customize your avatar? 👕",
+ center = false,
+ iconShape = bundle.Shape("voxels.change_room"),
+ duration = -1, -- negative duration means infinite
+ actionText = "Let's do this!",
+ action = function()
+ Menu:ShowProfileWearables()
+ end,
+ })
+ end
+ obj.trigger.OnCollisionEnd = function(self, other)
+ if other ~= Player then
+ return
+ end
+ if self.toast then
+ self.toast:remove()
+ self.toast = nil
+ end
+ end
+ elseif obj.Name == "voxels.dj_table" then
+ local music = bundle.Data("misc/hubmusic.ogg")
+ if music then
+ local as = AudioSource()
+ as.Sound = music
+ as.Loop = true
+ as.Volume = 1.0
+ as.Radius = 100
+ as.Spatialized = true
+ as:SetParent(obj)
+ as.LocalPosition = { 0, 0, 0 }
+ as:Play()
+ end
+ elseif obj.Name == "voxels.standing_speaker" then
+ local speaker = obj:GetChild(1)
+ local collider = Object()
+ collider.CollisionBox = speaker.CollisionBox
+ collider.Physics = PhysicsMode.Static
+ collider:SetParent(obj)
+ collider.LocalPosition = speaker.LocalPosition - speaker.Pivot
+ speaker.Physics = PhysicsMode.Disabled
+ speaker.originalScale = speaker.Scale:Copy()
+ table.insert(speakers, obj:GetChild(1))
+ elseif obj.Name == "voxels.speaker_left" or obj.Name == "voxels.speaker_right" then
+ local speaker = obj
+ local collider = Object()
+ collider.CollisionBox = Box(speaker.CollisionBox.Min - speaker.Pivot, speaker.CollisionBox.Max - speaker.Pivot)
+ collider.Physics = PhysicsMode.Static
+ collider:SetParent(World)
+ collider.Scale = speaker.Scale
+ collider.Position = speaker.Position
+ collider.Rotation = speaker.Rotation
+ speaker.Physics = PhysicsMode.Disabled
+ speaker.originalScale = speaker.Scale:Copy()
+ table.insert(speakers, speaker)
+ elseif obj.Name == "voxels.portal" then
+ obj.trigger = _helpers.addTriggerArea(obj, obj.BoundingBox)
+ obj.trigger.OnCollisionBegin = function(self, other)
+ if other ~= Player then
+ return
+ end
+ self.toast = toast:create({
+ message = "Ready to explore other Worlds? 🌎",
+ center = false,
+ iconShape = bundle.Shape("voxels.portal"),
+ duration = -1, -- negative duration means infinite
+ actionText = "Let's go!",
+ action = function()
+ Menu:ShowWorlds()
+ end,
+ })
+ end
+ obj.trigger.OnCollisionEnd = function(self, other)
+ if other ~= Player then
+ return
+ end
+ if self.toast then
+ self.toast:remove()
+ self.toast = nil
+ end
+ end
+ local animatePortal = function(portal)
+ local kANIMATION_SPEED = 1
+ local kOFFSET_Y = 16
+
+ local ringsParent = portal:GetChild(2)
+ hierarchyactions:applyToDescendants(ringsParent, { includeRoot = true }, function(o)
+ o.Physics = PhysicsMode.Trigger
+ o.IsUnlit = true
+ end)
+
+ ringsParent.OnCollisionBegin = function(_, other)
+ if other.CollisionGroups == Player.CollisionGroups then
+ kANIMATION_SPEED = 5
+ end
+ end
+ ringsParent.OnCollisionEnd = function(_, other)
+ if other.CollisionGroups == Player.CollisionGroups then
+ kANIMATION_SPEED = 1
+ end
+ end
+ local rings, start, range, speed, timer = {}, {}, {}, {}, {}
+
+ for i = 1, ringsParent.ChildrenCount do
+ rings[i] = ringsParent:GetChild(i)
+ rings[i].Scale = rings[i].Scale * (1 - 0.01 * i) --Clipping OTP
+ start[i] = math.random(-4, 4)
+ range[i] = math.random(4, 8)
+ speed[i] = math.random(1, 2) * 0.5
+ timer[i] = math.random(1, 5)
+ rings[i].Tick = function(self, dt)
+ timer[i] = timer[i] + speed[i] * dt * kANIMATION_SPEED
+ self.LocalPosition.Y = kOFFSET_Y + start[i] + math.sin(timer[i]) * range[i]
+ end
+ end
+ end
+ animatePortal(obj)
+ elseif obj.Name == "pet_npc" then
+ obj = _helpers.replaceWithAvatar(obj, "voxels")
+ obj.OnCollisionBegin = function(self, other)
+ if other ~= Player then
+ return
+ end
+ _helpers.lookAt(self.avatarContainer, other)
+ if DEBUG_PET then
+ pet:dialogTree(self.avatar)
+ else
+ pet:dialogTreeTeaser(self.avatar)
+ end
+ end
+ obj.OnCollisionEnd = function(self, other)
+ if other ~= Player then
+ return
+ end
+ _helpers.lookAt(self.avatarContainer, nil)
+ dialog:remove()
+ end
+ elseif string.find(obj.fullname, "discord_sign") then
+ obj.trigger = _helpers.addTriggerArea(obj)
+ obj.trigger.OnCollisionBegin = function(self, other)
+ if other ~= Player then
+ return
+ end
+ local icon
+ pcall(function()
+ icon = bundle.Shape("aduermael.discord_logo")
+ end)
+ self.toast = toast:create({
+ message = "Might wanna join Cubzh's Discord to meet other players & builders?",
+ center = false,
+ iconShape = icon,
+ duration = -1, -- negative duration means infinite
+ actionText = "Sure!",
+ action = function()
+ URL:Open("https://discord.gg/cubzh")
+ end,
+ })
+ end
+ obj.trigger.OnCollisionEnd = function(self, other)
+ if other ~= Player then
+ return
+ end
+ if self.toast then
+ self.toast:remove()
+ self.toast = nil
+ end
+ end
+ elseif obj.Name == "pet_bird" or obj.Name == "pet_gator" or obj.Name == "pet_ram" then
+ obj.initialForward = obj.Forward:Copy()
+ obj.Physics = PhysicsMode.Disabled
+ obj.trigger = _helpers.addTriggerArea(obj)
+ obj.trigger.OnCollisionBegin = function(_, other)
+ if other ~= Player then
+ return
+ end
+ _helpers.lookAt(obj, other)
+ dialog:create("❤️❤️❤️", obj)
+ end
+ obj.trigger.OnCollisionEnd = function(_, other)
+ if other ~= Player then
+ return
+ end
+ _helpers.lookAt(obj, nil)
+ dialog:remove()
+ end
+ elseif obj.Name == "pet_spawner" then
+ obj.Physics = PhysicsMode.Disabled
+ for i = 1, 4 do -- 1 : player, 2 : gator, 3 : npc, 4 : fence
+ obj:GetChild(i).Physics = PhysicsMode.Disabled
+ obj.Palette[1].Color.A = 20
+ end
+ end
+
+ if obj.fullname ~= nil then
+ if
+ string.find(obj.fullname, "hedge")
+ or string.find(obj.fullname, "hay_bail")
+ or string.find(obj.fullname, "palm_tree")
+ or string.find(obj.fullname, "apple_tree")
+ or string.find(obj.fullname, "carrot_1")
+ or string.find(obj.fullname, "turnip")
+ or string.find(obj.fullname, "training_dummy")
+ or string.find(obj.fullname, "farmhat")
+ or string.find(obj.fullname, "broken_bridge_side_1")
+ or string.find(obj.fullname, "clothes_rack")
+ or string.find(obj.fullname, "city_lamp")
+ or string.find(obj.fullname, "solo_computer")
+ or string.find(obj.fullname, "no_fun_sign")
+ or string.find(obj.fullname, "soon")
+ or string.find(obj.fullname, "brick")
+ or string.find(obj.fullname, "small_water_pipe")
+ or string.find(obj.fullname, "pipe_tank")
+ or string.find(obj.fullname, "beach_umbrella")
+ or string.find(obj.fullname, "beach_chair")
+ then
+ hierarchyactions:applyToDescendants(obj, { includeRoot = true }, function(o)
+ o.Physics = PhysicsMode.Static
+ end)
+ elseif string.find(obj.fullname, "walking_plank") then
+ hierarchyactions:applyToDescendants(obj, { includeRoot = false }, function(o)
+ o.Physics = PhysicsMode.Disabled
+ end)
+ obj.Physics = PhysicsMode.Static
+ elseif
+ string.find(obj.fullname, "fence_gate")
+ or string.find(obj.fullname, "white_fence")
+ or string.find(obj.fullname, "rustic_fence")
+ then
+ hierarchyactions:applyToDescendants(obj, { includeRoot = true }, function(o)
+ o.Physics = PhysicsMode.Static
+ o.CollisionGroups = BARRIER_COLLISION_GROUPS
+ o.CollidesWithGroups = BARRIER_COLLIDES_WITH_GROUPS
+ end)
+ elseif string.find(obj.fullname, "beach_barrier") then
+ -- fix beach barries alignments (for better collisions)
+ if obj.fullname == "voxels.beach_barrier_2" then
+ hierarchyactions:applyToDescendants(obj, { includeRoot = true }, function(o)
+ o.Physics = PhysicsMode.StaticPerBlock
+ o.CollisionGroups = BARRIER_COLLISION_GROUPS
+ o.CollidesWithGroups = BARRIER_COLLIDES_WITH_GROUPS
+ end)
+ else
+ hierarchyactions:applyToDescendants(obj, { includeRoot = true }, function(o)
+ o.Physics = PhysicsMode.Static
+ o.CollisionGroups = BARRIER_COLLISION_GROUPS
+ o.CollidesWithGroups = BARRIER_COLLIDES_WITH_GROUPS
+ end)
+
+ -- trying to allign collision boxes to smoothly slide
+ -- along tiled barries.
+ -- doesn't work realy well so far
+
+ local p = obj.Position:Copy()
+ p.X = math.floor(p.X + 0.5)
+ p.Y = math.floor(p.Y + 0.5)
+ p.Z = math.floor(p.Z + 0.5)
+ local diff = obj.Position - p
+
+ obj.CollisionBox = Box(
+ Number3(
+ math.floor(obj.CollisionBox.Max.X + diff.X + 0.5),
+ 14,
+ math.floor(obj.CollisionBox.Max.Z + diff.Z + 0.5)
+ ),
+ Number3(
+ math.floor(obj.CollisionBox.Min.X + diff.X + 0.5),
+ math.floor(obj.CollisionBox.Min.Y + diff.Y + 0.5),
+ math.floor(obj.CollisionBox.Min.Z + diff.Z + 0.5)
+ )
+ )
+ end
+
+ -- obj.Position.X = math.floor(obj.Position.X + 0.5)
+ -- obj.Position.Y = math.floor(obj.Position.Y + 0.5)
+ -- obj.Position.Z = math.floor(obj.Position.Z + 0.5)
+ elseif string.find(obj.fullname, "plank_") then -- items that are "part of the map"
+ hierarchyactions:applyToDescendants(obj, { includeRoot = false }, function(o)
+ o.Physics = PhysicsMode.Disabled
+ end)
+ obj.Physics = PhysicsMode.Static
+ obj.CollisionGroups = MAP_COLLISION_GROUPS
+ obj.CollidesWithGroups = MAP_COLLIDES_WITH_GROUPS
+ elseif
+ string.find(obj.fullname, "shell_1")
+ or string.find(obj.fullname, "shell_2")
+ or string.find(obj.fullname, "shell_3")
+ or string.find(obj.fullname, "sand_1")
+ or string.find(obj.fullname, "sand_2")
+ or string.find(obj.fullname, "sand_3")
+ or string.find(obj.fullname, "sand_4")
+ or string.find(obj.fullname, "lily_pads")
+ or string.find(obj.fullname, "vines")
+ or string.find(obj.fullname, "moss")
+ then
+ hierarchyactions:applyToDescendants(obj, { includeRoot = true }, function(o)
+ o.Physics = PhysicsMode.Disabled
+ end)
+ elseif
+ string.find(obj.fullname, "tuft")
+ or string.find(obj.fullname, "grass")
+ or string.find(obj.fullname, "dirt")
+ then
+ hierarchyactions:applyToDescendants(obj, { includeRoot = true }, function(o)
+ o.Physics = PhysicsMode.Disabled
+ end)
+ if string.find(obj.Name, "_n") then
+ return
+ end
+ obj.Position.Y = obj.Position.Y - 0.40 * MAP_SCALE
+ elseif string.find(obj.fullname, "stone") or string.find(obj.fullname, "log") then
+ hierarchyactions:applyToDescendants(obj, { includeRoot = true }, function(o)
+ o.Physics = PhysicsMode.Static
+ end)
+ if string.find(obj.Name, "_n") then
+ return
+ end
+ obj.Position.Y = obj.Position.Y - 0.40 * MAP_SCALE
+ end
+ end
+end
+
+Pointer.Click = function(pe)
+ Player:SwingRight()
+ multi:action("swingRight")
+ dialog:complete()
+
+ if DEBUG_ITEMS then
+ local impact = pe:CastRay(ITEM_BUILDING_AND_BARRIER_COLLISION_GROUPS)
+ if impact ~= nil then
+ if impact.Object ~= nil then
+ local o = impact.Object
+
+ while o.Parent ~= World do
+ o = o.Parent
+ end
+
+ if o ~= nil then
+ if o.fullname ~= nil then
+ print(o.fullname, "(copied)")
+ Dev:CopyToClipboard(o.fullname)
+ elseif o.Name ~= nil then
+ print(o.Name, "(copied)")
+ Dev:CopyToClipboard(o.fullname)
+ end
+ end
+ end
+ end
+ end
+end
+
+local JUMP_VELOCITY = 82
+local MAX_AIR_JUMP_VELOCITY = 85
+initPlayer = function(p)
+ if p == Player then -- Player properties for local simulation
+ require("camera_modes"):setThirdPerson({
+ rigidity = 0.4,
+ target = p,
+ collidesWithGroups = CAMERA_COLLIDES_WITH_GROUPS,
+ })
+
+ jumpParticles = particles:newEmitter({
+ life = function()
+ return 0.3
+ end,
+ velocity = function()
+ local v = Number3(15 + math.random() * 10, 0, 0)
+ v:Rotate(0, math.random() * math.pi * 2, 0)
+ return v
+ end,
+ color = function()
+ return Color.White
+ end,
+ acceleration = function()
+ return -Config.ConstantAcceleration
+ end,
+ collidesWithGroups = function()
+ return {}
+ end,
+ })
+ collectParticles = particles:newEmitter({
+ life = function()
+ return 1.0
+ end,
+ velocity = function()
+ local v = Number3(20 + math.random() * 10, 0, 0)
+ v:Rotate(0, math.random() * math.pi * 2, 0)
+ v.Y = 30 + math.random() * 20
+ return v
+ end,
+ color = function()
+ return Color.White
+ end,
+ scale = function()
+ return 0.5
+ end,
+ collidesWithGroups = function()
+ return {}
+ end,
+ })
+ local spawnJumpParticles = function(o)
+ jumpParticles.Position = o.Position
+ jumpParticles:spawn(10)
+ sfx("walk_concrete_2", { Position = o.Position, Volume = 0.2 })
+ end
+ skills.addStepClimbing(Player, {
+ mapScale = MAP_SCALE,
+ collisionGroups = Map.CollisionGroups + ITEM_COLLISION_GROUPS + BUILDING_COLLISION_GROUPS,
+ })
+ skills.addJump(Player, {
+ maxGroundDistance = 1.0,
+ airJumps = 1,
+ jumpVelocity = JUMP_VELOCITY,
+ maxAirJumpVelocity = MAX_AIR_JUMP_VELOCITY,
+ onJump = spawnJumpParticles,
+ onAirJump = spawnJumpParticles,
+ })
+ localPlayerShown = true
+
+ -- Timer to save recent on ground positions
+ local saveIdx = 1
+ Timer(SAVE_INTERVAL, true, function()
+ if Player.IsOnGround then
+ savedPositions[saveIdx] = Player.Position:Copy() + { 0, MAP_SCALE, 0 } -- adding a one block Y offset on the respawn
+ savedRotations[saveIdx] = Player.Rotation:Copy()
+ saveIdx = saveIdx + 1
+ if saveIdx > SAVE_AMOUNT then
+ saveIdx = 1
+ end
+ end
+ end)
+
+ p.Head:AddChild(AudioListener) -- Adding an audio listener to the player
+ end
+
+ World:AddChild(p) -- Adding the player to the world
+ p.Physics = PhysicsMode.Dynamic
+ p.CollisionGroups = PLAYER_COLLISION_GROUPS
+ p.CollidesWithGroups = PLAYER_COLLIDES_WITH_GROUPS
+ addPlayerAnimations(p) -- Adding animations
+ walkSFX:register(p) -- Adding step sounds
+ playerControls:walk(p) -- Setting the default control to walk
+end
+
+function dropPlayer(p)
+ playerControls:walk(p)
+ p.Velocity, p.Motion = { 0, 0, 0 }, { 0, 0, 0 }
+
+ if p == Player then
+ -- cycling through saved positions to find a valid one
+ for k, v in ipairs(savedPositions) do
+ local ray = Ray(v, Number3.Down)
+ if ray:Cast(Map) ~= nil then
+ p.Position = v
+ p.Rotation = savedRotations[k]
+ return
+ end
+ end
+ end
+
+ p.Position = SPAWN_POSITION + Number3(math.random(-6, 6), 0, math.random(-6, 6))
+ p.Rotation = SPAWN_ROTATION + Number3(0, math.random(-1, 1) * math.pi * 0.08, 0)
+end
+
+local HOLDING_TIME = 0.6 -- time to trigger action when holding button pressed
+local holdTimer = nil
+function action1()
+ playerControls:walk(Player)
+ skills.jump(Player)
+
+ holdTimer = Timer(HOLDING_TIME, function()
+ holdTimer = nil
+ if backEquipment == "" then
+ return
+ end
+ if backEquipment == "glider" then
+ if gliderUsageToast ~= nil then
+ gliderUsageToast:remove()
+ gliderUsageToast = nil
+ end
+ playerControls:glide(Player)
+ end
+ end)
+
+ if DEBUG_AMBIENCES then
+ nextAmbience()
+ end
+end
+
+function action1Release()
+ if holdTimer ~= nil then
+ holdTimer:Cancel()
+ end
+end
+
+function mapEffects()
+ local sea = Map:GetChild(1)
+ sea.Physics = PhysicsMode.TriggerPerBlock -- let the player go through
+ sea.CollisionGroups = {}
+ sea.CollidesWithGroups = { 2 }
+ sea.InnerTransparentFaces = false -- no inner surfaces for the renderer
+ sea.LocalPosition = { 0, 1, 0 } -- placement
+ local t = 0
+ sea.Tick = function(self, dt)
+ t = t + dt
+ self.Scale.Y = 1 + (math.sin(t) * 0.05)
+ end
+ sea.OnCollisionBegin = function(_, other)
+ sfx("water_impact_" .. math.random(1, 3), { Position = other.Position, Volume = 0.5, Pitch = 1.0 })
+ end
+
+ local grass = Map:GetChild(2)
+ grass.Physics = PhysicsMode.Disabled
+ grass.CollisionGroups = { 1 }
+ grass.Scale = 0.999
+ grass.LocalPosition = { 5, 12.1, 27 }
+end
+
+addTimers = function()
+ Timer(TIME_TO_AVATAR_CTA, function()
+ require("api").getAvatar(Player.Username, function(err, data)
+ if not err then
+ if
+ data.hair ~= nil
+ or data.jacket ~= "official.jacket"
+ or data.pants ~= "official.pants"
+ or data.boots ~= "official.boots"
+ then
+ return -- If the player has at least one customized equipment, don't send toastMsg
+ else
+ toast:create({
+ message = "You can customize your avatar anytime!",
+ center = false,
+ iconShape = bundle.Shape("voxels.change_room"),
+ duration = -1,
+ closeButton = true,
+ actionText = "Ok!",
+ action = function(self)
+ Menu:ShowProfileWearables()
+ self:remove()
+ end,
+ })
+ end
+ end
+ end)
+ end)
+
+ Timer(TIME_TO_FRIENDS_CTA, function()
+ require("api"):getFriendCount(function(ok, count)
+ if ok then
+ if count > 0 then
+ return -- If the player already has friends, don't toastMsg
+ else
+ toast:create({
+ message = "Add friends and play together!",
+ center = false,
+ iconShape = bundle.Shape("voxels.friend_icon"),
+ duration = -1,
+ closeButton = true,
+ actionText = "Ok!",
+ action = function(self)
+ Menu:ShowFriends()
+ self:remove()
+ end,
+ })
+ end
+ end
+ end)
+ end)
+end
+
+local _ambiences
+local _nextAmbience
+function nextAmbience()
+ if _ambiences == nil then
+ _ambiences = { dawn, day, dusk, night }
+ _nextAmbience = 1
+ end
+ local a = _ambiences[_nextAmbience]
+ _ambiences:set(a)
+
+ _nextAmbience = _nextAmbience + 1
+ if _nextAmbience > #_ambiences then
+ _nextAmbience = 1
+ end
+end
+-- HELPERS
+
+_helpers = {}
+_helpers.stringStartsWith = function(str, prefix)
+ return string.sub(str, 1, string.len(prefix)) == prefix
+end
+
+_helpers.stringRemovePrefix = function(str, prefix)
+ if string.sub(str, 1, string.len(prefix)) == prefix then
+ return string.sub(str, string.len(prefix) + 1)
+ else
+ return str
+ end
+end
+
+_helpers.replaceWithAvatar = function(obj, name)
+ local o = Object()
+ o:SetParent(World)
+ o.Position = obj.Position
+ o.Scale = obj.Scale
+ o.Physics = PhysicsMode.Trigger
+
+ o.CollisionBox = Box({
+ -TRIGGER_AREA_SIZE.Width * 0.5,
+ math.min(-TRIGGER_AREA_SIZE.Height, o.CollisionBox.Min.Y),
+ -TRIGGER_AREA_SIZE.Depth * 0.5,
+ }, {
+ TRIGGER_AREA_SIZE.Width * 0.5,
+ math.max(TRIGGER_AREA_SIZE.Height, o.CollisionBox.Max.Y),
+ TRIGGER_AREA_SIZE.Depth * 0.5,
+ })
+ o.CollidesWithGroups = { 2 }
+ o.CollisionGroups = {}
+
+ local container = Object()
+ container.Rotation = obj.Rotation
+ container.initialRotation = obj.Rotation:Copy()
+ container.initialForward = obj.Forward:Copy()
+ container:SetParent(o)
+ o.avatarContainer = container
+
+ local newObj = avatar:get(name)
+ o.avatar = newObj
+ newObj:SetParent(o.avatarContainer)
+
+ obj:RemoveFromParent()
+ return o
+end
+
+_helpers.addTriggerArea = function(obj, box, offset)
+ local o = Object()
+ o:SetParent(World)
+ o.Scale = obj.Scale
+ o.Physics = PhysicsMode.Trigger
+ o.CollidesWithGroups = { 2 }
+ o.CollisionGroups = {}
+ o.CollisionBox = box ~= nil and box
+ or (
+ Box({
+ -TRIGGER_AREA_SIZE.Width * 0.5,
+ math.min(-TRIGGER_AREA_SIZE.Height, o.CollisionBox.Min.Y),
+ -TRIGGER_AREA_SIZE.Depth * 0.5,
+ }, {
+ TRIGGER_AREA_SIZE.Width * 0.5,
+ math.max(TRIGGER_AREA_SIZE.Height, o.CollisionBox.Max.Y),
+ TRIGGER_AREA_SIZE.Depth * 0.5,
+ })
+ )
+ o.Position = offset ~= nil and (obj.Position + offset) or (obj.Position - obj.Pivot * 0.5)
+ return o
+end
+
+_helpers.lookAt = function(obj, target)
+ if not target then
+ ease:linear(obj, 0.1).Forward = obj.initialForward
+ obj.Tick = nil
+ return
+ end
+ obj.Tick = function(self, _)
+ _helpers.lookAtHorizontal(self, target)
+ end
+end
+
+_helpers.lookAtHorizontal = function(o1, o2)
+ local n3_1 = Number3.Zero
+ local n3_2 = Number3.Zero
+ n3_1:Set(o1.Position.X, 0, o1.Position.Z)
+ n3_2:Set(o2.Position.X, 0, o2.Position.Z)
+ ease:linear(o1, 0.1).Forward = n3_2 - n3_1
+end
+
+_helpers.contains = function(t, v)
+ for _, value in ipairs(t) do
+ if value == v then
+ return true
+ end
+ end
+ return false
+end
+
+-- MODULE : DAY NIGHT CYCLE
+
+dawn = {
+ sky = {
+ skyColor = Color(246, 40, 140),
+ horizonColor = Color(239, 147, 17),
+ abyssColor = Color(0, 77, 172),
+ lightColor = Color(177, 111, 55),
+ lightIntensity = 0.510000,
+ },
+ fog = {
+ color = Color(74, 15, 6),
+ near = 300,
+ far = 700,
+ lightAbsorbtion = 0.400000,
+ },
+ sun = {
+ color = Color(172, 71, 71),
+ intensity = 1.000000,
+ rotation = Rotation(math.rad(30), math.rad(-60), 0),
+ },
+ ambient = {
+ skyLightFactor = 0.100000,
+ dirLightFactor = 0.200000,
+ },
+}
+
+day = {
+ sky = {
+ skyColor = Color(0, 103, 255),
+ horizonColor = Color(0, 248, 248),
+ abyssColor = Color(202, 255, 245),
+ lightColor = Color(199, 174, 148),
+ lightIntensity = 0.600000,
+ },
+ fog = {
+ color = Color(20, 159, 204),
+ near = 300,
+ far = 700,
+ lightAbsorbtion = 0.400000,
+ },
+ sun = {
+ color = Color(199, 195, 73),
+ intensity = 1.000000,
+ rotation = Rotation(math.rad(50), math.rad(-30), 0),
+ },
+ ambient = {
+ skyLightFactor = 0.100000,
+ dirLightFactor = 0.200000,
+ },
+}
+
+dusk = {
+ sky = {
+ skyColor = Color(0, 9, 192),
+ horizonColor = Color(227, 43, 70),
+ abyssColor = Color(238, 168, 0),
+ lightColor = Color(180, 51, 180),
+ lightIntensity = 0.510000,
+ },
+ fog = {
+ color = Color(10, 15, 83),
+ near = 300,
+ far = 700,
+ lightAbsorbtion = 0.400000,
+ },
+ sun = {
+ color = Color(91, 28, 164),
+ intensity = 1.000000,
+ rotation = Rotation(math.rad(30), math.rad(60), 0),
+ },
+ ambient = {
+ skyLightFactor = 0.100000,
+ dirLightFactor = 0.210000,
+ },
+}
+
+night = {
+ sky = {
+ skyColor = Color(1, 3, 50),
+ horizonColor = Color(70, 64, 146),
+ abyssColor = Color(64, 117, 190),
+ lightColor = Color(69, 80, 181),
+ lightIntensity = 0.600000,
+ },
+ fog = {
+ color = Color(89, 28, 112),
+ near = 310,
+ far = 700,
+ lightAbsorbtion = 0.310000,
+ },
+ sun = {
+ color = Color(107, 65, 200),
+ intensity = 1.000000,
+ rotation = Rotation(1.061161, 3.089219, 0.000000),
+ },
+ ambient = {
+ skyLightFactor = 0.100000,
+ dirLightFactor = 0.200000,
+ },
+}
+
+-- MODULE : PLAYER CONTROLS
+
+function updateSync()
+ local p = Player
+ local pID = p.ID
+
+ multi:unlink("g_" .. pID)
+ multi:unlink("p_" .. pID)
+ multi:unlink("ph_" .. pID)
+
+ if Client.Connected then
+ local playerControlID = playerControls:getPlayerID(p)
+ local vehicle = playerControls.vehicles[playerControlID]
+ if vehicle then
+ if vehicle.type == "glider" then
+ -- sync vehicleRoll child object,
+ -- it contains all needed information
+ multi:sync(vehicle.roll, "g_" .. pID, {
+ keys = { "Velocity", "Position", "Rotation" },
+ triggers = { "LocalRotation", "Velocity" },
+ })
+ end
+ else
+ multi:sync(p, "p_" .. pID, {
+ keys = { "Motion", "Velocity", "Position", "Rotation.Y" },
+ triggers = { "LocalRotation", "Rotation", "Motion", "Position", "Velocity" },
+ })
+ multi:sync(
+ p.Head,
+ "ph_" .. pID,
+ { keys = { "LocalRotation.X" }, triggers = { "LocalRotation", "Rotation" } }
+ )
+ end
+ end
+end
+
+function addPlayerAnimations(player)
+ local animLiftArms = Animation("LiftArms", { speed = 5, loops = 1, removeWhenDone = false, priority = 255 })
+ local liftRightArm = {
+ { time = 0.0, rotation = { 0, 0, -1.0472 } },
+ { time = 1.0, rotation = { 0, 0, math.rad(30) } },
+ }
+ local liftRightHand = {
+ { time = 0.0, rotation = { 0, -0.392699, 0 } },
+ { time = 1.0, rotation = { math.rad(-180), 0, math.rad(-30) } },
+ }
+ local liftLeftArm = {
+ { time = 0.0, rotation = { 0, 0, 1.0472 } },
+ { time = 1.0, rotation = { 0, 0, math.rad(-30) } },
+ }
+ local liftLeftHand = {
+ { time = 0.0, rotation = { 0, -0.392699, 0 } },
+ { time = 1.0, rotation = { math.rad(-180), 0, math.rad(30) } },
+ }
+ local animLiftRightConfig = {
+ RightArm = liftRightArm,
+ RightHand = liftRightHand,
+ LeftArm = liftLeftArm,
+ LeftHand = liftLeftHand,
+ }
+ for name, v in pairs(animLiftRightConfig) do
+ for _, frame in ipairs(v) do
+ animLiftArms:AddFrameInGroup(name, frame.time, { position = frame.position, rotation = frame.rotation })
+ animLiftArms:Bind(
+ name,
+ (name == "Body" and not player.Avatar[name]) and player.Avatar or player.Avatar[name]
+ )
+ end
+ end
+ player.Animations.LiftArms = animLiftArms
+end
+
+playerControls = {
+ shapeCache = {},
+ vehicles = {}, -- vehicles, indexed by player ID
+ current = {}, -- control names, indexed by player ID
+ onDrag = nil,
+ dirPad = nil,
+}
+
+playerControls.getPlayerID = function(_, player)
+ if player == Player then
+ -- using "local" because the local player ID may change while still maintaining active controls
+ return "local"
+ else
+ return player.ID
+ end
+end
+
+playerControls.pointerDrag = function(pe)
+ if playerControls.onDrag ~= nil then
+ playerControls.onDrag(pe)
+ end
+end
+
+playerControls.directionalPad = function(x, y)
+ if playerControls.dirPad ~= nil then
+ playerControls.dirPad(x, y)
+ end
+end
+
+playerControls.getShape = function(self, shapeName)
+ if self.shapeCache[shapeName] == nil then
+ self.shapeCache[shapeName] = bundle.Shape(shapeName)
+ end
+ return Shape(self.shapeCache[shapeName], { includeChildren = true })
+end
+
+playerControls.exitVehicle = function(self, player)
+ local pID = self:getPlayerID(player)
+
+ local vehicle = self.vehicles[pID]
+
+ if vehicle == nil then
+ return
+ end
+
+ if player.Animations.LiftArms then
+ player.Animations.LiftArms:Stop()
+ end
+
+ vehicle.Tick = nil
+
+ -- NOTE: each vehicle should implement an onExit
+ -- callback instead of harcoding everything here.
+
+ if vehicle.wingTrails then
+ for _, t in ipairs(vehicle.wingTrails) do
+ wingTrail:remove(t)
+ end
+ vehicle.wingTrails = nil
+ end
+
+ player:SetParent(World, true)
+ player.Rotation = { 0, vehicle.Rotation.Y, 0 }
+ player.Position = vehicle.Position + { 0, -27, 0 }
+
+ player.Head.LocalRotation = { 0, 0, 0 }
+ player.Physics = PhysicsMode.Dynamic
+ player.Scale = 0.5
+ player.Velocity = Number3.Zero
+
+ if player == Player then
+ require("camera_modes"):setThirdPerson({
+ rigidity = 0.4,
+ target = player,
+ collidesWithGroups = CAMERA_COLLIDES_WITH_GROUPS,
+ })
+ Camera.FOV = cameraDefaultFOV
+ end
+
+ vehicle.Physics = PhysicsMode.Disabled
+
+ if vehicle.model ~= nil then
+ vehicle.model.Physics = PhysicsMode.Disabled
+ vehicle.model:SetParent(player, true)
+ vehicle:RemoveFromParent()
+ ease:linear(vehicle.model, 0.3, {
+ onDone = function(o)
+ o:RemoveFromParent()
+ end,
+ }).Scale =
+ Number3.Zero
+ else
+ vehicle:SetParent(World, true)
+ ease:linear(vehicle, 0.3, {
+ onDone = function(o)
+ o:RemoveFromParent()
+ end,
+ }).Scale = Number3.Zero
+ end
+
+ self.vehicles[pID] = nil
+end
+
+playerControls.walk = function(self, player)
+ local pID = self:getPlayerID(player)
+
+ if self.current[pID] == "walk" then
+ return -- already walking
+ end
+ self.current[pID] = "walk"
+
+ self:exitVehicle(player)
+
+ player:SetParent(World, true)
+
+ if player == Player then
+ self.onDrag = function(pe)
+ Player.LocalRotation = Rotation(0, pe.DX * 0.01, 0) * Player.LocalRotation
+ Player.Head.LocalRotation = Rotation(-pe.DY * 0.01, 0, 0) * Player.Head.LocalRotation
+ local dpad = require("controls").DirectionalPadValues
+ Player.Motion = (Player.Forward * dpad.Y + Player.Right * dpad.X) * 50
+ end
+ self.dirPad = function(x, y)
+ Player.Motion = (Player.Forward * y + Player.Right * x) * 50
+ end
+ updateSync()
+ end
+end
+
+local GLIDER_MAX_SPEED_FOR_EFFECTS = 80 -- speed can be above that, but used for visual effects
+local GLIDER_MAX_SPEED = 200
+local GLIDER_WING_LENGTH = 24
+local GLIDER_MAX_START_SPEED = 50
+local GLIDER_DRAG_DOWN = -400
+
+playerControls.glide = function(self, player)
+ local pID = self:getPlayerID(player)
+
+ if self.current[pID] == "glide" then
+ return -- already gliding
+ end
+ self.current[pID] = "glide"
+
+ self:exitVehicle(player)
+
+ local vehicle = Object()
+ vehicle.Scale = 0.5
+ vehicle:SetParent(World)
+ vehicle.type = "glider"
+
+ self.vehicles[pID] = vehicle
+
+ local glider = self:getShape("voxels.glider")
+ glider.Shadow = true
+ glider.Physics = PhysicsMode.Disabled
+ vehicle.model = glider
+
+ vehicle.Position = player:PositionLocalToWorld({ 0, player.BoundingBox.Max.Y - 2, 0 })
+
+ glider.Scale = 0
+ ease:cancel(glider)
+ ease:outElastic(glider, 0.3).Scale = Number3(1, 1, 1)
+
+ if player.Animations.LiftArms then
+ player.Animations.LiftArms:Play()
+ end
+
+ vehicle.Physics = PhysicsMode.Dynamic
+ -- vehicle.Tick resets velocity every frame, it means we have to emulate each individual part ourself
+ -- instead of letting velocity compound (from forces, other objects, collision responses etc.), it also
+ -- means that vehicle.Acceleration does nothing for us
+ vehicle.gliderSpd = 0
+ vehicle.gliderPull = Number3(0, GLIDER_DRAG_DOWN, 0)
+
+ local rightTrail = wingTrail:create({ scale = 0.5 })
+ rightTrail.LocalPosition = { GLIDER_WING_LENGTH, 8, 0 }
+
+ local leftTrail = wingTrail:create({ scale = 0.5 })
+ leftTrail.LocalPosition = { -GLIDER_WING_LENGTH, 8, 0 }
+
+ vehicle.wingTrails = {}
+ table.insert(vehicle.wingTrails, rightTrail)
+ table.insert(vehicle.wingTrails, leftTrail)
+
+ local rightWingTip
+ local leftWingTip
+ local diffY
+ local maxDiff
+ local p
+ local leftLift
+ local rightLift
+ local down
+ local up
+ local speedOverMax
+ local f
+ local dot
+
+ vehicle.Rotation:Set(0, player.Rotation.Y, 0)
+ local initSpd = (player.Motion + player.Velocity * 0.1).Length
+ vehicle.gliderSpd = math.min(initSpd, GLIDER_MAX_START_SPEED)
+
+ player.Head.LocalRotation = { 0, 0, 0 }
+
+ player.Motion:Set(0, 0, 0)
+ player.Velocity:Set(0, 0, 0)
+ player.Physics = PhysicsMode.Disabled
+ player.Scale = 1.0
+
+ if player == Player then -- local simulation
+ local yaw = Rotation(0, 0, 0)
+ local yawDelta = Rotation(0, 0, 0)
+
+ local tilt = Rotation(0, 0, 0)
+ local tiltDelta = Rotation(0, 0, 0)
+
+ local roll = Rotation(0, 0, 0)
+ local rollDelta = Rotation(0, 0, 1)
+
+ local vehicleRoll = Object()
+ vehicleRoll.Velocity = Number3.Zero
+ vehicleRoll.Physics = PhysicsMode.Disabled
+ vehicleRoll:SetParent(vehicle)
+ glider:SetParent(vehicleRoll)
+ glider.LocalRotation = { 0, math.rad(180), 0 }
+
+ vehicle.roll = vehicleRoll -- used for sync
+
+ yaw:Set(0, player.Rotation.Y, 0)
+
+ player:SetParent(vehicleRoll, true)
+ player.LocalRotation:Set(Number3.Zero)
+ player.LocalPosition:Set(0, -27, 0)
+
+ rightTrail:SetParent(vehicleRoll)
+ leftTrail:SetParent(vehicleRoll)
+
+ vehicleRoll.Velocity:Set(vehicle.Velocity) -- copying for sync (physics disabled on vehicleRoll)
+
+ vehicle.CollisionBox = Box({ -10, -30, -10 }, { 10, 14, 10 })
+ vehicle.CollidesWithGroups = MAP_COLLISION_GROUPS
+ + BUILDING_COLLISION_GROUPS
+ + ITEM_COLLISION_GROUPS
+ + DRAFT_COLLISION_GROUPS
+ + BARRIER_COLLISION_GROUPS
+ vehicle.CollisionGroups = {}
+
+ vehicle.OnCollisionBegin = function(_, other)
+ if other.CollisionGroups == DRAFT_COLLISION_GROUPS then
+ return
+ end
+ playerControls:walk(player)
+ end
+
+ vehicle.Tick = function(o, dt)
+ rightWingTip = vehicleRoll:PositionLocalToWorld(GLIDER_WING_LENGTH, 0, 0)
+ leftWingTip = vehicleRoll:PositionLocalToWorld(-GLIDER_WING_LENGTH, 0, 0)
+
+ diffY = leftWingTip.Y - rightWingTip.Y
+
+ maxDiff = GLIDER_WING_LENGTH * 2
+ p = math.abs(diffY / maxDiff)
+
+ if diffY < 0 then
+ leftLift = 0.5 + p * 0.5
+ rightLift = 1.0 - leftLift
+ else
+ rightLift = 0.5 + p * 0.5
+ leftLift = 1.0 - rightLift
+ end
+
+ yawDelta.Y = diffY * dt * 0.001 * 70
+ yaw = yawDelta * yaw
+
+ o.Rotation = yaw * tilt
+
+ dot = o.Forward:Dot(Number3.Down)
+ down = math.max(0, dot) -- 0 -> 1
+ up = math.max(0, -dot)
+
+ -- accelerate when facing down / lose more velocity when going up
+ o.gliderSpd = o.gliderSpd + down * 80.0 * dt - (8.0 + up * 30.0) * dt
+ o.gliderSpd = math.max(o.gliderSpd, 0)
+ o.gliderSpd = math.min(GLIDER_MAX_SPEED, o.gliderSpd)
+
+ o.Velocity:Set(o.Forward * o.gliderSpd + o.gliderPull * dt)
+ vehicleRoll.Velocity:Set(o.Velocity) -- copying for sync (physics disabled on vehicleRoll)
+
+ -- EFFECTS
+ speedOverMax = math.min(1.0, o.gliderSpd / GLIDER_MAX_SPEED_FOR_EFFECTS)
+ Camera.FOV = cameraDefaultFOV + 20 * speedOverMax
+
+ f = 0.2 * speedOverMax
+ rightTrail:setColor(Color(255, 255, 255, rightLift * f))
+ leftTrail:setColor(Color(255, 255, 255, leftLift * f))
+ end
+
+ require("camera_modes"):setThirdPerson({
+ rigidity = 0.3,
+ target = vehicle,
+ rotationOffset = Rotation(math.rad(20), 0, 0),
+ collidesWithGroups = CAMERA_COLLIDES_WITH_GROUPS,
+ })
+
+ self.onDrag = function(pe)
+ rollDelta.Z = -pe.DX * 0.01
+ tiltDelta.X = -pe.DY * 0.01
+
+ roll = rollDelta * roll
+ tilt = tiltDelta * tilt
+
+ vehicle.Rotation = yaw * tilt
+ vehicleRoll.LocalRotation = roll -- triggers sync
+ end
+
+ self.dirPad = function(_, _)
+ -- nothing to do, just turning off walk controls
+ end
+
+ updateSync()
+ else -- distant player
+ glider:SetParent(vehicle)
+ glider.LocalRotation = { 0, math.rad(180), 0 }
+
+ player:SetParent(vehicle, true)
+ player.LocalRotation:Set(Number3.Zero)
+ player.LocalPosition:Set(0, -27, 0)
+
+ rightTrail:SetParent(vehicle)
+ leftTrail:SetParent(vehicle)
+
+ vehicle.Tick = function(o, _)
+ -- only update wing trail colors
+ -- no local simulation (for now?), looks good enough so far
+
+ rightWingTip = vehicle:PositionLocalToWorld(GLIDER_WING_LENGTH, 0, 0)
+ leftWingTip = vehicle:PositionLocalToWorld(-GLIDER_WING_LENGTH, 0, 0)
+
+ diffY = leftWingTip.Y - rightWingTip.Y
+
+ maxDiff = GLIDER_WING_LENGTH * 2
+ p = math.abs(diffY / maxDiff)
+
+ if diffY < 0 then
+ leftLift = 0.5 + p * 0.5
+ rightLift = 1.0 - leftLift
+ else
+ rightLift = 0.5 + p * 0.5
+ leftLift = 1.0 - rightLift
+ end
+
+ l = o.Velocity.Length
+
+ -- EFFECTS
+ speedOverMax = math.min(1.0, l / GLIDER_MAX_SPEED_FOR_EFFECTS)
+
+ f = 0.2 * speedOverMax
+ rightTrail:setColor(Color(255, 255, 255, rightLift * f))
+ leftTrail:setColor(Color(255, 255, 255, leftLift * f))
+ end
+ end
+
+ return vehicle
+end
+
+-- MODULE : COLLECTIBLES
+
+collectedGliderParts = {}
+gliderBackpackCollectibles = {}
+gliderUnlocked = false
+
+local REQUEST_FAIL_RETRY_DELAY = 5.0
+-- local GLIDER_PARTS = 10
+
+backEquipment = nil
+
+-- function resetKVS()
+-- -- if debug then
+-- local retry = {}
+-- retry.fn = function()
+-- local store = KeyValueStore(Player.UserID)
+-- store:set("collectedGliderParts", {}, "collectedJetpackParts", {}, "CollectedNerfParts", {}, function(ok)
+-- if not ok then
+-- Timer(REQUEST_FAIL_RETRY_DELAY, retry.fn)
+-- end
+-- end)
+-- end
+-- retry.fn()
+-- addCollectibles()
+-- -- end
+-- end
+
+function addCollectibles()
+ conf = require("config")
+
+ gliderParts = {}
+
+ local function unlockGlider()
+ gliderUnlocked = true
+ for _, backpack in ipairs(gliderBackpackCollectibles) do
+ backpack.object.PrivateDrawMode = 0
+ end
+ end
+
+ local function spawnBackpacks()
+ local defaultBackpackConfig = {
+ scale = GLIDER_BACKPACK.SCALE,
+ rotation = Number3.Zero,
+ position = Number3.Zero,
+ itemName = GLIDER_BACKPACK.ITEM_NAME,
+ onCollisionBegin = function(c)
+ if gliderUnlocked then
+ collectParticles.Position = c.object.Position
+ collectParticles:spawn(20)
+ sfx("wood_impact_3", { Position = c.object.Position, Volume = 0.6, Pitch = 1.3 })
+ Client:HapticFeedback()
+ collectible:remove(c)
+
+ Player:EquipBackpack(c.object)
+
+ backEquipment = "glider"
+ multi:action("equipGlider")
+
+ gliderUsageToast = toast:create({
+ message = "Maintain jump key to start gliding!",
+ center = false,
+ iconShape = bundle.Shape("voxels.glider"),
+ duration = -1, -- negative duration means infinite
+ })
+ else
+ backpackTransparentToast = toast:create({
+ message = #collectedGliderParts .. "/" .. #gliderParts .. " collected",
+ center = true,
+ duration = -1, -- negative duration means infinite
+ iconShape = bundle.Shape("voxels.glider_parts"),
+ })
+ end
+ end,
+ onCollisionEnd = function(_)
+ if backpackTransparentToast then
+ backpackTransparentToast:remove()
+ backpackTransparentToast = nil
+ end
+ end,
+ }
+
+ -- To replace by segment below when world editor is fixed
+ local gliderBackpackConfigs = {
+ { position = Number3(75, 186, 262) },
+ { position = Number3(330, 80, 160) },
+ }
+
+ for _, bpConfig in pairs(gliderBackpackConfigs) do
+ local config = conf:merge(defaultBackpackConfig, bpConfig)
+ local c = collectible:create(config)
+ c.object.PrivateDrawMode = 1
+ table.insert(gliderBackpackCollectibles, c)
+ end
+
+ -- segment
+ --[[
+ local bp = World:FindObjectsByName("voxels.glider_backpack")
+
+ for _, v in pairs(bp) do
+ local config = {position = v.Position}
+ config = conf:merge(defaultBackpackConfig, config)
+ local c = collectible:create(config)
+ c.object.PrivateDrawMode = 1
+ table.insert(gliderBackpackCollectibles, c)
+ end
+ ]]
+ end
+
+ local function spawnCollectibles()
+ -- To replace with segment below when World Editor is fixed
+ tempPos = {
+ Number3(264, 80, 504),
+ Number3(144, 164, 408),
+ Number3(75, 186, 300),
+ }
+
+ for k, v in ipairs(tempPos) do
+ local s = bundle.Shape("voxels.glider_parts")
+ s.Name = "voxels.glider_parts_" .. k
+ s.Position = v
+ table.insert(gliderParts, s)
+ end
+
+ -- segment
+ --[[
+ for i = 1, GLIDER_PARTS do
+ table.insert(gliderParts, World:FindObjectByName("voxels.glider_parts_" .. i))
+ end
+ ]]
+
+ local gliderPartConfig = {
+ scale = 0.5,
+ itemName = "voxels.glider_parts",
+ position = Number3.Zero,
+ userdata = {
+ ID = -1,
+ },
+ onCollisionBegin = function(c)
+ collectParticles.Position = c.object.Position
+ collectParticles:spawn(20)
+ sfx("wood_impact_3", { Position = c.object.Position, Volume = 0.6, Pitch = 1.3 })
+ Client:HapticFeedback()
+ collectible:remove(c)
+ if _helpers.contains(collectedGliderParts, c.userdata.ID) then
+ return
+ end
+
+ table.insert(collectedGliderParts, c.userdata.ID)
+
+ local retry = {}
+ retry.fn = function()
+ local store = KeyValueStore(Player.UserID)
+ store:set("collectedGliderParts", collectedGliderParts, function(ok)
+ if not ok then
+ Timer(REQUEST_FAIL_RETRY_DELAY, retry.fn)
+ end
+ end)
+ end
+ retry.fn()
+
+ if #collectedGliderParts >= #gliderParts then
+ -- the last glider part has been collected
+ toast:create({
+ message = "Glider unlocked!",
+ center = false,
+ iconShape = bundle.Shape(GLIDER_BACKPACK.ITEM_NAME),
+ duration = 2,
+ })
+ unlockGlider()
+ else
+ -- a glider part has been collected
+ toast:create({
+ message = #collectedGliderParts .. "/" .. #gliderParts .. " collected",
+ iconShape = bundle.Shape("voxels.glider_parts"),
+ keepInStack = false,
+ })
+ end
+ end,
+ }
+
+ if #collectedGliderParts >= #gliderParts then
+ unlockGlider()
+ for _, v in pairs(gliderParts) do
+ v:RemoveFromParent()
+ end
+ else
+ for k, v in ipairs(gliderParts) do
+ if not _helpers.contains(collectedGliderParts, k) then
+ local config = conf:merge(gliderPartConfig, { position = v.Position, userdata = { ID = k } })
+ collectible:create(config)
+ end
+ v:RemoveFromParent()
+ end
+ end
+ end
+
+ local t = {}
+ t.get = function()
+ local store = KeyValueStore(Player.UserID)
+ store:get("collectedGliderParts", "collectedJetpackParts", function(ok, results)
+ if type(ok) ~= "boolean" then
+ error("KeyValueStore:get() unexpected type of 'ok'", 2)
+ end
+ if type(results) ~= "table" and type(results) ~= "nil" then
+ error("KeyValueStore:get() unexpected type of 'results'", 2)
+ end
+ if ok == true then
+ if results.collectedGliderParts ~= nil then
+ collectedGliderParts = results.collectedGliderParts
+ end
+ spawnBackpacks()
+ spawnCollectibles()
+ else
+ Timer(REQUEST_FAIL_RETRY_DELAY, t.get)
+ end
+ end)
+ end
+ t.get()
+end
+
+-- CREATURES
+
+pet = {}
+pet.dialogTreeTeaser = function(_, target)
+ dialog:create(
+ "Hey there! 🙂 You seem like a kind-hearted soul. I'm sure you would take good care of a companion! ✨",
+ target,
+ { "➡️ Yes of course!", "➡️ No thank you" },
+ function(idx)
+ if idx == 1 then
+ dialog:create(
+ "This machine here can spawn a random egg for you, containing a loving companion!",
+ target,
+ { "➡️ Ok!" },
+ function()
+ dialog:create(
+ "I'm currently fixing it, come back in a few days!",
+ target,
+ { "➡️ I'll be back!" },
+ function()
+ dialog:remove()
+ end
+ )
+ end
+ )
+ elseif idx == 2 then
+ dialog:create(
+ "Oh, I could swear you would like a small friend around. Come back if you change your mind!",
+ target,
+ { "➡️ Ok!" },
+ function()
+ dialog:remove()
+ end
+ )
+ end
+ end
+ )
+end
\ No newline at end of file
diff --git a/cubzh/cubzh.lua b/cubzh/scripts/imagine.lua
similarity index 50%
rename from cubzh/cubzh.lua
rename to cubzh/scripts/imagine.lua
index e96e4e4..9f9b286 100644
--- a/cubzh/cubzh.lua
+++ b/cubzh/scripts/imagine.lua
@@ -1,16 +1,6 @@
Config = {
Map = "boumety.building_challenge",
- Items = {
- "kooow.white_yeti",
- "kooow.ribberhead_dino",
- "petroglyph.hp_bar",
- "petroglyph.block_red",
- "petroglyph.block_green",
- }
-}
-local opponentsList = {
- "kooow.white_yeti",
- "kooow.ribberhead_dino",
+ Items = {}
}
-- CONSTANTS
@@ -23,8 +13,6 @@ faceNormals = {
[Face.Left] = Number3(-1,0,0), [Face.Right] = Number3(1,0,0), [Face.Top] = Number3(0,1,0)
}
-hpBars = {}
-
Client.OnStart = function()
multi = require "multi"
@@ -42,24 +30,12 @@ Client.OnStart = function()
ui = require "uikit"
controls = require "controls"
controls:setButtonIcon("action1", "⬆️")
-
+
showInstructions()
showWelcomeHint()
- -- HP bars array init
- for i=1,16,1 do
- hpBars[i] = {}
- end
-
Camera:SetModeThirdPerson()
dropPlayer(Player)
- opponent = spawnRandomOpponent()
- attachHPBar(opponent)
-
-
- Player.healthIndex = 0
- Player.name = "player"
- attachHPBar(Player)
end
Pointer.Click = function(pe)
@@ -85,7 +61,6 @@ Client.Tick = function(dt)
dropPlayer(Player)
Player:TextBubble("💀 Oops!")
end
- updateHPBars(dt)
end
function cancelMenu()
@@ -171,13 +146,12 @@ Client.DidReceiveEvent = function(e)
end
local s = Shape(e.vox)
s.CollisionGroups = Map.CollisionGroups
- s.tag = "generatedImage" -- Tagging the object as a generated image
s.userInput = e.userInput
s.user = e.user
s:SetParent(World)
-- first block is not at 0,0,0
- -- use collision box min to offset the pivot
+ -- use collision box min to offset the pivot
local collisionBoxMin = s.CollisionBox.Min
local center = s.CollisionBox.Center:Copy()
@@ -202,27 +176,6 @@ Client.DidReceiveEvent = function(e)
end
elseif e.action == "otherGen" then
makeBubble(e)
- elseif e.action == "update_hp" then
- local target = Player
- if e.targetName ~= "player" then
- print("targeting opponent")
- target = opponent
- end
- if target and hpBars[target.healthIndex + 1] then
- local hpBarInfo = hpBars[target.healthIndex + 1]
- -- Assuming damageAmount is the amount of HP to remove
- local damageAmount = tonumber(e.damageAmount)
- local currentPercentage = hpBarInfo.hpShape.LocalScale.Z / hpBarMaxLength
- local currentHP = currentPercentage * playerMaxHP
- -- Calculate new HP after taking damage
- local newHP = math.max(currentHP - damageAmount, 0)
- local newPercentage = newHP / playerMaxHP
- -- Update the target's HP visually
- setHPBar(target, newHP, true) -- true to animate the HP bar change
- -- if HP reaches 0, explode the target
- require("explode"):shapes(target)
- target.IsHidden = true
- end
end
end
@@ -240,6 +193,7 @@ Server.OnPlayerJoin = function(p)
HTTP:Get(d.url, headers, function(data)
local e = Event()
e.vox = data.Body
+ print("d.url : " .. d.url)
e.id = d.e.id
e.pos = d.e.pos
e.rotY = d.e.rotY
@@ -276,48 +230,7 @@ Server.DidReceiveEvent = function(e)
e2:SendTo(Players)
end)
end)
- elseif e.action == "resolve_encounter" then
- -- Construct the URL with query parameters for the GET request
- local apiURL = "https://gig.ax:5678/encounter/" -- Update this with the correct path if needed
- -- Assuming 'opponent' and 'image' are encoded correctly for use in a URL
- -- You may need to URL encode these values if they contain spaces or special characters
- apiURL = apiURL .. "?opponent=" .. e.opponentName .. "&tool=" .. e.image
-
- local headers = {} -- Headers if needed, though typically not required for a GET request
-
- HTTP:Get(apiURL, headers, function(data)
- local result = JSON:Decode(data.Body)
- if result and result.operations then
- print("Encounter resolved successfully: " .. result.description.text)
- for _, operation in ipairs(result.operations) do
- if operation.name == "HURT" then
- -- Assuming 'parameters' contains the target ID and the damage amount
- local targetName = operation.parameters[1]
- local damageAmount = tonumber(operation.parameters[2])
-
- -- Send an event to the client to update the target's HP
- local hpUpdateEvent = Event()
- hpUpdateEvent.action = "update_hp"
- hpUpdateEvent.targetName = targetName
- hpUpdateEvent.damageAmount = damageAmount
- hpUpdateEvent:SendTo(Players) -- Adjust as necessary to target specific player(s)
- elseif operation.name == "NOTHING" then
- -- Handle no effect
- local updateEvent = Event()
- updateEvent.action = "no_effect"
- updateEvent.description = result.description.text
- print("NOTHING: " .. result.description.text)
- updateEvent:SendTo(Players)
- end
- end
- else
- -- Handle error or no response
- print("Error: can't resolve encounter or no operations returned.")
- print(data.Body)
- print(result)
- end
- end)
- end
+ end
end
-- Utility functions
@@ -329,8 +242,6 @@ function hideMenu()
deleteTarget()
end
-local equippedImage = nil -- This will hold the reference to the equipped image object
-
function showMenu(pointerEvent)
hideMenu()
@@ -341,9 +252,9 @@ function showMenu(pointerEvent)
if impact.Object == Map then
local pos = pointerEvent.Position + pointerEvent.Direction * impact.Distance
-
+
showTarget(impact, pos)
-
+
createButton = ui:createButton("➕ Create Image")
local px = screenPos.X - createButton.Width * 0.5
@@ -352,7 +263,7 @@ function showMenu(pointerEvent)
local py = screenPos.Y + POINTER_OFFSET
if py < Screen.SafeArea.Bottom + PADDING then py = Screen.SafeArea.Bottom + PADDING end
- if py > Screen.Height - Screen.SafeArea.Top - createButton.Height - PADDING then py = Screen.Height - Screen.SafeArea.Top - createButton.Height - PADDING end
+ if py > Screen.Height - Screen.SafeArea.Top - createButton.Height - PADDING then py = Screen.Height - Screen.SafeArea.Top - createButton.Height - PADDING end
createButton.pos.X = px
createButton.pos.Y = py
@@ -399,95 +310,40 @@ function showMenu(pointerEvent)
prompt.pos.X = px
prompt.pos.Y = py
end
- elseif impact.Object and impact.Object.tag == "generatedImage" and impact.Distance < 100 then
- print("Picked up the generated image at position", impact.Object.Position)
- -- Logic to pick up the generated image if not already equipped
- if not equippedImage then
- equippedImage = impact.Object
- -- Disable physics to "pick up" the object
- impact.Object.Physics = PhysicsMode.Disabled
- Player:EquipRightHand(impact.Object)
- impact.Object.Scale = 0.5 -- Adjust scale if needed
-
- -- Inform server and other players that the object has been picked up
- local e = Event()
- e.action = "picked_object"
- e.objectId = impact.Object.Id
- e:SendTo(Server, OtherPlayers)
-
- sfx("pick_up_sound", {Position = impact.Object.Position}) -- Adjust sound effect as needed
- end
- elseif impact.Object and impact.Object.tag == "opponent" and equippedImage then
- print("Used equipped image on opponent at position", impact.Object.Position)
- -- Use the equipped image on the opponent
- useImageOnOpponent(impact.Object, equippedImage)
- equippedImage = nil -- Reset the equipped image reference
- else
- print("No action for this object")
- itemDetails = ui:createFrame(Color(0,0,0,128))
- local line1 = ui:createText(impact.Object.userInput, Color.White)
- line1.object.MaxWidth = 200
- line1:setParent(itemDetails)
- local line2 = ui:createText("by " .. impact.Object.user, Color.White, "small")
- line2:setParent(itemDetails)
-
- itemDetails.parentDidResize = function()
- local width = math.max(line1.Width, line2.Width) + PADDING * 2
- local height = line1.Height + line2.Height + PADDING * 3
- itemDetails.Width = width
- itemDetails.Height = height
- line1.pos = {PADDING, itemDetails.Height - PADDING - line1.Height, 0}
- line2.pos = line1.pos - {0, line2.Height + PADDING, 0}
-
- local px = screenPos.X - itemDetails.Width * 0.5
- if px < Screen.SafeArea.Left + PADDING then px = Screen.SafeArea.Left + PADDING end
- if px > Screen.Width - Screen.SafeArea.Right - itemDetails.Width - PADDING then px = Screen.Width - Screen.SafeArea.Right - itemDetails.Width - PADDING end
-
- local py = screenPos.Y + POINTER_OFFSET
- if py < Screen.SafeArea.Bottom + PADDING then py = Screen.SafeArea.Bottom + PADDING end
- if py > Screen.Height - Screen.SafeArea.Top - itemDetails.Height - PADDING then py = Screen.Height - Screen.SafeArea.Top - itemDetails.Height - PADDING end
-
- itemDetails.pos = {px, py, 0}
- end
+
+ else -- clicked on generated item
+
+ itemDetails = ui:createFrame(Color(0,0,0,128))
+ local line1 = ui:createText(impact.Object.userInput, Color.White)
+ line1.object.MaxWidth = 200
+ line1:setParent(itemDetails)
+ local line2 = ui:createText("by " .. impact.Object.user, Color.White, "small")
+ line2:setParent(itemDetails)
+
+ itemDetails.parentDidResize = function()
+ local width = math.max(line1.Width, line2.Width) + PADDING * 2
+ local height = line1.Height + line2.Height + PADDING * 3
+ itemDetails.Width = width
+ itemDetails.Height = height
+ line1.pos = {PADDING, itemDetails.Height - PADDING - line1.Height, 0}
+ line2.pos = line1.pos - {0, line2.Height + PADDING, 0}
+
+ local px = screenPos.X - itemDetails.Width * 0.5
+ if px < Screen.SafeArea.Left + PADDING then px = Screen.SafeArea.Left + PADDING end
+ if px > Screen.Width - Screen.SafeArea.Right - itemDetails.Width - PADDING then px = Screen.Width - Screen.SafeArea.Right - itemDetails.Width - PADDING end
+
+ local py = screenPos.Y + POINTER_OFFSET
+ if py < Screen.SafeArea.Bottom + PADDING then py = Screen.SafeArea.Bottom + PADDING end
+ if py > Screen.Height - Screen.SafeArea.Top - itemDetails.Height - PADDING then py = Screen.Height - Screen.SafeArea.Top - itemDetails.Height - PADDING end
+
+ itemDetails.pos = {px, py, 0}
+ end
itemDetails:parentDidResize()
+
end
end
end
-function useImageOnOpponent(opponentObject, imageObject)
- -- Send an event to the server to process the encounter
- local e = Event()
- e.action = "resolve_encounter"
- local author, name = splitAtFirst(opponentObject.ItemName, '.') -- Assuming this contains the "opponent" name
- e.opponentName = name
- e.image = imageObject.userInput -- Assuming this contains the "tool" used
- e:SendTo(Server)
-
- -- Optionally, remove the image from the player's hand immediately or wait for server response
- imageObject:RemoveFromParent()
-end
-
-function spawnRandomOpponent()
- -- Randomly select an opponent from the Config.Items list
- local opponentIndex = math.random(1, #opponentsList)
- local opponentKey = opponentsList[opponentIndex]
-
- -- Instantiate the selected opponent shape
- local opponentShape = Shape(Items[opponentIndex])
-
- -- Set opponent properties and add it to the world
- opponentShape:SetParent(World)
- opponentShape.Position = Player.Position
- opponentShape.IsHidden = false
- opponentShape.tag = "opponent"
- opponentShape.name = opponentKey
- opponentShape.healthIndex = opponentIndex
-
- -- Add to collision group
- opponentShape.CollisionGroups = Map.CollisionGroups
- return opponentShape
-end
-
function showTarget(impact, pos)
if impact == nil then return end
@@ -538,8 +394,8 @@ function dropPlayer(p)
end
-- shows instructions at the top left corner of the screen
-function showInstructions()
- if instructions ~= nil then
+function showInstructions()
+ if instructions ~= nil then
instructions:show()
return
end
@@ -572,20 +428,20 @@ end
function showWelcomeHint()
if welcomeHint ~= nil then return end
welcomeHint = ui:createText("Click on a block!", Color(1.0,1.0,1.0), "big")
- welcomeHint.parentDidResize = function()
+ welcomeHint.parentDidResize = function()
welcomeHint.pos.X = Screen.Width * 0.5 - welcomeHint.Width * 0.5
- welcomeHint.pos.Y = Screen.Height * 0.66 - welcomeHint.Height * 0.5
+ welcomeHint.pos.Y = Screen.Height * 0.66 - welcomeHint.Height * 0.5
end
welcomeHint:parentDidResize()
local t = 0
- welcomeHint.object.Tick = function(o, dt)
+ welcomeHint.object.Tick = function(o, dt)
t = t + dt
- if (t % 0.4) <= 0.2 then
- o.Color = Color(0.0, 0.8, 0.6)
- else
- o.Color = Color(1.0, 1.0, 1.0)
- end
+ if (t % 0.4) <= 0.2 then
+ o.Color = Color(0.0, 0.8, 0.6)
+ else
+ o.Color = Color(1.0, 1.0, 1.0)
+ end
end
end
@@ -636,122 +492,4 @@ function makeBubble(e)
end)
gens[e.id] = bubble
-end
-
-function splitAtFirst(inputString, delimiter)
- local pos = string.find(inputString, delimiter, 1, true)
- if pos then
- return string.sub(inputString, 1, pos - 1), string.sub(inputString, pos + 1)
- else
- return inputString
- end
-end
-
-
--- HP Bars
-
--- create and attach HP bar to given player
-attachHPBar = function(obj)
- local frame = Shape(Items.petroglyph.hp_bar)
- obj:AddChild(frame)
- frame.LocalPosition = { 0, 35, 4.5 }
- frame.LocalRotation = { 0, math.pi * .5, 0 } -- item was drawn along Z
- if hpBarMaxLength == nil then
- hpBarMaxLength = frame.Depth - 2
- end
-
- local hp = Shape(Items.petroglyph.block_red)
- frame:AddChild(hp)
- hp.Pivot.Z = 0
- hp.LocalPosition.Z = -hpBarMaxLength * .5
- hp.LocalScale.Z = hpBarMaxLength
- hp.IsHidden = true
-
- local hpFull = Shape(Items.petroglyph.block_green)
- frame:AddChild(hpFull)
- hpFull.LocalScale.Z = hpBarMaxLength
-
- print("Attaching HP bar to player at index: " .. obj.healthIndex .. " player name is " .. obj.name)
- hpBar = {
- player = obj,
- startScale = hpBarMaxLength,
- targetScale = hpBarMaxLength,
- timer = 0,
- hpShape = hp,
- hpFullShape = hpFull,
- frameShape = frame
- }
- print("hpBar length: " .. hpBar.hpShape.LocalScale.Z)
- hpBars[obj.healthIndex + 1] = hpBar
-end
-
--- remove HP bar from player
-removeHPBar = function(player)
- hpBars[player.healthIndex + 1] = {}
-end
-
--- sets target HP for given player, animated by default
-setHPBar = function(player, hpValue, isAnimated)
- local hp = hpBars[player.healthIndex + 1]
- local v = clamp(hpValue / playerMaxHP, 0, 1)
- if isAnimated or isAnimated == nil then
- print("Animating HP bar")
- hp.startScale = hp.hpShape.LocalScale.Z
- hp.targetScale = hpBarMaxLength * v
- hp.timer = playerHPDuration
- else
- print("Setting HP bar without animation")
- if v == 1.0 then
- hp.hpFullShape.IsHidden = false
- hp.hpShape.IsHidden = true
- else
- hp.hpFullShape.IsHidden = true
- hp.hpShape.IsHidden = false
- hp.hpShape.LocalScale.Z = hpBarMaxLength * v
- end
- hp.timer = 0
- end
-end
-
--- update all HP bars animation
-updateHPBars = function (dt)
- local hp = nil
- for i=1,#hpBars,1 do
- hp = hpBars[i]
- if hp.timer ~= nil and hp.timer > 0 then
- hp.timer = hp.timer - dt
-
- local delta = hp.targetScale - hp.startScale
- local v = clamp(1 - hp.timer / playerHPDuration, 0, 1)
- hp.hpShape.LocalScale.Z = hp.startScale + delta * v
-
- local isFull = hp.hpShape.LocalScale.Z == hpBarMaxLength
- hp.hpFullShape.IsHidden = not isFull
- hp.hpShape.IsHidden = isFull
-
- if delta < 0 then
- hp.frameShape.LocalPosition.X = playerHPNegativeShake * math.sin(60 * hp.timer)
- else
- hp.frameShape.LocalPosition.Y = 26 + playerHPPositiveBump * math.sin(v * math.pi)
- end
- end
- end
-end
-
-
-clamp = function(value, min, max)
- if value < min then
- return min
- elseif value > max then
- return max
- else
- return value
- end
-end
-
-
--- HP bars settings
-playerMaxHP = 100
-playerHPDuration = .5 -- update animation in seconds
-playerHPNegativeShake = .3
-playerHPPositiveBump = .6
\ No newline at end of file
+end
\ No newline at end of file
diff --git a/cubzh/scripts/mod_test.lua b/cubzh/scripts/mod_test.lua
new file mode 100644
index 0000000..dbe0041
--- /dev/null
+++ b/cubzh/scripts/mod_test.lua
@@ -0,0 +1,484 @@
+
+-- This is Cubzh's default world script.
+-- We'll provide more templates later on to cover specific use cases like FPS games, data storage, synchornized shapes, etc.
+-- Cubzh dev team and devs from the community will be happy to help you on Discord if you have questions: discord.gg/cubzh
+
+Config = {
+ -- using item as map
+ Map = "buche.playground_heights",
+ -- items that are going to be loaded before startup
+ Items = {
+ "jacksbertox.crate"
+ }
+}
+
+local CRATE_START_POSITION = Number3(382, 290, 153)
+local YOUR_API_TOKEN = "H4gjL-e9kvLF??2pz6oh=kJL497cBnsyCrQFdVkFadUkLnIaEamroYHb91GywMXrbGeDdmTiHxi8EqmJduCKPrDnfqWsjGuF0JJCUTrasGcBfGx=tlJCjq5q8jhVHWL?krIE74GT9AJ7qqX8nZQgsDa!Unk8GWaqWcVYT-19C!tCo11DcLvrnJPEOPlSbH7dDcXmAMfMEf1ZwZ1v1C9?2/BjPDeiAVTRlLFilwRFmKz7k4H-kCQnDH-RrBk!ZHl7"
+local API_URL = "https://0074-195-154-25-43.ngrok-free.app"
+
+-- Client.OnStart is the first function to be called when the world is launched, on each user's device.
+Client.OnStart = function()
+
+ -- Setting up the ambience (lights)
+ -- other possible presets:
+ -- - ambience.dawn
+ -- - ambience.dusk
+ -- - ambience.midnight
+ -- The "ambience" module also accepts
+ -- custom settings (light colors, angles, etc.)
+ local ambience = require("ambience")
+ ambience:set(ambience.noon)
+
+ -- The sfx module can be used to play spatialized sounds in one line calls.
+ -- A list of available sounds can be found here:
+ -- https://docs.cu.bzh/guides/quick/adding-sounds#list-of-available-sounds
+ sfx = require("sfx")
+ -- There's only one AudioListener, but it can be placed wherever you want:
+ Player.Head:AddChild(AudioListener)
+
+ -- Requiring "multi" module is all you need to see other players in your game!
+ -- (remove this line if you want to be solo)
+ require("multi")
+
+ -- This function drops the local player above the center of the map:
+ dropPlayer = function()
+ Player.Position = Number3(Map.Width * 0.5, Map.Height + 10, Map.Depth * 0.5) * Map.Scale
+ Player.Rotation = { 0, 0, 0 }
+ Player.Velocity = { 0, 0, 0 }
+ end
+
+ -- Add player to the World (root scene Object) and call dropPlayer().
+ World:AddChild(Player)
+ dropPlayer()
+
+ dialog = require("dialog")
+ dialog:setMaxWidth(400)
+
+ avatar = require("avatar")
+ -- Create an avatarId to avatar mapping
+ avatarIdToAvatar = {
+ }
+
+ updateLocationTimer = nil
+ character = nil
+end
+
+LocalEvent:Listen(LocalEvent.Name.OnPlayerJoin, function(p)
+ print("Player joined")
+end)
+
+LocalEvent:Listen(LocalEvent.Name.Tick, function(dt)
+ if _config == nil then
+ return
+ end
+end)
+
+-- jump function, triggered with Action1
+-- (space bar on PC, button 1 on mobile)
+Client.Action1 = function()
+ if Player.IsOnGround then
+ Player.Velocity.Y = 100
+ sfx("hurtscream_1", {Position = Player.Position, Volume = 0.4})
+ end
+
+ -- Example location registration
+ local loc1 = createLocation(
+ "Medieval Inn",
+ Number3(130, 23, 75),
+ "An inn lost in the middle of the forest, where travelers can rest and eat."
+ )
+ local loc2 = createLocation(
+ "Abandoned temple",
+ Number3(303, 20, 263),
+ "Lost deep inside the woods, this temple features a mysterious altar statue. Fresh fruits and offrands are scattered on the ground."
+ )
+ local loc3 = createLocation(
+ "Lone grave in the woods",
+ Number3(142, 20, 258),
+ "Inside a small clearing in the forest lies a stone cross, marking the grave of a lost soul."
+ )
+ local loc4 = createLocation(
+ "Rope bridge",
+ Number3(26, 20, 301),
+ "Near the edge of a cliff, a rope bridge connects the forest to the island. The bridge is old and fragile, but still usable."
+ )
+ local loc5 = createLocation(
+ "Forest entrance",
+ Number3(156, 20, 168),
+ "The entrance to the forest is marked by a large stone arch. The path is wide and well maintained."
+ )
+
+ local NPC1 = createNPC("aduermael", "Tall, with green eyes", "Friendly and helpful", "Medieval Inn", Number3(130, 23, 75))
+ local NPC2 = createNPC("soliton", "Short, with a big nose", "Grumpy and suspicious", "Abandoned temple", Number3(303, 20, 263))
+ local NPC3 = createNPC("caillef", "Tall, with a big beard", "Wise and mysterious", "Lone grave in the woods", Number3(142, 20, 258))
+
+ local e = Event()
+ e.action = "testRegisterEngine"
+ e:SendTo(Server)
+end
+
+
+function createNPC(avatarId, physicalDescription, psychologicalProfile, currentLocationName, currentPosition)
+ -- Create the NPC's Object and Avatar
+ local NPC = {}
+ NPC.object = Object()
+ World:AddChild(NPC.object)
+ NPC.object.Position = currentPosition or Number3(0, 0, 0)
+ print("Placing NPC at position: " .. currentPosition._x .. ", " .. currentPosition._y .. ", " .. currentPosition._z)
+ NPC.object.Scale = 0.5
+ NPC.object.Physics = PhysicsMode.Dynamic
+
+ avatar = require("avatar")
+ NPC.avatar = avatar:get(avatarId)
+ print("Getting avatar for NPC: " .. avatarId)
+ NPC.avatar:SetParent(NPC.object)
+ avatarIdToAvatar[avatarId] = NPC.avatar
+
+ -- Register it
+ local e = Event()
+ e.action = "registerNPC"
+ e.avatarId = avatarId
+ e.physicalDescription = physicalDescription
+ e.psychologicalProfile = psychologicalProfile
+ e.currentLocationName = currentLocationName
+ e:SendTo(Server)
+ return NPC
+end
+
+function createLocation(name, position, description)
+ local e = Event()
+ e.action = "registerLocation"
+ e.name = name
+ e.position = position
+ e.description = description
+ e:SendTo(Server)
+end
+
+
+-- Function to calculate distance between two positions
+local function calculateDistance(pos1, pos2)
+ local dx = pos1.X - pos2.x
+ local dy = pos1.Y - pos2.y
+ local dz = pos1.Z - pos2.z
+ return math.sqrt(dx*dx + dy*dy + dz*dz)
+end
+
+function findClosestLocation(playerPosition, locationData)
+ -- Assume `playerPosition` holds the current position of the player
+ local closestLocation = nil
+ local smallestDistance = math.huge -- Large initial value
+
+ for _, location in pairs(locationData) do
+ local distance = calculateDistance(playerPosition, location.position)
+ if distance < smallestDistance then
+ smallestDistance = distance
+ closestLocation = location
+ end
+ end
+
+ if closestLocation then
+ -- Closest location found, now send its ID to update the character's location
+ return closestLocation
+ end
+end
+
+-- Client.Tick is executed up to 60 times per second on player's device.
+Client.Tick = function(dt)
+ -- Detect if player is falling and use dropPlayer() when it happens!
+ if Player.Position.Y < -500 then
+ dropPlayer()
+ -- It's funnier with a message.
+ Player:TextBubble("💀 Oops!", true)
+ end
+end
+
+-- Triggered when posting message with chat input
+Client.OnChat = function(payload)
+ -- <0.0.52 : "payload" was a string value.
+ -- 0.0.52+ : "payload" is a table, with a "message" key
+ local msg = type(payload) == "string" and payload or payload.message
+
+ Player:TextBubble(msg, 3, true)
+ sfx("waterdrop_2", {Position = Player.Position, Pitch = 1.1 + math.random() * 0.5})
+
+ local e = Event()
+ e.action = "stepMainCharacter"
+ e.content = msg
+ e:SendTo(Server)
+
+end
+
+-- Pointer.Click is called following click/touch down & up events,
+-- without draging the pointer in between.
+-- Let's use this function to add a few interactions with the scene!
+Pointer.Click = function(pointerEvent)
+
+ -- Cast a ray from pointer event,
+ -- do different things depending on what it hits.
+ local impact = pointerEvent:CastRay()
+ if impact ~= nil then
+ if impact.Object == Player then
+ -- clicked on local player -> display message + little jump
+ Player:TextBubble("Easy, I'm ticklish! 😬", 1.0, true)
+ sfx("waterdrop_2", {Position = Player.Position, Pitch = 1.1 + math.random() * 0.5})
+ Player.Velocity.Y = 50
+ end
+ end
+end
+
+
+Client.DidReceiveEvent = function(e)
+ if e.action == "displayDialog" then
+ dialog:create(e.content, avatarIdToAvatar[e.avatarId])
+ elseif e.action == "characterResponse" then
+ -- Setup a new timer to delay the next update call
+ characterId = e.character._id
+ updateLocationTimer = Timer(0.5, true, function()
+ local e = Event()
+ e.action = "updateCharacterLocation"
+ e.position = Player.Position
+ e.characterId = characterId
+ e:SendTo(Server)
+ end)
+ -- print("Character ID: " .. character._id)
+ end
+end
+
+
+------------------------------------------------------------------------------------------------
+-- Server Stuff ---------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------
+
+-- Server --------------------------------------------------------------------------------------
+Server.OnStart = function()
+ -- Initialize tables to hold NPC and location data
+ engineId = nil
+ locationId = nil
+ character = nil
+ npcData = {}
+ locationData = {}
+end
+
+Server.DidReceiveEvent = function(e)
+ if e.action == "registerNPC" then
+ registerNPC(e.avatarId, e.physicalDescription, e.psychologicalProfile, e.currentLocationName)
+ elseif e.action == "registerLocation" then
+ registerLocation(e.name, e.position, e.description)
+ elseif e.action == "testRegisterEngine" then
+ print("Registering engine...")
+ registerEngine(e.Sender)
+ elseif e.action == "stepMainCharacter" then
+ stepMainCharacter(character, engineId, npcData["aduermael"]._id, npcData["aduermael"].name, e.content)
+ elseif e.action == "updateCharacterLocation" then
+ closest = findClosestLocation(e.position, locationData)
+ -- if closest._id is different from the current location, update the character's location
+ if closest._id ~= character.current_location._id then
+ updateCharacterLocation(engineId, e.characterId, closest._id)
+ end
+ else
+ print("Unknown action received.")
+ end
+end
+
+Server.Tick = function(dt)
+end
+
+Server.OnPlayerJoin = function(player)
+end
+
+------------------------------------------------------------------------------------------------
+-- Gigax Stuff ---------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------
+
+-- Function to create and register an NPC
+function registerNPC(avatarId, physicalDescription, psychologicalProfile, currentLocationName)
+ -- Add NPC to npcData table
+ npcData[avatarId] = {
+ name = avatarId,
+ physical_description = physicalDescription,
+ psychological_profile = psychologicalProfile,
+ current_location_name = currentLocationName,
+ skills = {
+ {
+ name = "say",
+ description = "Say smthg out loud",
+ parameter_types = {"character", "content"}
+ },
+ {
+ name = "move",
+ description = "Move to a new location",
+ parameter_types = {"location"}
+ }
+ }
+ }
+end
+
+-- Function to register a location
+function registerLocation(name, position, description)
+ locationData[name] = {
+ position = {x = position._x, y = position._y, z = position._z},
+ name = name,
+ description = description
+ }
+end
+
+function registerEngine(sender)
+ local apiUrl = API_URL .. "/api/engine/company/"
+ print("Updating engine with NPC data...")
+ local headers = {
+ ["Content-Type"] = "application/json",
+ ["Authorization"] = YOUR_API_TOKEN
+ }
+
+ -- Prepare the data structure expected by the backend
+ local engineData = {
+ name = "mod_test",
+ NPCs = {},
+ locations = {} -- Populate if you have dynamic location data similar to NPCs
+ }
+
+ for _, npc in pairs(npcData) do
+ table.insert(engineData.NPCs, {
+ name = npc.name,
+ physical_description = npc.physical_description,
+ psychological_profile = npc.psychological_profile,
+ current_location_name = npc.current_location_name,
+ skills = npc.skills
+ })
+ end
+
+ -- Populate locations
+ for _, loc in pairs(locationData) do
+ table.insert(engineData.locations, {
+ name = loc.name,
+ position = loc.position,
+ description = loc.description
+ })
+ end
+
+ local body = JSON:Encode(engineData)
+
+ HTTP:Post(apiUrl, headers, body, function(res)
+ if res.StatusCode ~= 201 then
+ print("Error updating engine: " .. res.StatusCode)
+ return
+ end
+ -- Decode the response body to extract engine and location IDs
+ local responseData = JSON:Decode(res.Body)
+
+ -- Save the engine_id for future use
+ engineId = responseData.engine.id
+
+ -- Saving all the _ids inside locationData table:
+ for _, loc in pairs(responseData.locations) do
+ locationData[loc.name]._id = loc._id
+ end
+
+ -- same for characters:
+ for _, npc in pairs(responseData.NPCs) do
+ npcData[npc.name]._id = npc._id
+ end
+
+
+ registerMainCharacter(engineId, locationData["Medieval Inn"]._id, sender)
+ -- print the location data as JSON
+ end)
+end
+
+function registerMainCharacter(engineId, locationId, sender)
+ -- Example character data, replace with actual data as needed
+ local newCharacterData = {
+ name = "oncheman",
+ physical_description = "A human playing the game",
+ current_location_id = locationId,
+ position = {x = 0, y = 0, z = 0}
+ }
+
+ -- Serialize the character data to JSON
+ local jsonData = JSON:Encode(newCharacterData)
+
+ local headers = {
+ ["Content-Type"] = "application/json",
+ ["Authorization"] = YOUR_API_TOKEN
+ }
+
+ local apiUrl = API_URL .. "/api/character/company/main?engine_id=" .. engineId
+
+ -- Make the HTTP POST request
+ HTTP:Post(apiUrl, headers, jsonData, function(response)
+ if response.StatusCode ~= 200 then
+ print("Error creating or fetching main character: " .. response.StatusCode)
+ end
+ print("Main character created/fetched successfully.")
+ character = JSON:Decode(response.Body)
+ local e = Event()
+ e.action = "characterResponse"
+ e.character = character
+ e:SendTo(sender)
+ end)
+end
+
+function stepMainCharacter(character, engineId, targetId, targetName, content)
+
+ -- Now, step the character
+ local stepUrl = API_URL .. "/api/character/" .. character._id .. "/step-no-ws?engine_id=" .. engineId
+ local stepActionData = {
+ character_id = character._id, -- Use the character ID from the creation/fetch response
+ action_type = "SAY",
+ target = targetId,
+ target_name = targetName,
+ content = content
+ }
+ local stepJsonData = JSON:Encode(stepActionData)
+
+ local headers = {
+ ["Content-Type"] = "application/json",
+ ["Authorization"] = YOUR_API_TOKEN
+ }
+ -- You might need to adjust headers or use the same if they include the needed Authorization
+ HTTP:Post(stepUrl, headers, stepJsonData, function(stepResponse)
+ if stepResponse.StatusCode ~= 200 then
+ -- print("Error stepping character: " .. stepResponse.StatusCode)
+ return
+ end
+
+ local actions = JSON:Decode(stepResponse.Body)
+ -- Find the target character by id using the "target" field in the response:
+ for _, action in ipairs(actions) do
+ for _, npc in pairs(npcData) do
+ if action.character_id == npc._id then
+ -- Perform the action on the target character
+ local e = Event()
+ e.action = "displayDialog"
+ e.avatarId = npc.name
+ e.content = action.content
+ e:SendTo(Players)
+ end
+ end
+ end
+ end)
+end
+
+function updateCharacterLocation(engineId, characterId, locationId)
+ local updateData = {
+ -- Fill with necessary character update information
+ current_location_id = locationId
+ }
+
+ local jsonData = JSON:Encode(updateData)
+ local headers = {
+ ["Content-Type"] = "application/json",
+ ["Authorization"] = YOUR_API_TOKEN
+ }
+
+ -- Assuming `characterId` and `engineId` are available globally or passed appropriately
+ local apiUrl = API_URL .. "/api/character/" .. characterId .. "?engine_id=" .. engineId
+
+ HTTP:Post(apiUrl, headers, jsonData, function(response)
+ if response.StatusCode ~= 200 then
+ print("Error updating character location: " .. response.StatusCode)
+ else
+ character = JSON:Decode(response.Body)
+ end
+ end)
+end
\ No newline at end of file
diff --git a/cubzh/scripts/mod_test_pathfinding.lua b/cubzh/scripts/mod_test_pathfinding.lua
new file mode 100644
index 0000000..13f3385
--- /dev/null
+++ b/cubzh/scripts/mod_test_pathfinding.lua
@@ -0,0 +1,932 @@
+
+-- This is Cubzh's default world script.
+-- We'll provide more templates later on to cover specific use cases like FPS games, data storage, synchornized shapes, etc.
+-- Cubzh dev team and devs from the community will be happy to help you on Discord if you have questions: discord.gg/cubzh
+
+Config = {
+ -- using item as map
+ Map = "buche.playground_heights",
+ -- items that are going to be loaded before startup
+ Items = {
+ "jacksbertox.crate"
+ }
+}
+
+local CRATE_START_POSITION = Number3(382, 290, 153)
+local GUY_START_POSITION = Number3(37, 3, 36)
+local OFFSET_Y = Number3(0, 1, 0)
+local OFFSET_XZ = Number3(0.5, 0, 0.5)
+local YOUR_API_TOKEN = "H4gjL-e9kvLF??2pz6oh=kJL497cBnsyCrQFdVkFadUkLnIaEamroYHb91GywMXrbGeDdmTiHxi8EqmJduCKPrDnfqWsjGuF0JJCUTrasGcBfGx=tlJCjq5q8jhVHWL?krIE74GT9AJ7qqX8nZQgsDa!Unk8GWaqWcVYT-19C!tCo11DcLvrnJPEOPlSbH7dDcXmAMfMEf1ZwZ1v1C9?2/BjPDeiAVTRlLFilwRFmKz7k4H-kCQnDH-RrBk!ZHl7"
+local API_URL = "https://0074-195-154-25-43.ngrok-free.app"
+local multi = require("multi")
+local ease = require("ease")
+
+-- Client.OnStart is the first function to be called when the world is launched, on each user's device.
+Client.OnStart = function()
+
+ -- Setting up the ambience (lights)
+ -- other possible presets:
+ -- - ambience.dawn
+ -- - ambience.dusk
+ -- - ambience.midnight
+ -- The "ambience" module also accepts
+ -- custom settings (light colors, angles, etc.)
+ local ambience = require("ambience")
+ ambience:set(ambience.noon)
+
+ -- The sfx module can be used to play spatialized sounds in one line calls.
+ -- A list of available sounds can be found here:
+ -- https://docs.cu.bzh/guides/quick/adding-sounds#list-of-available-sounds
+ sfx = require("sfx")
+ -- There's only one AudioListener, but it can be placed wherever you want:
+ Player.Head:AddChild(AudioListener)
+
+ -- Requiring "multi" module is all you need to see other players in your game!
+ -- (remove this line if you want to be solo)
+ require("multi")
+
+ -- This function drops the local player above the center of the map:
+ dropPlayer = function()
+ Player.Position = Number3(Map.Width * 0.5, Map.Height + 10, Map.Depth * 0.5) * Map.Scale
+ Player.Rotation = { 0, 0, 0 }
+ Player.Velocity = { 0, 0, 0 }
+ end
+
+ -- Add player to the World (root scene Object) and call dropPlayer().
+ World:AddChild(Player)
+ dropPlayer()
+
+ -- A Shape is an object made out of cubes.
+ -- Let's instantiate one with one of our imported items:
+ crate = Shape(Items.jacksbertox.crate)
+ World:AddChild(crate)
+ crate.Physics = PhysicsMode.StaticPerBlock
+ -- By default, the pivot is at the center of the bounding box,
+ -- but since want to place this one with ground positions, let's
+ -- move the pivot to the bottom:
+ crate.Pivot.Y = 0
+ crate.Position = CRATE_START_POSITION
+ crateOwner = nil
+
+ dialog = require("dialog")
+ dialog:setMaxWidth(400)
+
+ avatar = require("avatar")
+ -- Create an avatarId to avatar mapping
+ avatarIdToAvatar = {
+ }
+end
+
+LocalEvent:Listen(LocalEvent.Name.OnPlayerJoin, function(p)
+ print("Player joined")
+ -- _initPathfinding(p)
+end)
+
+LocalEvent:Listen(LocalEvent.Name.Tick, function(dt)
+ if _config == nil then
+ return
+ end
+ -- _cameraFollow(_config.camera, Player)
+ -- _cameraHideBlockingObjects(_config.camera)
+ -- _moveNoPhysics(dt)
+end)
+
+-- INIT FUNCTIONS
+_initPathfinding = function(_, p)
+ Client.DirectionalPad = nil
+ Client.AnalogPad = nil
+ Pointer.Drag = nil
+
+ if not _config then
+ _setConfig()
+ end
+ print("Initializing player")
+ _initializePlayer(p, _config.camera)
+ if not p == Player then
+ return
+ end
+ print("Initializing map")
+ _initializeMap(_config.map)
+end
+
+-- jump function, triggered with Action1
+-- (space bar on PC, button 1 on mobile)
+Client.Action1 = function()
+ if Player.IsOnGround then
+ Player.Velocity.Y = 100
+ sfx("hurtscream_1", {Position = Player.Position, Volume = 0.4})
+ end
+
+ o = Object()
+ o:SetParent(World)
+ o.Position = GUY_START_POSITION + {0, 0, 3}
+ o.Scale = 0.5
+ o.Physics = PhysicsMode.Dynamic
+ a = avatar:get("aduermael")
+ a:SetParent(o)
+ avatarIdToAvatar["aduermael"] = a
+
+ local e = Event()
+ e.action = "registerNPC"
+ e.avatarId = "aduermael"
+ e.physicalDescription = "Tall, with green eyes"
+ e.psychologicalProfile = "Friendly and helpful"
+ e.currentLocationName = "Medieval Inn"
+ e:SendTo(Server)
+
+ NPC1 = createNPC("aduermael", "Tall, with green eyes", "Friendly and helpful", "Medieval Inn")
+ NPC2 = createNPC("soliton", "Short, with a big nose", "Grumpy and suspicious", "Abandoned temple")
+ NPC3 = createNPC("caillef", "Tall, with a big beard", "Wise and mysterious", "Lone grave in the woods")
+
+ local e = Event()
+ e.action = "testRegisterEngine"
+ e:SendTo(Server)
+end
+
+
+function createNPC(avatarId, physicalDescription, psychologicalProfile, currentLocationName)
+ -- Create the NPC's Object and Avatar
+ NPC = {}
+ NPC.object = Object()
+ NPC.object.SetParent(World)
+ NPC.object.Position = GUY_START_POSITION + {0, 0, 3}
+ NPC.object.Scale = 0.5
+ NPC.object.Physics = PhysicsMode.Dynamic
+
+ NPC.avatar = avatar:get("aduermael")
+ NPC.avatar:SetParent(NPC.object)
+ avatarIdToAvatar["aduermael"] = NPC.avatar
+
+ -- Register it
+ local e = Event()
+ e.action = "registerNPC"
+ e.avatarId = avatarId
+ e.physicalDescription = physicalDescription
+ e.psychologicalProfile = psychologicalProfile
+ e.currentLocationName = currentLocationName
+ e:SendTo(Server)
+ return NPC
+end
+
+
+-- Client.Tick is executed up to 60 times per second on player's device.
+Client.Tick = function(dt)
+ -- Detect if player is falling and use dropPlayer() when it happens!
+ if Player.Position.Y < -500 then
+ dropPlayer()
+ -- It's funnier with a message.
+ Player:TextBubble("💀 Oops!", true)
+ end
+end
+
+-- Triggered when posting message with chat input
+Client.OnChat = function(payload)
+ -- <0.0.52 : "payload" was a string value.
+ -- 0.0.52+ : "payload" is a table, with a "message" key
+ local msg = type(payload) == "string" and payload or payload.message
+
+ Player:TextBubble(msg, 3, true)
+ sfx("waterdrop_2", {Position = Player.Position, Pitch = 1.1 + math.random() * 0.5})
+
+ local e = Event()
+ e.action = "stepMainCharacter"
+ e.content = msg
+ e:SendTo(Server)
+
+end
+
+-- Pointer.Click is called following click/touch down & up events,
+-- without draging the pointer in between.
+-- Let's use this function to add a few interactions with the scene!
+Pointer.Click = function(pointerEvent)
+
+ -- Cast a ray from pointer event,
+ -- do different things depending on what it hits.
+ local impact = pointerEvent:CastRay()
+ if impact ~= nil then
+ if impact.Object == Player then
+ -- clicked on local player -> display message + little jump
+ Player:TextBubble("Easy, I'm ticklish! 😬", 1.0, true)
+ sfx("waterdrop_2", {Position = Player.Position, Pitch = 1.1 + math.random() * 0.5})
+ Player.Velocity.Y = 50
+
+ elseif impact.Object == Map and impact.Block ~= nil then
+ -- clicked on Map block -> display block info
+
+ -- making an exception if player is holding the crate to place it
+ if crateEquiped and crateOwner == Player.ID and impact.FaceTouched == Face.Top then
+ -- The position of the impact can be computed from the origin of the ray
+ -- (pointerEvent.Position here), its direction (pointerEvent.Direction),
+ -- and the distance:
+ local impactPosition = pointerEvent.Position + pointerEvent.Direction * impact.Distance
+
+ Player:EquipRightHand(nil)
+ World:AddChild(crate)
+ crate.Physics = PhysicsMode.StaticPerBlock
+ crate.Scale = 1
+ crate.Pivot.Y = 0
+ crate.Position = impactPosition
+ crateEquiped = false
+
+ -- send event to inform server and other players that
+ -- crate as been placed. Of course this code is not
+ -- needed if you turn off multiplayer.
+ local e = Event()
+ e.action = "place_crate"
+ e.owner = Player.ID
+ e.pos = crate.Position
+ e:SendTo(Server, OtherPlayers)
+
+ sfx("wood_impact_1", {Position = crate.Position})
+
+ crateOwner = nil
+
+ return
+ end
+
+ local b = impact.Block
+ local t = Text()
+ t.Text = string.format("coords: %d,%d,%d\ncolor: %d,%d,%d",
+ b.Coords.X, b.Coords.Y, b.Coords.Z,
+ b.Color.R, b.Color.G, b.Color.B)
+ t.FontSize = 44
+ t.Type = TextType.Screen -- display text in screen space
+ t.BackgroundColor = Color(0,0,0,0) -- transparent
+ t.Color = Color(255,255,255)
+ World:AddChild(t)
+
+ local blockCenter = b.Coords + {0.5,0.5,0.5}
+ -- convert block coordinates to world position:
+ t.Position = impact.Object:BlockToWorld(blockCenter)
+
+ -- Timer to request text removal in 1 second
+ Timer(1.0, function()
+ t:RemoveFromParent()
+ end)
+
+ _pathTo(pointerEvent, o)
+
+ elseif not crateEquiped and impact.Object == crate then
+ if impact.Distance < 80 then
+
+ crate.Physics = PhysicsMode.Disabled
+ Player:EquipRightHand(crate)
+ crate.Scale = 0.5
+ crateEquiped = true
+ crateOwner = Player.ID
+
+ -- inform server and other players
+ -- that crate has been picked
+ local e = Event()
+ e.action = "picked_crate"
+ e:SendTo(Server, OtherPlayers)
+
+ sfx("wood_impact_5", {Position = crate.Position, Pitch = 1.2})
+
+ else
+ Player:TextBubble("I'm too far to grab it!", 1, true)
+ sfx("waterdrop_2", {Position = Player.Position, Pitch = 1.1 + math.random() * 0.5})
+ end
+ end
+ end
+end
+
+
+Client.DidReceiveEvent = function(e)
+ if e.action == "displayDialog" then
+ dialog:create(e.content, avatarIdToAvatar[e.avatarId])
+ end
+end
+
+
+------------------------------------------------------------------------------------------------
+-- Server Stuff ---------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------
+
+-- Server --------------------------------------------------------------------------------------
+Server.OnStart = function()
+ -- Initialize tables to hold NPC and location data
+ engineId = nil
+ locationId = nil
+ character = nil
+ npcData = {}
+ locationData = {}
+end
+
+Server.DidReceiveEvent = function(e)
+ if e.action == "registerNPC" then
+ registerNPC(e.avatarId, e.physicalDescription, e.psychologicalProfile, e.currentLocationName)
+ elseif e.action == "testRegisterEngine" then
+ -- Example location registration
+ registerLocation(
+ "Medieval Inn",
+ Vector3(23,3,13),
+ "An inn lost in the middle of the forest, where travelers can rest and eat."
+ )
+ registerLocation(
+ "Abandoned temple",
+ Vector3(60,3,57),
+ "Lost deep inside the woods, this temple features a mysterious altar statue. Fresh fruits and offrands are scattered on the ground."
+ )
+ registerLocation(
+ "Lone grave in the woods",
+ Vector3(28,3,53),
+ "Inside a small clearing in the forest lies a stone cross, marking the grave of a lost soul."
+ )
+ registerLocation(
+ "Rope bridge",
+ Vector3(4,3,63),
+ "Near the edge of a cliff, a rope bridge connects the forest to the island. The bridge is old and fragile, but still usable."
+ )
+ registerLocation(
+ "Forest entrance",
+ Vector3(32,3,30),
+ "The entrance to the forest is marked by a large stone arch. The path is wide and well maintained."
+ )
+ registerEngine()
+ elseif e.action == "stepMainCharacter" then
+ stepMainCharacter(character, engineId, npcData["aduermael"]._id, npcData["aduermael"].name, e.content)
+ end
+ -- if e.action == "interactWithNPC" then
+ -- -- Assume e contains NPC identifier and the player's action
+ -- local apiUrl = "https://yourbackend.com/api/simulation/step" -- Adjust to your actual endpoint
+ -- local headers = {
+ -- ["Content-Type"] = "application/json",
+ -- ["Authorization"] = "Bearer YOUR_API_TOKEN"
+ -- }
+
+ -- local actionData = {
+ -- npcId = e.npcId,
+ -- action = e.actionType,
+ -- playerId = e.Sender.ID
+ -- }
+
+ -- local body = JSON:Encode(actionData)
+
+ -- HTTP:Post(apiUrl, headers, body, function(res)
+ -- if res.StatusCode ~= 200 then
+ -- print("Error stepping simulation: " .. res.StatusCode)
+ -- return
+ -- end
+ -- local updates = JSON:Decode(res.Body)
+ -- -- Handle the updates, such as moving NPCs, changing dialogue, etc.
+ -- -- You might need to send events back to the client to reflect these updates
+ -- end)
+ -- end
+end
+
+Server.OnPlayerJoin = function(player)
+end
+
+------------------------------------------------------------------------------------------------
+-- Gigax Stuff ---------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------
+
+-- Function to create and register an NPC
+function registerNPC(avatarId, physicalDescription, psychologicalProfile, currentLocationName)
+ -- Add NPC to npcData table
+ npcData[avatarId] = {
+ name = avatarId,
+ physical_description = physicalDescription,
+ psychological_profile = psychologicalProfile,
+ current_location_name = currentLocationName,
+ skills = {
+ {
+ name = "say",
+ description = "Say smthg out loud",
+ parameter_types = {"character", "content"}
+ },
+ {
+ name = "move",
+ description = "Move to a new location",
+ parameter_types = {"location"}
+ }
+ }
+ }
+end
+
+-- Function to register a location
+function registerLocation(name, position, description)
+ locationData[name] = {
+ position = {x = position._x, y = position._y, z = position._z},
+ name = name,
+ description = description
+ }
+end
+
+function registerEngine()
+ local apiUrl = API_URL .. "/api/engine/company/"
+ print("Updating engine with NPC data...")
+ local headers = {
+ ["Content-Type"] = "application/json",
+ ["Authorization"] = YOUR_API_TOKEN
+ }
+
+ -- Prepare the data structure expected by the backend
+ local engineData = {
+ name = "mod_test",
+ NPCs = {},
+ locations = {} -- Populate if you have dynamic location data similar to NPCs
+ }
+
+ for _, npc in pairs(npcData) do
+ table.insert(engineData.NPCs, {
+ name = npc.name,
+ physical_description = npc.physical_description,
+ psychological_profile = npc.psychological_profile,
+ current_location_name = npc.current_location_name,
+ skills = npc.skills
+ })
+ end
+
+ -- Populate locations
+ for _, loc in pairs(locationData) do
+ table.insert(engineData.locations, {
+ name = loc.name,
+ position = loc.position,
+ description = loc.description
+ })
+ end
+
+ local body = JSON:Encode(engineData)
+
+ HTTP:Post(apiUrl, headers, body, function(res)
+ if res.StatusCode ~= 201 then
+ print("Error updating engine: " .. res.StatusCode)
+ return
+ end
+ -- Decode the response body to extract engine and location IDs
+ local responseData = JSON:Decode(res.Body)
+
+ -- Save the engine_id for future use
+ engineId = responseData.engine.id
+
+ -- Saving all the _ids inside locationData table:
+ for _, loc in pairs(responseData.locations) do
+ locationData[loc.name]._id = loc._id
+ end
+
+ -- same for characters:
+ for _, npc in pairs(responseData.NPCs) do
+ npcData[npc.name]._id = npc._id
+ end
+
+
+ character = registerMainCharacter(engineId, locationData["Village Entrance"]._id)
+ -- print the location data as JSON
+ end)
+end
+
+function registerMainCharacter(engineId, locationId)
+ -- Example character data, replace with actual data as needed
+ local newCharacterData = {
+ name = "oncheman",
+ physical_description = "Tall, with green eyes",
+ current_location_id = locationId,
+ position = {x = 0, y = 0, z = 0}
+ }
+
+ -- Serialize the character data to JSON
+ local jsonData = JSON:Encode(newCharacterData)
+
+ local headers = {
+ ["Content-Type"] = "application/json",
+ ["Authorization"] = YOUR_API_TOKEN
+ }
+
+ local apiUrl = API_URL .. "/api/character/company/main?engine_id=" .. engineId
+
+ -- Make the HTTP POST request
+ HTTP:Post(apiUrl, headers, jsonData, function(response)
+ if response.StatusCode ~= 200 then
+ print("Error creating or fetching main character: " .. response.StatusCode)
+ end
+ print("Main character created/fetched successfully.")
+ character = JSON:Decode(response.Body)
+ end)
+end
+
+function stepMainCharacter(character, engineId, targetId, targetName, content)
+
+ -- Now, step the character
+ local stepUrl = API_URL .. "/api/character/" .. character._id .. "/step-no-ws?engine_id=" .. engineId
+ local stepActionData = {
+ character_id = character._id, -- Use the character ID from the creation/fetch response
+ action_type = "SAY",
+ target = targetId,
+ target_name = targetName,
+ content = content
+ }
+ local stepJsonData = JSON:Encode(stepActionData)
+ print("Stepping character with data: " .. stepJsonData)
+
+ local headers = {
+ ["Content-Type"] = "application/json",
+ ["Authorization"] = YOUR_API_TOKEN
+ }
+ -- You might need to adjust headers or use the same if they include the needed Authorization
+ HTTP:Post(stepUrl, headers, stepJsonData, function(stepResponse)
+ if stepResponse.StatusCode ~= 200 then
+ -- print("Error stepping character: " .. stepResponse.StatusCode)
+ return
+ end
+
+ local actions = JSON:Decode(stepResponse.Body)
+ print("Character stepped successfully.")
+ -- Find the target character by id using the "target" field in the response:
+ for _, action in ipairs(actions) do
+ for _, npc in pairs(npcData) do
+ if action.character_id == npc._id then
+ -- Perform the action on the target character
+ -- Process actions as needed
+ print("Target character: " .. npc.name)
+ local e = Event()
+ e.action = "displayDialog"
+ e.avatarId = npc.name
+ e.content = action.content
+ e:SendTo(Players)
+ end
+ end
+ end
+ end)
+end
+
+
+----------------------
+--module "pathfinding"
+----------------------
+pathfinding = {}
+local instance = {}
+instance.camera = nil
+instance.pfMap = nil
+instance.pfPath = nil
+instance.pfStep = nil
+instance.cursorPath = {}
+instance.cursors = {}
+local defaultConfig = {
+ zoomMin = 50,
+ zoomMax = 150,
+ zoomSpeed = 1,
+ zoomDefault = 150,
+ cameraOffset = Number3(0, 0, 80),
+ cameraFocusY = 25,
+ moveSpeed = 50,
+ mapGroups = { 1 },
+ playerGroups = { 2 },
+ map = nil,
+ camera = nil,
+}
+
+multi:onAction("showCursor", function(_, data)
+ _showCursor(Players[data.id], data.pos)
+end)
+
+
+_setConfig = function(conf)
+ config = require("config"):merge(defaultConfig, conf)
+ if not config.map then
+ config.map = Map
+ end
+ if not config.camera then
+ config.camera = Camera
+ end
+end
+
+_initializePlayer = function(p, c)
+ if p == Player then
+ p.walkFX = particles:newEmitter({
+ life = function()
+ return 0.3
+ end,
+ velocity = function()
+ local v = Number3(5 + math.random() * 5, 5, 0)
+ v:Rotate(0, math.random() * math.pi * 2, 0)
+ return v
+ end,
+ color = function()
+ return Color(255, 255, 255, 128)
+ end,
+ acceleration = function()
+ return -Config.ConstantAcceleration
+ end,
+ collidesWithGroups = function()
+ return { 1 }
+ end,
+ })
+ p.walkFX:SetParent(p)
+ p.walkFX.LocalPosition = { 0, 0, 0 }
+ end
+ -- _cursorCreate(p)
+end
+
+
+_initializeMap = function()
+ instance.pfMap = pathfinding.createPathfindingMap()
+end
+
+-- Add defaultConfig, setConfig...
+pathfinding.createPathfindingMap = function(config) -- Takes the map as argument
+ local defaultPathfindingConfig = {
+ map = Map,
+ pathHeight = 3,
+ pathLevel = 1,
+ obstacleGroups = { 3 },
+ }
+
+ _config = require("config"):merge(defaultPathfindingConfig, config)
+
+ local map2d = {} --create a 2D map to store which blocks can be walked on with a height map
+ local box = Box({ 0, 0, 0 }, _config.map.Scale) --create a box the size of a block
+ local dist = _config.map.Scale.Y * _config.pathHeight -- check for ~3 blocks up
+ local dir = Number3.Up -- checking up
+ for x = 0, _config.map.Width do
+ map2d[x] = {}
+ for z = 0, _config.map.Depth do
+ local h = _config.pathLevel -- default height is the path level
+ for y = 0, _config.map.Height do
+ if _config.map:GetBlock(x, y, z) then
+ h = y + 1
+ end -- adjust height by checking all blocks on the column to
+ end
+ box.Min = { x, h, z } * _config.map.Scale
+ box.Max = box.Min + _config.map.Scale
+ local data = h -- by default, store the height for the pathfinder
+ local impact = box:Cast(dir, dist, _config.obstacleGroups) -- check object above the retained height
+ if impact.Object ~= nil then
+ data = impact.Object
+ end -- if any, store the object for further use
+ map2d[x][z] = data
+ end
+ end
+ return map2d
+end
+
+pathfinding.findPath = function(origin, destination, map)
+ local kCount = 500
+ local diagonalsAllowed = true
+ local kClimb = 1
+
+ local directNeighbors = {
+ { x = -1, z = 0 },
+ { x = 0, z = 1 },
+ { x = 1, z = 0 },
+ { x = 0, z = -1 },
+ }
+ local diagonalNeighbours = {
+ { x = -1, z = 0 },
+ { x = 0, z = 1 },
+ { x = 1, z = 0 },
+ { x = 0, z = -1 },
+ { x = -1, z = -1 },
+ { x = 1, z = -1 },
+ { x = 1, z = 1 },
+ { x = -1, z = 1 },
+ }
+
+ local createNode = function(x, y, z, parent)
+ local node = {}
+ node.x, node.y, node.z, node.parent = x, y, z, parent
+ return node
+ end
+
+ local heuristic = function(x1, x2, z1, z2, y1, y2)
+ local dx = x1 - x2
+ local dz = z1 - z2
+ local dy = y1 - y2
+ local h = dx * dx + dz * dz + dy * dy
+ return h
+ end
+
+ local elapsed = function(parentNode)
+ return parentNode.g + 1
+ end
+
+ local calculateScores = function(node, endNode)
+ node.g = node.parent and elapsed(node.parent) or 0
+ node.h = heuristic(node.x, endNode.x, node.z, endNode.z, node.y, endNode.y)
+ node.f = node.g + node.h
+ end
+
+ local listContains = function(list, node)
+ for _, v in ipairs(list) do
+ if v.x == node.x and v.y == node.y and v.z == node.z then
+ return v
+ end
+ end
+ return false
+ end
+
+ local getChildren = function(node, map)
+ local children = {}
+ local neighbors = diagonalsAllowed and diagonalNeighbours or directNeighbors
+ local parentHeight = map[node.x][node.z]
+
+ for _, neighbor in ipairs(neighbors) do
+ local x = node.x + neighbor.x
+ local z = node.z + neighbor.z
+ local y = map[x][z]
+ if type(y) == "integer" and math.abs(y - parentHeight) <= kClimb then
+ table.insert(children, { x = x, y = y, z = z })
+ end
+ end
+ return children
+ end
+
+ -- Init lists to run the nodes & a count as protection for while
+ local openList = {}
+ local closedList = {}
+ local count = 0
+ -- Setup startNode and endNode
+ local endNode = createNode(destination.X, destination.Y, destination.Z, nil)
+ local startNode = createNode(origin.X, origin.Y, origin.Z, nil)
+ -- Calculate starting node score
+ calculateScores(startNode, endNode)
+ -- Insert the startNode as first node to examine
+ table.insert(openList, startNode)
+ -- While there are nodes to examine and the count is under kCount (and the function did not return)
+ while #openList > 0 and count < kCount do
+ count = count + 1
+ -- Sort openList with ascending f
+ table.sort(openList, function(a, b)
+ return a.f > b.f
+ end)
+ -- Examine the last node
+ local currentNode = table.remove(openList)
+ table.insert(closedList, currentNode)
+ if listContains(closedList, endNode) then
+ local path = {}
+ local current = currentNode
+ while current ~= nil do
+ table.insert(path, current)
+ current = current.parent
+ end
+ return path
+ end
+ -- Generate children based on map and test function
+ local children = getChildren(currentNode, map)
+ for _, child in ipairs(children) do
+ -- Create child node
+ local childNode = createNode(child.x, child.y, child.z, currentNode)
+ -- Check if it's already been examined
+ if not listContains(closedList, childNode) then
+ -- Check if it's already planned to be examined with a bigger f (meaning further away)
+ if not listContains(openList, childNode) then -- or self.listContains(openList, childNode).f > childNode.f then
+ calculateScores(childNode, endNode)
+ table.insert(openList, childNode)
+ end
+ end
+ end
+ end
+ print("No path found in " .. count .. " iterations")
+ return false
+end
+
+
+
+_pathTo = function(pointerEvent, protagonist)
+ local impact = pointerEvent:CastRay(config.mapGroups)
+ if impact.Block == nil then
+ return
+ end
+
+ local origin = Map:WorldToBlock(protagonist.Position)
+ origin = Number3(math.floor(origin.X), math.floor(origin.Y), math.floor(origin.Z))
+ local destination = impact.Block.Position / Map.Scale + OFFSET_Y
+ local path = pathfinding.findPath(origin, destination, instance.pfMap)
+ if not path then
+ print("No path found")
+ return
+ end
+ print("Path found")
+
+ instance.pfPath = path
+ instance.pfStep = #path - 1 --skipping the first block, which we're standing on
+
+ destination = impact.Block.Position + (OFFSET_XZ + OFFSET_Y) * Map.Scale
+ -- _showCursor(Player, destination)
+ _showPath(instance.pfPath)
+ _followPath(protagonist, instance.pfPath, instance.pfStep)
+end
+
+_showCursor = function(p, pos)
+ if p == Player then
+ multi:action("showCursor", { id = Player.ID, pos = pos })
+ end
+ _cursorSet(instance.cursors[p.ID], pos)
+end
+
+_followPath = function(p, path, idx)
+ if not path[idx] then
+ print("No path index found")
+ return
+ end
+ if instance.cursorPath[idx + 1] then
+ instance.cursorPath[idx + 1]:RemoveFromParent()
+ end
+ p.destination = (Number3(path[idx].x, path[idx].y, path[idx].z) + OFFSET_XZ) * Map.Scale
+ p.Forward = { p.destination.X - p.Position.X, 0, p.destination.Z - p.Position.Z } -- just to know where to face
+end
+
+_cursorCreate = function(p)
+ ease = require("ease")
+ Object:Load("aduermael.selector", function(o)
+ o:SetParent(World)
+ o.Physics = PhysicsMode.Trigger
+ o.CollisionGroups = nil
+ o.CollidesWithGroups = config.playerGroups
+ o.Scale = 0
+
+ o.Tick = function(self, dt)
+ self:RotateLocal(0, dt, 0)
+ end
+
+ o.OnCollisionBegin = function(self, _)
+ ease:cancel(self)
+ ease:outQuad(self, 0.5).Scale = { 0, 0, 0 }
+ end
+
+ if p ~= Player then
+ o.PrivateDrawMode = 1
+ end
+
+ print("Player ID: " .. p.ID)
+ instance.cursors[p.ID] = o
+ end)
+end
+
+_cursorSet = function(cursor, pos)
+ ease:cancel(cursor)
+ -- cursor.Position = pos
+ ease:outQuad(cursor, 0.5).Scale = CURSOR_SCALE
+end
+
+_showPath = function(path)
+ local createPath = function(v)
+ local s = MutableShape()
+ s:AddBlock(Color.White, 0, 0, 0)
+ s.Pivot = { 0.5, 0.5, 0.5 }
+ s.Physics = PhysicsMode.Trigger
+ s.CollidesWithGroups = { 2 }
+ s.CollisionGroups = {}
+ s.OnCollisionBegin = function(self, _)
+ self:RemoveFromParent()
+ end
+ s:SetParent(World)
+ s.Position = (Number3(v.x, v.y, v.z) + OFFSET_XZ) * Map.Scale
+
+ return s
+ end
+
+ for _, v in pairs(instance.cursorPath) do
+ v:RemoveFromParent()
+ end
+ instance.cursorPath = {}
+
+ for _, v in pairs(path) do
+ local c = createPath(v)
+ table.insert(instance.cursorPath, c)
+ end
+end
+
+_moveNoPhysics = function(dt)
+ if Player.Physics then
+ Player.Physics = PhysicsMode.Trigger
+ end
+ if not instance.pfPath then
+ return
+ end
+
+ local dest = Number3(Player.destination.X, 0, Player.destination.Z)
+ local pos = Number3(Player.Position.X, 0, Player.Position.Z)
+ local test = math.sqrt(2)
+ if (dest - pos).Length < test then --checking on a 2D plane only
+ Player.Position = Player.destination
+ instance.pfStep = instance.pfStep - 1
+ if instance.pfPath[instance.pfStep] ~= nil then
+ _followPath(Player, instance.pfPath, instance.pfStep)
+ else
+ if Player.Animations.Idle.IsPlaying then
+ return
+ end
+ if Player.Animations.Walk.IsPlaying then
+ Player.Animations.Walk:Stop()
+ end
+ Player.Animations.Idle:Play()
+ end
+ else
+ _followPath(Player, instance.pfPath, instance.pfStep)
+ Player.moveDir = (Player.destination - Player.Position):Normalize()
+ Player.Position = Player.Position + Player.moveDir * config.moveSpeed * dt
+ local r = math.random()
+ if Player.walkFX and r > 0.8 then
+ Player.walkFX:spawn(1)
+ end
+ if Player.Animations.Walk.IsPlaying then
+ return
+ end
+ if Player.Animations.Idle.IsPlaying then
+ Player.Animations.Idle:Stop()
+ end
+ Player.Animations.Walk:Play()
+ end
+end
diff --git a/cubzh/scripts/pathfinding.lua b/cubzh/scripts/pathfinding.lua
new file mode 100644
index 0000000..ffaf155
--- /dev/null
+++ b/cubzh/scripts/pathfinding.lua
@@ -0,0 +1,777 @@
+-- Modules
+-- Pathfinding
+-- Map loader
+-- Ressources
+-- Collect & Respawn
+-- Craft
+Config = {
+ Map = "buche.playground_heights",
+}
+
+local UI_ICON_SIZE = 32
+local UI_MARGIN_SMALL = 4
+
+localPlayerResources = {
+ ["Iron"] = 10,
+ ["Ash"] = 7,
+}
+
+inventory = {
+ maxWeight = 500,
+}
+
+resourcesList = {
+ ["Iron"] = { min = 0, level = 1, trade = "Miner", weight = 5, icon = "jacksbertox.iron_ingot" },
+ ["Copper"] = { min = 0, level = 2, trade = "Miner", weight = 5, icon = "jacksbertox.iron_ingot" },
+ ["Bronze"] = { min = 0, level = 3, trade = "Miner", weight = 5, icon = "jacksbertox.iron_ingot" },
+ ["Cobalt"] = { min = 0, level = 4, trade = "Miner", weight = 5, icon = "jacksbertox.iron_ingot" },
+ ["Tin"] = { min = 0, level = 5, trade = "Miner", weight = 5, icon = "jacksbertox.iron_ingot" },
+ --
+ ["Ash"] = { min = 0, level = 1, trade = "Lumberjack", weight = 5, icon = "voxels.log_2" },
+ ["Chestnut"] = { min = 0, level = 2, trade = "Lumberjack", weight = 5, icon = "voxels.log_2" },
+ ["Walnut"] = { min = 0, level = 3, trade = "Lumberjack", weight = 5, icon = "voxels.log_2" },
+ ["Oak"] = { min = 0, level = 4, trade = "Lumberjack", weight = 5, icon = "voxels.log_2" },
+ ["Maple"] = { min = 0, level = 5, trade = "Lumberjack", weight = 5, icon = "voxels.log_2" },
+ --
+ ["Wheat"] = { min = 0, level = 1, trade = "Farmer", weight = 2, icon = "pratamacam.corn" },
+ ["Barley"] = { min = 0, level = 2, trade = "Farmer", weight = 2, icon = "pratamacam.corn" },
+ ["Oats"] = { min = 0, level = 3, trade = "Farmer", weight = 2, icon = "pratamacam.corn" },
+ ["Hop"] = { min = 0, level = 4, trade = "Farmer", weight = 2, icon = "pratamacam.corn" },
+ ["Flax"] = { min = 0, level = 5, trade = "Farmer", weight = 2, icon = "pratamacam.corn" },
+ --
+ ["Nettles"] = { min = 0, level = 1, trade = "Alchemist", weight = 1, icon = "aduermael.leaf" },
+ ["Sage"] = { min = 0, level = 2, trade = "Alchemist", weight = 1, icon = "aduermael.leaf" },
+ ["Clover"] = { min = 0, level = 3, trade = "Alchemist", weight = 1, icon = "aduermael.leaf" },
+ ["Mint"] = { min = 0, level = 4, trade = "Alchemist", weight = 1, icon = "aduermael.leaf" },
+ ["Edelweiss"] = { min = 0, level = 5, trade = "Alchemist", weight = 1, icon = "aduermael.leaf" },
+}
+
+craftsList = {
+ ["Ferrite"] = {
+ trade = "Miner",
+ level = 2,
+ weight = 10,
+ ingredients = {
+ ["Iron"] = 10,
+ ["Copper"] = 10,
+ },
+ },
+ ["Loaf"] = {
+ trade = "Farmer",
+ level = 2,
+ weight = 4,
+ ingredients = {
+ ["Wheat"] = 10,
+ ["Barley"] = 10,
+ },
+ },
+}
+
+uiUpdateResource = function(_, resourceName, amount)
+ local text = uiElements[resourceName]
+ text.Text = string.format("%d", amount)
+ text.pos = text.icon.pos + { -text.Width - 10, text.icon.Height * 0.5 - text.Height * 0.5, 0 }
+ if amount > 0 then
+ text.icon:show()
+ text:show()
+ else
+ text.icon:hide()
+ text:hide()
+ end
+end
+
+Client.OnStart = function()
+ require("multi")
+ require("sfx")
+ require("textbubbles").displayPlayerChatBubbles = true
+ walkSFX = require("walk_sfx")
+ ui = require("uikit")
+
+ cResources:init(resourcesList)
+ for k, _ in pairs(resourcesList) do
+ cResources:addResourceCallbacks(k, {
+ onChange = uiUpdateResource,
+ })
+ end
+
+ initMap()
+ initUI()
+ initPlayer(Player)
+ dropPlayer(Player, Number3(Map.Width * 0.5, Map.Height + 1, Map.Depth * 0.5), Number3(0, 0, 0))
+
+ --map = loadMap(0, 0)
+end
+
+mapList = {}
+initMap = function()
+ for i = 0, 3 do
+ mapList[i] = {}
+ end
+ mapList[0][0] = "buche.playground_heights"
+ mapList[0][1] = "buche.playground_heights"
+end
+
+uiElements = {}
+initUI = function()
+ local idx = 0
+ for k, v in pairs(resourcesList) do
+ local i = idx + 1
+ Object:Load(v.icon, function(obj)
+ local text = ui:createText(string.format("%d", v.min), Color.White, "default")
+ local icon = ui:createShape(obj, { spherized = true })
+ icon.Size = UI_ICON_SIZE
+ text.icon = icon
+ text.displayIndex = i
+ uiElements[k] = text
+ text.parentDidResize = function(self)
+ self.icon.pos = {
+ Screen.Width - UI_MARGIN_SMALL - self.icon.Width,
+ Screen.Height - Screen.SafeArea.Top - (self.icon.Height + UI_MARGIN_SMALL) * self.displayIndex,
+ }
+ self.pos = self.icon.pos + { -self.Width - 10, self.Height * 0.5 - self.Height * 0.5, 0 }
+ end
+ text:parentDidResize()
+ end)
+ idx = idx + 1
+ end
+end
+
+initPlayer = function(p)
+ World:AddChild(p)
+ p.Head:AddChild(AudioListener)
+ p.Physics = true
+ walkSFX:register(p)
+end
+
+dropPlayer = function(p, pos, rot)
+ p.Position = pos
+ p.Rotation = rot
+end
+
+-- CRESOURCES MODULE
+cResources = {}
+local index = {
+ resourcesListeners = {},
+}
+
+local metatable = {
+ __index = index,
+ __metatable = false,
+}
+setmetatable(cResources, metatable)
+
+-- table of "name = { min, max, default }"
+index.init = function(_, resourcesList)
+ index.resourcesList = resourcesList
+ for _, v in pairs(index.resourcesList) do
+ v.default = v.default or v.min
+ end
+end
+
+-- config can be { max = 20 } to change the max value for example
+index.updateResource = function(_, target, resourceName, config)
+ for k, v in pairs(config) do
+ target.cResources[resourceName][k] = v
+ end
+ if index.resourcesListeners[resourceName] then
+ for _, callbacks in ipairs(index.resourcesListeners[resourceName]) do
+ if callbacks.onChange then
+ callbacks.onChange(target, resourceName, target.cResources[resourceName].value)
+ end
+ end
+ end
+end
+
+-- return a listener with a Remove function
+-- callbacks is an array containing two functions (optional)
+-- - onChange = function(target, resourceName, amount)
+-- - onMinReach = function(target, resourceName, amount)
+-- - onMaxReach = function(target, resourceName, amount)
+index.addResourceCallbacks = function(_, resourceName, callbacks)
+ index.resourcesListeners[resourceName] = index.resourcesListeners[resourceName] or {}
+ table.insert(index.resourcesListeners[resourceName], callbacks)
+ return {
+ Remove = function()
+ for i = #index.resourcesListeners[resourceName], 1, -1 do
+ local item = index.resourcesListeners[resourceName][i]
+ if item == callbacks then
+ table.remove(index.resourcesListeners[resourceName], i)
+ return
+ end
+ end
+ end,
+ }
+end
+
+index.add = function(_, target, resourceName, amount)
+ local resource = target.cResources[resourceName]
+ if resource.max and resource.value >= resource.max then
+ if index.resourcesListeners[resourceName] then
+ for _, callbacks in ipairs(index.resourcesListeners[resourceName]) do
+ if callbacks.onMaxReach then
+ callbacks.onMaxReach(target, resourceName, target.cResources[resourceName].value)
+ end
+ end
+ end
+ return false
+ end
+
+ resource.value = math.floor(resource.value + amount)
+
+ if resource.max and resource.value > resource.max then
+ resource.value = resource.max
+ if index.resourcesListeners[resourceName] then
+ for _, callbacks in ipairs(index.resourcesListeners[resourceName]) do
+ if callbacks.onMaxReach then
+ callbacks.onMaxReach(target, resourceName, resource.value)
+ end
+ end
+ end
+ end
+
+ if index.resourcesListeners[resourceName] then
+ for _, callbacks in ipairs(index.resourcesListeners[resourceName]) do
+ if callbacks.onChange then
+ callbacks.onChange(target, resourceName, resource.value)
+ end
+ end
+ end
+
+ -- TODO: save
+ return true
+end
+
+index.remove = function(_, target, resourceName, amount)
+ local resource = target.cResources[resourceName]
+ local newValue = math.floor(resource.value - amount)
+ if resource.min and resource.value < resource.min then
+ if index.resourcesListeners[resourceName] then
+ for _, callbacks in ipairs(index.resourcesListeners[resourceName]) do
+ if callbacks.onMinReach then
+ callbacks.onMinReach(target, resourceName, resource.value)
+ end
+ end
+ end
+ return false
+ end
+
+ resource.value = newValue
+
+ if index.resourcesListeners[resourceName] then
+ for _, callbacks in ipairs(index.resourcesListeners[resourceName]) do
+ if callbacks.onChange then
+ callbacks.onChange(target, resourceName, resource.value)
+ end
+ end
+ end
+
+ -- TODO: save
+ return true
+end
+
+LocalEvent:Listen(LocalEvent.Name.OnPlayerJoin, function(p)
+ if not index.resourcesList then
+ --error("resourcesList is not defined")
+ return
+ end
+ p.cResources = {}
+ for name, v in pairs(index.resourcesList) do
+ p.cResources[name] = {
+ value = v.default,
+ default = v.default,
+ min = v.min,
+ max = v.max,
+ }
+ end
+
+ for k, v in pairs(localPlayerResources) do
+ index:add(p, k, v)
+ end
+end)
+-- END CRESOURCES MODULE
+
+-- MODULE
+-- POINT AND CLICK
+-- Create a point and click mode with a topdown camera with movement mechanics including pathfinding
+pointAndClick = {}
+
+local defaultConfig = {
+ zoomMin = 50,
+ zoomMax = 150,
+ zoomSpeed = 1,
+ zoomDefault = 150,
+ cameraOffset = Number3(0, 0, 80),
+ cameraFocusY = 25,
+ moveSpeed = 50,
+ mapGroups = { 1 },
+ playerGroups = { 2 },
+ map = nil,
+ camera = nil,
+}
+
+local CURSOR_SCALE = Number3(0.5, 0.5, 0.5)
+local OFFSET_XZ = Number3(0.5, 0, 0.5)
+local OFFSET_Y = Number3(0, 1, 0)
+local BOXCAST_DIST = 500
+
+local multi = require("multi")
+local ease = require("ease")
+local particles = require("particles")
+
+local _config = nil
+local instance = {}
+instance.camera = nil
+instance.pfMap = nil
+instance.pfPath = nil
+instance.pfStep = nil
+instance.cursorPath = {}
+instance.cursors = {}
+
+-- AUTO INTEGRATION
+LocalEvent:Listen(LocalEvent.Name.OnPlayerJoin, function(p)
+ if type(Client.IsMobile) == "boolean" then
+ print("lol")
+ pointAndClick:init(p)
+ end
+end)
+LocalEvent:Listen(LocalEvent.Name.Tick, function(dt)
+ if _config == nil then
+ return
+ end
+ _cameraFollow(_config.camera, Player)
+ _cameraHideBlockingObjects(_config.camera)
+ _moveNoPhysics(dt)
+end)
+LocalEvent:Listen(LocalEvent.Name.PointerUp, function(pointerEvent)
+ if not instance.pfMap then
+ return
+ end
+ _pathTo(pointerEvent)
+end)
+LocalEvent:Listen(LocalEvent.Name.PointerWheel, function(value)
+ if not instance.camera then
+ return
+ end
+ _cameraZoom(instance.camera, value)
+end)
+LocalEvent:Listen(LocalEvent.Name.PointerDrag, function(pointerEvent)
+ if not instance.camera then
+ return
+ end
+ _cameraRotate(instance.camera, pointerEvent)
+end)
+
+multi:onAction("showCursor", function(_, data)
+ _showCursor(Players[data.id], data.pos)
+end)
+
+-- INIT FUNCTIONS
+pointAndClick.init = function(_, p)
+ Client.DirectionalPad = nil
+ Client.AnalogPad = nil
+ Pointer.Drag = nil
+
+ if not _config then
+ _setConfig()
+ end
+ _initializePlayer(p, _config.camera)
+ if not p == Player then
+ return
+ end
+ _initializeMap(_config.map)
+end
+
+_setConfig = function(conf)
+ config = require("config"):merge(defaultConfig, conf)
+ if not config.map then
+ config.map = Map
+ end
+ if not config.camera then
+ config.camera = Camera
+ end
+end
+
+_initializePlayer = function(p, c)
+ if p == Player then
+ print("init player")
+ instance.camera = _initializeCamera(c)
+ p.walkFX = particles:newEmitter({
+ life = function()
+ return 0.3
+ end,
+ velocity = function()
+ local v = Number3(5 + math.random() * 5, 5, 0)
+ v:Rotate(0, math.random() * math.pi * 2, 0)
+ return v
+ end,
+ color = function()
+ return Color(255, 255, 255, 128)
+ end,
+ acceleration = function()
+ return -Config.ConstantAcceleration
+ end,
+ collidesWithGroups = function()
+ return { 1 }
+ end,
+ })
+ p.walkFX:SetParent(p)
+ p.walkFX.LocalPosition = { 0, 0, 0 }
+ end
+ _cursorCreate(p)
+end
+
+_initializeMap = function()
+ instance.pfMap = pathfinding.createPathfindingMap()
+end
+
+_initializeCamera = function(c)
+ Fog.On = false
+ -- c:SetModeFree()
+ -- c.zoom = defaultConfig.zoomDefault
+ return c
+end
+
+-- CAMERA FUNCTIONS
+_cameraZoom = function(c, value)
+ if c.zoom + config.zoomSpeed * value < config.zoomMin then
+ c.zoom = config.zoomMin
+ elseif c.zoom + config.zoomSpeed * value > config.zoomMax then
+ c.zoom = config.zoomMax
+ else
+ c.zoom = c.zoom + config.zoomSpeed * value
+ end
+end
+
+_cameraFollow = function(c, t)
+ -- c.Position.X, c.Position.Z = t.Position.X + config.cameraOffset.X, t.Position.Z + config.cameraOffset.Z
+ -- c.Position.Y = c.zoom
+ -- c.Forward = Number3(t.Position.X, config.cameraFocusY, t.Position.Z) - c.Position
+end
+
+_cameraRotate = function(c, pe)
+ c:RotateLocal({ 0, 1, 0 }, pe.DX * 0.01)
+end
+
+_cameraHideBlockingObjects = function(c)
+ -- local box = Box(c.Position - Map.Scale, c.Position + Map.Scale)
+ -- local dist = math.min((c.Position - Player.Position).Length, BOXCAST_DIST)
+ -- local impact = box:Cast(c.Forward, dist, config.obstacleGroups)
+ -- if not impact.Object then
+ -- return
+ -- end
+
+ -- impact.Object.PrivateDrawMode = 1
+ -- if not impact.Object.drawTimer then
+ -- impact.Object.drawTimer = Timer(0.5, function()
+ -- impact.Object.PrivateDrawMode = 0
+ -- impact.Object.drawTimer = nil
+ -- end)
+ -- end
+end
+
+-- MOVEMENT FUNCTIONS
+_pathTo = function(pointerEvent)
+ local impact = pointerEvent:CastRay(config.mapGroups)
+ if impact.Block == nil then
+ return
+ end
+
+ local origin = Map:WorldToBlock(Player.Position)
+ origin = Number3(math.floor(origin.X), math.floor(origin.Y), math.floor(origin.Z))
+ local destination = impact.Block.Position / Map.Scale + OFFSET_Y
+ local path = pathfinding.findPath(origin, destination, instance.pfMap)
+ if not path then
+ return
+ end
+
+ instance.pfPath = path
+ instance.pfStep = #path - 1 --skipping the first block, which we're standing on
+
+ destination = impact.Block.Position + (OFFSET_XZ + OFFSET_Y) * Map.Scale
+ _showCursor(Player, destination)
+ _showPath(instance.pfPath)
+ _followPath(Player, instance.pfPath, instance.pfStep)
+end
+
+_showCursor = function(p, pos)
+ if p == Player then
+ multi:action("showCursor", { id = Player.ID, pos = pos })
+ end
+ _cursorSet(instance.cursors[p.ID], pos)
+end
+
+_followPath = function(p, path, idx)
+ if not path[idx] then
+ return
+ end
+ if instance.cursorPath[idx + 1] then
+ instance.cursorPath[idx + 1]:RemoveFromParent()
+ end
+ p.destination = (Number3(path[idx].x, path[idx].y, path[idx].z) + OFFSET_XZ) * Map.Scale
+ p.Forward = { p.destination.X - p.Position.X, 0, p.destination.Z - p.Position.Z } -- just to know where to face
+end
+
+_cursorCreate = function(p)
+ ease = require("ease")
+ Object:Load("aduermael.selector", function(o)
+ o:SetParent(World)
+ o.Physics = PhysicsMode.Trigger
+ o.CollisionGroups = nil
+ o.CollidesWithGroups = config.playerGroups
+ o.Scale = 0
+
+ o.Tick = function(self, dt)
+ self:RotateLocal(0, dt, 0)
+ end
+
+ o.OnCollisionBegin = function(self, _)
+ ease:cancel(self)
+ ease:outQuad(self, 0.5).Scale = { 0, 0, 0 }
+ end
+
+ if p ~= Player then
+ o.PrivateDrawMode = 1
+ end
+
+ print("Player ID: " .. p.ID)
+ instance.cursors[p.ID] = o
+ end)
+end
+
+_cursorSet = function(cursor, pos)
+ ease:cancel(cursor)
+ -- cursor.Position = pos
+ ease:outQuad(cursor, 0.5).Scale = CURSOR_SCALE
+end
+
+_showPath = function(path)
+ local createPath = function(v)
+ local s = MutableShape()
+ s:AddBlock(Color.White, 0, 0, 0)
+ s.Pivot = { 0.5, 0.5, 0.5 }
+ s.Physics = PhysicsMode.Trigger
+ s.CollidesWithGroups = { 2 }
+ s.CollisionGroups = {}
+ s.OnCollisionBegin = function(self, _)
+ self:RemoveFromParent()
+ end
+ s:SetParent(World)
+ s.Position = (Number3(v.x, v.y, v.z) + OFFSET_XZ) * Map.Scale
+
+ return s
+ end
+
+ for _, v in pairs(instance.cursorPath) do
+ v:RemoveFromParent()
+ end
+ instance.cursorPath = {}
+
+ for _, v in pairs(path) do
+ local c = createPath(v)
+ table.insert(instance.cursorPath, c)
+ end
+end
+
+_moveNoPhysics = function(dt)
+ if Player.Physics then
+ Player.Physics = PhysicsMode.Trigger
+ end
+ if not instance.pfPath then
+ return
+ end
+
+ local dest = Number3(Player.destination.X, 0, Player.destination.Z)
+ local pos = Number3(Player.Position.X, 0, Player.Position.Z)
+ local test = math.sqrt(2)
+ if (dest - pos).Length < test then --checking on a 2D plane only
+ Player.Position = Player.destination
+ instance.pfStep = instance.pfStep - 1
+ if instance.pfPath[instance.pfStep] ~= nil then
+ _followPath(Player, instance.pfPath, instance.pfStep)
+ else
+ if Player.Animations.Idle.IsPlaying then
+ return
+ end
+ if Player.Animations.Walk.IsPlaying then
+ Player.Animations.Walk:Stop()
+ end
+ Player.Animations.Idle:Play()
+ end
+ else
+ _followPath(Player, instance.pfPath, instance.pfStep)
+ Player.moveDir = (Player.destination - Player.Position):Normalize()
+ Player.Position = Player.Position + Player.moveDir * config.moveSpeed * dt
+ local r = math.random()
+ if Player.walkFX and r > 0.8 then
+ Player.walkFX:spawn(1)
+ end
+ if Player.Animations.Walk.IsPlaying then
+ return
+ end
+ if Player.Animations.Idle.IsPlaying then
+ Player.Animations.Idle:Stop()
+ end
+ Player.Animations.Walk:Play()
+ end
+end
+
+----------------------
+--module "pathfinding"
+----------------------
+pathfinding = {}
+
+-- Add defaultConfig, setConfig...
+pathfinding.createPathfindingMap = function(config) -- Takes the map as argument
+ local defaultPathfindingConfig = {
+ map = Map,
+ pathHeight = 3,
+ pathLevel = 1,
+ obstacleGroups = { 3 },
+ }
+
+ _config = require("config"):merge(defaultPathfindingConfig, config)
+
+ local map2d = {} --create a 2D map to store which blocks can be walked on with a height map
+ local box = Box({ 0, 0, 0 }, _config.map.Scale) --create a box the size of a block
+ local dist = _config.map.Scale.Y * _config.pathHeight -- check for ~3 blocks up
+ local dir = Number3.Up -- checking up
+ for x = 0, _config.map.Width do
+ map2d[x] = {}
+ for z = 0, _config.map.Depth do
+ local h = _config.pathLevel -- default height is the path level
+ for y = 0, _config.map.Height do
+ if _config.map:GetBlock(x, y, z) then
+ h = y + 1
+ end -- adjust height by checking all blocks on the column to
+ end
+ box.Min = { x, h, z } * _config.map.Scale
+ box.Max = box.Min + _config.map.Scale
+ local data = h -- by default, store the height for the pathfinder
+ local impact = box:Cast(dir, dist, _config.obstacleGroups) -- check object above the retained height
+ if impact.Object ~= nil then
+ data = impact.Object
+ end -- if any, store the object for further use
+ map2d[x][z] = data
+ end
+ end
+ return map2d
+end
+
+pathfinding.findPath = function(origin, destination, map)
+ local kCount = 500
+ local diagonalsAllowed = true
+ local kClimb = 1
+
+ local directNeighbors = {
+ { x = -1, z = 0 },
+ { x = 0, z = 1 },
+ { x = 1, z = 0 },
+ { x = 0, z = -1 },
+ }
+ local diagonalNeighbours = {
+ { x = -1, z = 0 },
+ { x = 0, z = 1 },
+ { x = 1, z = 0 },
+ { x = 0, z = -1 },
+ { x = -1, z = -1 },
+ { x = 1, z = -1 },
+ { x = 1, z = 1 },
+ { x = -1, z = 1 },
+ }
+
+ local createNode = function(x, y, z, parent)
+ local node = {}
+ node.x, node.y, node.z, node.parent = x, y, z, parent
+ return node
+ end
+
+ local heuristic = function(x1, x2, z1, z2, y1, y2)
+ local dx = x1 - x2
+ local dz = z1 - z2
+ local dy = y1 - y2
+ local h = dx * dx + dz * dz + dy * dy
+ return h
+ end
+
+ local elapsed = function(parentNode)
+ return parentNode.g + 1
+ end
+
+ local calculateScores = function(node, endNode)
+ node.g = node.parent and elapsed(node.parent) or 0
+ node.h = heuristic(node.x, endNode.x, node.z, endNode.z, node.y, endNode.y)
+ node.f = node.g + node.h
+ end
+
+ local listContains = function(list, node)
+ for _, v in ipairs(list) do
+ if v.x == node.x and v.y == node.y and v.z == node.z then
+ return v
+ end
+ end
+ return false
+ end
+
+ local getChildren = function(node, map)
+ local children = {}
+ local neighbors = diagonalsAllowed and diagonalNeighbours or directNeighbors
+ local parentHeight = map[node.x][node.z]
+
+ for _, neighbor in ipairs(neighbors) do
+ local x = node.x + neighbor.x
+ local z = node.z + neighbor.z
+ local y = map[x][z]
+ if type(y) == "integer" and math.abs(y - parentHeight) <= kClimb then
+ table.insert(children, { x = x, y = y, z = z })
+ end
+ end
+ return children
+ end
+
+ -- Init lists to run the nodes & a count as protection for while
+ local openList = {}
+ local closedList = {}
+ local count = 0
+ -- Setup startNode and endNode
+ local endNode = createNode(destination.X, destination.Y, destination.Z, nil)
+ local startNode = createNode(origin.X, origin.Y, origin.Z, nil)
+ -- Calculate starting node score
+ calculateScores(startNode, endNode)
+ -- Insert the startNode as first node to examine
+ table.insert(openList, startNode)
+ -- While there are nodes to examine and the count is under kCount (and the function did not return)
+ while #openList > 0 and count < kCount do
+ count = count + 1
+ -- Sort openList with ascending f
+ table.sort(openList, function(a, b)
+ return a.f > b.f
+ end)
+ -- Examine the last node
+ local currentNode = table.remove(openList)
+ table.insert(closedList, currentNode)
+ if listContains(closedList, endNode) then
+ local path = {}
+ local current = currentNode
+ while current ~= nil do
+ table.insert(path, current)
+ current = current.parent
+ end
+ return path
+ end
+ -- Generate children based on map and test function
+ local children = getChildren(currentNode, map)
+ for _, child in ipairs(children) do
+ -- Create child node
+ local childNode = createNode(child.x, child.y, child.z, currentNode)
+ -- Check if it's already been examined
+ if not listContains(closedList, childNode) then
+ -- Check if it's already planned to be examined with a bigger f (meaning further away)
+ if not listContains(openList, childNode) then -- or self.listContains(openList, childNode).f > childNode.f then
+ calculateScores(childNode, endNode)
+ table.insert(openList, childNode)
+ end
+ end
+ end
+ end
+ return false
+end
\ No newline at end of file
diff --git a/cubzh/scripts/procedural_maze.lua b/cubzh/scripts/procedural_maze.lua
new file mode 100644
index 0000000..5de5ffe
--- /dev/null
+++ b/cubzh/scripts/procedural_maze.lua
@@ -0,0 +1,1089 @@
+Config = {
+ Items = {
+ -- the same as opponentsList
+ "kooow.white_yeti",
+ "kooow.small_boar",
+ "kooow.ribberhead_dino",
+ "kooow.purple_demon",
+ "kooow.lion",
+ "kooow.brown_bear",
+ "kooow.pinky_demon",
+ "kooow.purple_bat",
+ "kooow.beagle_dog",
+ "kooow.lava_golem",
+ "kooow.grey_blue_robot",
+ "kooow.snowman_with_broom",
+ "kooow.gorilla",
+ "kooow.young_piggy",
+ "kooow.blocky_giant",
+ "kooow.grey_flying_dragon",
+ "kooow.cactus_creature",
+ "kooow.cow",
+ -- becouse i cannot get name from Items
+ "claire.torch",
+ "voxels.gate",
+ "petroglyph.hp_bar",
+ "petroglyph.block_red",
+ "petroglyph.block_green",
+ "tantris.craft_item",
+ "tantris.craft_button",
+ "xavier.win",
+ }
+}
+
+local opponentsList = {
+ "kooow.white_yeti",
+ "kooow.small_boar",
+ "kooow.ribberhead_dino",
+ "kooow.purple_demon",
+ "kooow.lion",
+ "kooow.brown_bear",
+ "kooow.pinky_demon",
+ "kooow.purple_bat",
+ "kooow.beagle_dog",
+ "kooow.lava_golem",
+ "kooow.grey_blue_robot",
+ "kooow.snowman_with_broom",
+ "kooow.gorilla",
+ "kooow.young_piggy",
+ "kooow.blocky_giant",
+ "kooow.grey_flying_dragon",
+ "kooow.cactus_creature",
+ "kooow.cow",
+}
+
+-- becouse i cannot use https://docs.cu.bzh/reference/items - local s = Shape(Items[ "aduermael.pumpkin" ])
+local randomOpponent = {}
+local miniOpponents = {}
+local miniHeight = 40
+local miniWidth = 40
+
+-- DEBUG
+local DEBUG = false
+
+-- Corridor stuff
+local NUMBER_OF_CORRIDORS = #opponentsList
+local CORRIDOR_LENGTH = 150
+local WALL_HEIGHT = 40
+local TORCH_SPACING = 40
+local corridors = {}
+
+-- Door stuff
+local PADDING = 8 -- used for UI elements
+local doors = {}
+
+-- HP bars settings
+hpBars = {} -- may be we can use only 2 parameter player and opponent
+playerMaxHP = 100
+playerHPDuration = .5 -- update animation in seconds
+playerHPNegativeShake = .3
+playerHPPositiveBump = .6
+damageToApply = nil
+
+-- Misc
+local BROWN = Color(139, 69, 19)
+local POINTER_OFFSET = 20
+local GENERATED_ITEM_COLLISION_GROUP = 5
+faceNormals = {
+ [Face.Back] = Number3(0, 0, -1), [Face.Bottom] = Number3(0, -1, 0), [Face.Front] = Number3(0, 0, 1),
+ [Face.Left] = Number3(-1, 0, 0), [Face.Right] = Number3(1, 0, 0), [Face.Top] = Number3(0, 1, 0)
+}
+
+Client.OnStart = function()
+ -- Add player to game
+ World:AddChild(Player, true)
+
+ -- Initialize the corridors and gates
+ makeMap()
+
+ -- Set up the UI
+ ease = require "ease"
+ ui = require "uikit"
+ sfx = require "sfx"
+ controls = require "controls"
+ controls:setButtonIcon("action1", "⬆️")
+ hierarchyActions = require "hierarchyactions"
+
+ -- ui : score, opponent.name, miniOpponent
+ uiScore = ui:createText("", Color.White)
+ uiScore.pos.X = (Screen.Width - Screen.SafeArea.Right - uiScore.Width - PADDING) / 2
+ uiScore.pos.Y = Screen.Height - Screen.SafeArea.Top - uiScore.Height - PADDING
+
+ uiOpponent = ui:createText("", Color.White)
+ uiOpponent.pos.X = 0
+ uiOpponent.pos.Y = Screen.SafeArea.Top + uiOpponent.Height + PADDING
+
+ if (Screen.Width / NUMBER_OF_CORRIDORS < miniWidth) then
+ miniWidth = Screen.Width / NUMBER_OF_CORRIDORS
+ end
+ local node = ui:createNode()
+ for i = 1, NUMBER_OF_CORRIDORS, 1 do
+ mOp = ui:createShape(Shape(Items[i]))
+ mOp.Height = miniHeight
+ mOp.Width = miniWidth
+ mOp.pos.X = (i - 1) * miniWidth
+ mOp:setParent(node)
+ table.insert(miniOpponents, mOp)
+ end
+
+ -- restartButton = ui:Button("Restart!")
+ -- restartButton.OnRelease = initGame
+ initGame()
+end
+
+function initGame()
+ paused = false
+
+ -- Game variable init start
+ level = 0
+ generatedImage = nil
+ gens = {}
+
+ uiScore.Text = "Level: " .. level
+
+ -- HP bars array init
+ for i = 1, NUMBER_OF_CORRIDORS + 1, 1 do
+ hpBars[i] = {}
+ end
+
+ -- Start Information
+ showInstructions()
+ showWelcomeHint()
+
+ -- bad idea, but i cannot add opponentList to Items and cannot mix or fint element of opponentList in Items....
+ createRandomSpawnList()
+ opponent = spawnRandomOpponent()
+
+ -- update door
+ closeAllDoor()
+
+ dropPlayer(Player)
+ Player.healthIndex = 0
+ Player.name = "player"
+ attachHPBar(Player)
+
+ for i = 1, NUMBER_OF_CORRIDORS, 1 do
+ miniOpponents[i]:show()
+ end
+
+ -- Player.IsHidden = false
+ -- Pointer:Hide()
+ -- UI.Crosshair = true
+end
+
+function endGame(action)
+ paused = true
+ -- Player.IsHidden = true
+ -- Pointer:Show()
+ -- UI.Crosshair = false
+
+ -- restartButton:Add(Anchor.HCenter, Anchor.VCenter)
+
+ initGame()
+end
+
+Pointer.Click = function(pointerEvent)
+ hideWelcomeHint()
+ hideInstructions()
+ showMenu(pointerEvent)
+end
+
+Client.OnPlayerJoin = function(p)
+ if p == Player then
+ return
+ end
+ dropPlayer(p)
+end
+
+Client.Tick = function(dt)
+ if paused then
+ return
+ end
+ -- Detect if player is falling,
+ -- drop it above the map when it happens.
+ if Player.Position.Y < -500 then
+ dropPlayer(Player)
+ Player:TextBubble("💀 Oops!")
+ end
+ updateHPBars(dt)
+ launchToolOnOpponent(dt)
+end
+
+function dropPlayer(p)
+ World:AddChild(p)
+ p.Position = Number3(corridors[1].Position.X + 10, 10, corridors[1].Position.Z + 20)
+ -- Rotate player 90 degres to the right to face the corridor
+ p.Rotation = { 0, math.pi / 2, 0 }
+ p.Velocity = { 0, 0, 0 }
+end
+
+function cancelMenu()
+ -- hideMenu()
+ -- showInstructions()
+end
+
+-- Function to create and set up the map
+function closeAllDoor()
+ for i = 1, NUMBER_OF_CORRIDORS do
+ if doors[i].closed == false then
+ doors[i].closed = true
+ doors[i].Rotation = { 0, doors[i].rotClosed, 0 }
+ end
+ end
+end
+
+function makeMap()
+ for i = 1, NUMBER_OF_CORRIDORS do
+ corridors[i] = makeCorridor((i - 1) * CORRIDOR_LENGTH, i)
+ end
+
+ winTextSet(doors[NUMBER_OF_CORRIDORS].Position.X,
+ doors[NUMBER_OF_CORRIDORS].Position.Y / 2,
+ doors[NUMBER_OF_CORRIDORS].Position.Z / 2,
+ 2, 0, math.pi / 2, 0)
+end
+
+function winTextSet(x, y, z, scale, x_rot, y_rot, z_rot)
+ wintext = Shape(Items.xavier.win)
+ wintext.Position = { x, y, z }
+ wintext.Rotation = { x_rot, y_rot, z_rot }
+ wintext.Scale = scale
+ wintext.CollisionGroups = { GENERATED_ITEM_COLLISION_GROUP }
+ corridors[1]:AddChild(wintext)
+end
+
+function createDoorAtEndOfCorridor(offset, ix)
+ local door = Shape(Items.voxels.gate)
+ door.Position = Number3(offset + CORRIDOR_LENGTH, door.Height / 2, door.Width)
+ door.Rotation = Number3(0, math.pi / 2, 0) -- Adjusted to face the corridor
+ door.Pivot = { 0, door.Height * 0.5, door.Depth * 0.5 }
+ door.isDoor = true
+ door.number = ix -- If you have more doors, increment this number for each
+ door.closed = true
+ door.rotClosed = math.pi / 2 -- The rotation when the door is closed
+ doors[door.number] = door
+ World:AddChild(door) -- for what? if we attach it to corridor
+ return door
+end
+
+function makeCorridor(offset, ix)
+ local corridor = Object()
+ corridor.CollisionGroups = { GENERATED_ITEM_COLLISION_GROUP }
+ World:AddChild(corridor)
+
+ -- Add a door at the end of the corridor
+ doors[ix] = createDoorAtEndOfCorridor(offset, ix)
+ corridor:AddChild(doors[ix])
+
+ local CORRIDOR_WIDTH = doors[ix].Width
+ -- Create floor
+ local floor = MutableShape()
+ for x = offset, offset + CORRIDOR_LENGTH - 1 do
+ for z = 1, CORRIDOR_WIDTH - 2 do
+ -- Subtracting 2 since we're building walls on both sides
+ floor:AddBlock(BROWN, x, 0, z)
+ end
+ end
+ floor.CollisionGroups = { GENERATED_ITEM_COLLISION_GROUP }
+ corridor:AddChild(floor)
+
+ -- Create walls
+ local leftWall = MutableShape()
+ local rightWall = MutableShape()
+ for x = offset, offset + CORRIDOR_LENGTH - 1 do
+ for y = 1, WALL_HEIGHT do
+ -- Left wall
+ leftWall:AddBlock(BROWN, x, y, 0)
+ -- Right wall
+ rightWall:AddBlock(BROWN, x, y, CORRIDOR_WIDTH - 1)
+ end
+ end
+ leftWall.CollisionGroups = { GENERATED_ITEM_COLLISION_GROUP }
+ rightWall.CollisionGroups = { GENERATED_ITEM_COLLISION_GROUP }
+ corridor:AddChild(leftWall)
+ corridor:AddChild(rightWall)
+
+ -- Add torches alongside the walls, spaced evenly
+ for x = offset, offset + CORRIDOR_LENGTH - 1, TORCH_SPACING do
+ -- Left wall torches
+ local leftTorch = addTorch(x, 10, 4) -- Position adjusted for the left wall
+ -- Right wall torches
+ local rightTorch = addTorch(x, 10, CORRIDOR_WIDTH - 5) -- Position adjusted for the right wall
+ corridor:AddChild(leftTorch)
+ corridor:AddChild(rightTorch)
+ end
+
+ -- Add the "Craft Button" sign on the Left wall
+ local infoPanel = Shape(Items.tantris.craft_item)
+ infoPanel.Position = Number3(offset + 15, 15, CORRIDOR_WIDTH - 1) -- Position adjusted for scale
+ -- Scale it down to 1/2 size
+ infoPanel.Scale = 0.5
+ corridor:AddChild(infoPanel)
+
+ local craftButton = Shape(Items.tantris.craft_button)
+ craftButton.Position = Number3(offset + 27, 9, CORRIDOR_WIDTH - 2) -- Position adjusted for scale
+ craftButton.isCraftButton = true
+ -- Add it to the collision group
+ craftButton.CollisionGroups = { GENERATED_ITEM_COLLISION_GROUP }
+ corridor:AddChild(craftButton)
+
+ return corridor
+end
+
+function addTorch(x, y, z)
+ local torch = Shape(Items.claire.torch)
+ torch.Position = Number3(x, y, z) -- Position adjusted for scale
+ return torch
+end
+
+-- door stuff
+function openDoor(doorNumber)
+ local door = doors[doorNumber]
+ if door and door.isDoor and door.closed then
+ doorAction(door, "open")
+ end
+end
+
+-- Function to perform actions on doors
+function doorAction(object, action)
+ if action == "toggle" then
+ object.closed = not object.closed
+ elseif action == "close" then
+ object.closed = true
+ elseif action == "open" then
+ object.closed = false
+ end
+
+ -- Play sound effects based on the action
+ if object.closed then
+ sfx("doorclose_1", object.Position, 0.5)
+ else
+ sfx("dooropen_1", object.Position, 0.5)
+ end
+
+ -- Set physics to trigger to allow for interaction, but not immediately solid
+ object.Physics = PhysicsMode.Trigger
+ object.colliders = 0
+
+ -- Set the door's rotation based on whether it's open or closed
+ if object.closed then
+ object.Rotation = { 0, object.rotClosed, 0 }
+ else
+ object.Rotation = { 0, object.rotClosed + math.pi * 0.5, 0 }
+ end
+end
+
+-- "Craft Menu" (Just a text input UI for now)
+function displayCraftMenu(pointerEvent, impact, pos)
+ local screenPos = Number2(pointerEvent.X * Screen.Width, pointerEvent.Y * Screen.Height)
+
+ -- Modify the position to be at y = 1 and z - 3 vs the pos
+ pos = Number3(pos.X, 1, pos.Z - 6)
+ showTarget(impact, pos)
+
+ prompt = ui:createNode()
+ local input = ui:createTextInput(nil, "What do you want?")
+ input:setParent(prompt)
+ input:focus()
+
+ local send = function()
+ if input.Text ~= "" then
+ imageQuery(input.Text, impact, pos)
+ sfx("modal_3", { Spatialized = false, Pitch = 2.0 })
+ end
+ prompt:remove()
+ prompt = nil
+ deleteTarget()
+ end
+
+ input.onSubmit = send
+
+ local sendBtn = ui:createButton("✅", { textSize = "big" })
+ sendBtn:setParent(prompt)
+ sendBtn.onRelease = send
+
+ input.Height = sendBtn.Height
+ input.Width = 250
+ sendBtn.pos.X = input.Width
+
+ local width = input.Width + sendBtn.Width
+ local height = sendBtn.Height
+
+ local px = screenPos.X - width * 0.5
+ if px < Screen.SafeArea.Left + PADDING then
+ px = Screen.SafeArea.Left + PADDING
+ end
+ if px > Screen.Width - Screen.SafeArea.Right - width - PADDING then
+ px = Screen.Width - Screen.SafeArea.Right - width - PADDING
+ end
+
+ local py = screenPos.Y + POINTER_OFFSET
+ if py < Screen.SafeArea.Bottom + PADDING then
+ py = Screen.SafeArea.Bottom + PADDING
+ end
+ if py > Screen.Height - Screen.SafeArea.Top - height - PADDING then
+ py = Screen.Height - Screen.SafeArea.Top - height - PADDING
+ end
+
+ prompt.pos.X = px
+ prompt.pos.Y = py
+end
+
+-- Image query
+function imageQuery(message, impact, pos)
+ if impact then
+ pos = pos + { 0, 1, 0 }
+
+ local e = Event()
+ e.id = math.floor(math.random() * 1000000)
+ e.pos = pos
+ e.rotY = Player.Rotation.Y
+ e.action = "requestImageGeneration" -- This is the important bit; it tells the server to generate an image
+ local _, name = splitAtFirst(opponent.ItemName, '.') -- Assuming this contains the "opponent" name
+ e.opponentName = name
+ e.userInput = message
+ e.m = message
+ e:SendTo(Server)
+
+ local e2 = Event()
+ e2.action = "otherGen"
+ e2.id = e.id
+ e2.m = message
+ e2.pos = e.pos
+ e2.rotY = e.rotY
+ e2:SendTo(OtherPlayers)
+
+ makeBubble(e2)
+ end
+end
+
+-- Spawn an opponent from the opponentsList
+function shuffleList(list)
+ math.randomseed(os.time()) -- init random generator from time (like C++)
+ local len = #list
+ for i = len, 2, -1 do
+ local j = math.random(i)
+ list[i], list[j] = list[j], list[i]
+ end
+ return list
+end
+
+function createRandomSpawnList()
+ for i = 1, NUMBER_OF_CORRIDORS, 1 do
+ randomOpponent[i] = i
+ end
+ shuffleList(randomOpponent)
+end
+
+function spawnRandomOpponent()
+ -- Randomly select an opponent from the Config.Items list
+ local opponentIndex = randomOpponent[level + 1] -- math.random(1, #opponentsList)
+ local opponentKey = opponentsList[opponentIndex]
+
+ local opponentShape = Shape(Items[opponentIndex], { includeChildren = true })
+
+ -- Like above, but iterating over all children to find the lowest y value in child.BoundingBox.Min.Y
+ local minY = 1000
+ local minX = 1000
+ local minZ = 1000
+ -- using hierarchyActions
+ hierarchyActions:applyToDescendants(opponentShape, function(sh)
+ local bb = sh:ComputeWorldBoundingBox()
+ if bb.Min.Y < minY then
+ minY = bb.Min.Y
+ end
+ if bb.Min.X < minX then
+ minX = bb.Min.X
+ end
+ end)
+
+ -- Set opponent properties and add it to the world
+ -- opponentShape:SetParent(World)
+ World:AddChild(opponentShape)
+
+ opponentShape.Position = { doors[level + 1].Position.X + minX, -minY, (doors[level + 1].Position.Z - opponentShape.Width) / 2 }
+ opponentShape.Rotation = { 0, -math.pi / 2, 0 }
+ opponentShape.IsHidden = false
+ opponentShape.tag = "opponent"
+ opponentShape.name = opponentKey
+ -- split name to get only name without prefix
+ local _, opponentName = splitAtFirst(opponentKey, '.')
+ opponentShape.healthIndex = opponentIndex
+
+ --Add to collision group
+ opponentShape.CollisionGroups = Map.CollisionGroups
+ attachHPBar(opponentShape)
+ uiOpponent.Text = "Current opponent: " .. opponentName
+ return opponentShape
+end
+
+Client.DidReceiveEvent = function(e)
+ if paused then
+ return
+ end
+ if e.action == "imageIsGenerated" then
+ handleGeneratedImage(e)
+ elseif e.action == "damageToApply" then
+ damageToApply = e
+ end
+end
+
+function handleGeneratedImage(e)
+ local pos
+ local rotY
+
+ local bubble = gens[e.id]
+ if bubble then
+ pos = bubble.Position:Copy()
+ rotY = bubble.Rotation.Y
+ bubble.Tick = nil
+ if bubble.text then
+ bubble.text:RemoveFromParent()
+ end
+ bubble:RemoveFromParent()
+ gens[e.id] = nil
+ if e.vox == nil then
+ print("sorry, request failed!")
+ return
+ end
+ elseif e.pos then
+ pos = e.pos
+ rotY = e.rotY
+ else
+ return
+ end
+
+ local success = pcall(function()
+ if JSON:Encode(e.vox)[1] == "{" then
+ print(JSON:Encode(e.vox))
+ return
+ end
+ local s = Shape(e.vox)
+ s.CollisionGroups = Map.CollisionGroups
+ s.tag = "generatedImage" -- Tagging the object as a generated image
+ s.userInput = e.userInput
+ s.user = e.user
+ s:SetParent(World)
+
+ -- first block is not at 0,0,0
+ -- use collision box min to offset the pivot
+ local collisionBoxMin = s.CollisionBox.Min
+ local center = s.CollisionBox.Center:Copy()
+
+ center.Y = s.CollisionBox.Min.Y
+ s.Pivot = { s.Width * 0.5 + collisionBoxMin.X,
+ 0 + collisionBoxMin.Y,
+ s.Depth * 0.5 + collisionBoxMin.Z }
+ s.Position = pos
+ s.Rotation.Y = rotY
+ s.Physics = PhysicsMode.Dynamic
+
+ Timer(1, function()
+ s.Physics = PhysicsMode.TriggerPerBlock
+ s.CollisionGroups = { GENERATED_ITEM_COLLISION_GROUP }
+ firing = true
+ end)
+ generatedImage = s
+ sfx("waterdrop_2", { Position = pos })
+ end)
+ if not success then
+ sfx("twang_2", { Position = pos })
+ end
+end
+
+function weaponAction(action)
+ if generatedImage then
+ generatedImage:RemoveFromParent()
+ end
+ generatedImage = nil
+ print("" .. action)
+ firing = false
+end
+
+launchToolOnOpponent = function(dt)
+ if firing and generatedImage then
+ if opponent.IsHidden then
+ weaponAction("there is no opponent") -- don't work????
+ elseif (opponent.Position - generatedImage.Position).Length > CORRIDOR_LENGTH then
+ weaponAction("opponent is too far")
+ Player:TextBubble("opponent is too far. Use another button")
+ else
+ local targetPosition = opponent.Position -- Assume 'opponent' is accessible and has a position
+ local animationTime = 1.0 -- Duration to reach the target, adjust as needed
+ ease:outSine(generatedImage, animationTime).Position = targetPosition
+
+ -- Check if the tool has reached the opponent or if you need to stop firing
+ generatedImage.OnCollision = function(self, other)
+ if other == opponent then
+ applyDamage(damageToApply)
+ firing = false
+ -- destroy the tool
+ generatedImage:RemoveFromParent()
+ end
+ end
+ end
+ end
+end
+
+function applyDamage(e)
+ local target = Player
+ if e.targetName ~= "player" then
+ target = opponent
+ end
+
+ if target and hpBars[target.healthIndex + 1] and e.damageAmount then
+ local hpBarInfo = hpBars[target.healthIndex + 1]
+ -- Assuming damageAmount is the amount of HP to remove
+ local damageAmount = tonumber(e.damageAmount)
+ local currentPercentage = hpBarInfo.hpShape.LocalScale.Z / hpBarMaxLength
+ local currentHP = currentPercentage * playerMaxHP
+ -- Calculate new HP after taking damage
+ local newHP = math.max(currentHP - damageAmount, 0)
+ -- Update the target's HP visually
+ setHPBar(target, newHP, true) -- true to animate the HP bar change
+ -- Create a message to display the damage taken
+ Player:TextBubble(e.message)
+ -- if HP reaches 0, explode the target
+ if newHP <= 0 then
+ require("explode"):shapes(target)
+ -- Destroy the target
+ target:RemoveFromParent()
+ if target == opponent then
+ level = level + 1
+ miniOpponents[randomOpponent[level]]:hide()
+ uiScore.Text = "Level: " .. level
+ doorAction(doors[level], "open")
+
+ if level == NUMBER_OF_CORRIDORS then
+ opponent = nil
+ print("YOU WIN")
+ Player:TextBubble("Next Round")
+ endGame("YOU WIN") -- stop game........ if finish
+ else
+ opponent = spawnRandomOpponent()
+ end
+ elseif target == Player then
+ print("YOU LOSE")
+ Player:TextBubble("YOU Lose")
+ endGame("YOU Lose") -- stop game........ if finish
+ end
+ end
+ end
+end
+
+-- Target
+function showTarget(impact, pos)
+ if impact == nil then
+ return
+ end
+
+ if _target == nil then
+ local ms = MutableShape()
+ ms:AddBlock(Color.White, 0, 0, 0)
+
+ ms:AddBlock(Color.White, -2, 0, -2)
+ ms:AddBlock(Color.White, -2, 0, -1)
+ ms:AddBlock(Color.White, -1, 0, -2)
+
+ ms:AddBlock(Color.White, -2, 0, 2)
+ ms:AddBlock(Color.White, -2, 0, 1)
+ ms:AddBlock(Color.White, -1, 0, 2)
+
+ ms:AddBlock(Color.White, 2, 0, 2)
+ ms:AddBlock(Color.White, 2, 0, 1)
+ ms:AddBlock(Color.White, 1, 0, 2)
+
+ ms:AddBlock(Color.White, 2, 0, -2)
+ ms:AddBlock(Color.White, 2, 0, -1)
+ ms:AddBlock(Color.White, 1, 0, -2)
+
+ _target = Shape(ms)
+ _target.Pivot = { 0.5, 0.5, 0.5 }
+ _target.Physics = PhysicsMode.Disabled
+ end
+
+ _target.LocalScale = Number3(0, 0, 0)
+ _target.LocalPosition = pos
+ _target.Up = faceNormals[impact.FaceTouched] or Number3(0, 1, 0)
+ _target.Tick = function(o, dt)
+ o:RotateLocal(o.Up, dt)
+ end
+ _target:SetParent(World)
+ ease:outElastic(_target, 0.4).LocalScale = { 1.6, 1, 1.6 }
+end
+
+function deleteTarget()
+ if _target ~= nil then
+ _target:SetParent(nil)
+ end
+end
+
+function showMenu(pointerEvent)
+ hideMenu()
+
+ local impact = pointerEvent:CastRay(Map.CollisionGroups + { GENERATED_ITEM_COLLISION_GROUP })
+ if impact ~= nil then
+ if impact.Object and impact.Object.isCraftButton then
+ impact.Object.LocalPosition.Z = impact.Object.LocalPosition.Z + 1
+ displayCraftMenu(pointerEvent, impact, impact.Object.Position)
+ Timer(0.2, function()
+ impact.Object.LocalPosition.Z = impact.Object.LocalPosition.Z - 1
+ impact.Object.pushed = false
+ end)
+ end
+ end
+end
+
+function hideMenu()
+ if createButton then
+ createButton:remove()
+ createButton = nil
+ end
+ if prompt then
+ prompt:remove()
+ prompt = nil
+ end
+ if itemDetails then
+ itemDetails:remove()
+ itemDetails = nil
+ end
+ deleteTarget()
+end
+
+Client.OnChat = function(message)
+ local e = Event()
+ e.action = "chat"
+ e.msg = message
+ e:SendTo(Players)
+end
+
+-- Server code
+Server.OnStart = function()
+ gens = {}
+ encounterEvents = {}
+ firing = false
+end
+
+Server.OnPlayerJoin = function(p)
+ print("Player joined: " .. p.Username)
+ Timer(2, function()
+ for _, d in ipairs(gens) do
+ local headers = {}
+ headers["Content-Type"] = "application/octet-stream"
+ HTTP:Get(d.url, headers, function(data)
+ local e = Event()
+ e.vox = data.Body
+ e.id = d.e.id
+ e.pos = d.e.pos
+ e.rotY = d.e.rotY
+ e.userInput = d.e.userInput
+ e.user = d.e.user
+ e.action = "imageIsGenerated"
+ e:SendTo(p)
+ end)
+ end
+ end)
+end
+
+Server.DidReceiveEvent = function(e)
+ if e.action == "requestImageGeneration" then
+ local eMy = Event()
+ eMy.opponentName = e.opponentName
+ eMy.userInput = e.userInput
+ resolveEncounter(eMy)
+
+ -- Step 2: Continue with the image generation request
+ local headers = {}
+ local apiURL = "https://api.voxdream.art"
+
+ headers["Content-Type"] = "application/json"
+ HTTP:Post(apiURL .. "/pixelart/vox", headers, { userInput = e.userInput }, function(data)
+ local body = JSON:Decode(data.Body)
+ if not body.urls or #body.urls == 0 then
+ print("Error: can't generate content.")
+ return
+ end
+ voxURL = apiURL .. "/" .. body.urls[1]
+ table.insert(gens, { e = e, url = voxURL })
+
+ local headers = {}
+ headers["Content-Type"] = "application/octet-stream"
+ HTTP:Get(voxURL, headers, function(data)
+ local e2 = Event()
+ e2.vox = data.Body
+ e2.user = e.Sender.Username
+ e2.userInput = e.userInput
+ e2.id = e.id
+ e2.action = "imageIsGenerated"
+ e2:SendTo(Players)
+ end)
+ end)
+ else
+ print("Unknown action: " .. e.action)
+ end
+end
+
+function resolveEncounter(e)
+ -- Construct the URL for the encounter resolution
+ local apiURL = "https://gig.ax/api/encounter/"
+ apiURL = apiURL .. "?opponent=" .. e.opponentName .. "&tool=" .. e.userInput
+
+ HTTP:Get(apiURL, {}, function(data)
+ local result = JSON:Decode(data.Body)
+
+ if result and result.operations then
+ encounterEvents = processEncounterResult(result)
+ -- Send all events to all players
+ for _, event in ipairs(encounterEvents) do
+ event:SendTo(Players)
+ end
+ else
+ print("Error: can't resolve encounter or no operations returned.")
+ end
+ end)
+end
+
+function processEncounterResult(result)
+ local events = {}
+ for _, operation in ipairs(result.operations) do
+ if operation.name == "HURT" then
+ local targetName = operation.parameters[1]
+ local damageAmount = tonumber(operation.parameters[2])
+
+ local hpUpdateEvent = Event()
+ hpUpdateEvent.action = "damageToApply"
+ hpUpdateEvent.targetName = targetName
+ hpUpdateEvent.damageAmount = damageAmount
+ hpUpdateEvent.message = result.description.text
+ table.insert(events, hpUpdateEvent)
+ elseif operation.name == "NOTHING" then
+ local updateEvent = Event()
+ updateEvent.action = "no_effect"
+ updateEvent.description = result.description.text
+ table.insert(events, updateEvent)
+ end
+ end
+
+ return events
+end
+
+
+-- #### HP Bar stuff #####################
+-- create and attach HP bar to given player
+attachHPBar = function(obj)
+ local frame = Shape(Items.petroglyph.hp_bar)
+ obj:AddChild(frame)
+ frame.LocalPosition = { 0, 35, 4.5 }
+ frame.LocalRotation = { 0, math.pi * .5, 0 } -- item was drawn along Z
+ if hpBarMaxLength == nil then
+ hpBarMaxLength = frame.Depth - 2
+ end
+
+ local hp = Shape(Items.petroglyph.block_red)
+ frame:AddChild(hp)
+ hp.Pivot.Z = 0
+ hp.LocalPosition.Z = -hpBarMaxLength * .5
+ hp.LocalScale.Z = hpBarMaxLength
+ hp.IsHidden = true
+
+ local hpFull = Shape(Items.petroglyph.block_green)
+ frame:AddChild(hpFull)
+ hpFull.LocalScale.Z = hpBarMaxLength
+
+ --print("Attaching HP bar to player at index: " .. obj.healthIndex .. " player name is " .. obj.name)
+ hpBar = {
+ player = obj,
+ startScale = hpBarMaxLength,
+ targetScale = hpBarMaxLength,
+ timer = 0,
+ hpShape = hp,
+ hpFullShape = hpFull,
+ frameShape = frame
+ }
+ --print("hpBar length: " .. hpBar.hpShape.LocalScale.Z)
+ hpBars[obj.healthIndex + 1] = hpBar
+end
+
+-- remove HP bar from player
+removeHPBar = function(player)
+ hpBars[player.healthIndex + 1] = {}
+end
+
+-- sets target HP for given player, animated by default
+setHPBar = function(player, hpValue, isAnimated)
+ local hp = hpBars[player.healthIndex + 1]
+ local v = clamp(hpValue / playerMaxHP, 0, 1)
+ if isAnimated or isAnimated == nil then
+ hp.startScale = hp.hpShape.LocalScale.Z
+ hp.targetScale = hpBarMaxLength * v
+ hp.timer = playerHPDuration
+ else
+ if v == 1.0 then
+ hp.hpFullShape.IsHidden = false
+ hp.hpShape.IsHidden = true
+ else
+ hp.hpFullShape.IsHidden = true
+ hp.hpShape.IsHidden = false
+ hp.hpShape.LocalScale.Z = hpBarMaxLength * v
+ end
+ hp.timer = 0
+ end
+end
+
+-- update all HP bars animation
+updateHPBars = function(dt, i)
+ local hp = nil
+ for i = 1, #hpBars, 1 do
+ hp = hpBars[i]
+ if hp.timer ~= nil and hp.timer > 0 then
+ hp.timer = hp.timer - dt
+
+ local delta = hp.targetScale - hp.startScale
+ local v = clamp(1 - hp.timer / playerHPDuration, 0, 1)
+ hp.hpShape.LocalScale.Z = hp.startScale + delta * v
+
+ local isFull = hp.hpShape.LocalScale.Z == hpBarMaxLength
+ hp.hpFullShape.IsHidden = not isFull
+ hp.hpShape.IsHidden = isFull
+
+ if delta < 0 then
+ hp.frameShape.LocalPosition.X = playerHPNegativeShake * math.sin(60 * hp.timer)
+ else
+ hp.frameShape.LocalPosition.Y = 26 + playerHPPositiveBump * math.sin(v * math.pi)
+ end
+ end
+ end
+end
+
+
+-- #### UI Stuff #####################
+
+-- shows instructions at the top left corner of the screen
+function showInstructions()
+ if instructions ~= nil then
+ instructions:show()
+ return
+ end
+
+ instructions = ui:createFrame(Color(0, 0, 0, 128))
+ local line1 = ui:createText("🎥 Drag to move camera", Color.White)
+ line1:setParent(instructions)
+ local line2 = ui:createText("☝️ Click on the CRAFT WEAPON button to attack the monster!", Color.White)
+ line2:setParent(instructions)
+ local line3 = ui:createText("🔎 Beat all the monsters to win the game.", Color.White)
+ line3:setParent(instructions)
+
+ instructions.parentDidResize = function()
+ local width = math.max(line1.Width, line2.Width, line3.Width) + PADDING * 2
+ local height = line1.Height + line2.Height + line3.Height + PADDING * 4
+ instructions.Width = width
+ instructions.Height = height
+ line1.pos = { PADDING, instructions.Height - PADDING - line1.Height, 0 }
+ line2.pos = line1.pos - { 0, line1.Height + PADDING, 0 }
+ line3.pos = line2.pos - { 0, line2.Height + PADDING, 0 }
+ instructions.pos = { Screen.SafeArea.Left + PADDING, Screen.Height - Screen.SafeArea.Top - instructions.Height - PADDING, 0 }
+ end
+ instructions:parentDidResize()
+end
+
+function hideInstructions()
+ if instructions ~= nil then
+ instructions:hide()
+ end
+end
+
+function showWelcomeHint()
+ if welcomeHint ~= nil then
+ return
+ end
+ welcomeHint = ui:createText("Click on the CRAFT WEAPON block!", Color(1.0, 1.0, 1.0), "big")
+ welcomeHint.parentDidResize = function()
+ welcomeHint.pos.X = Screen.Width * 0.5 - welcomeHint.Width * 0.5
+ welcomeHint.pos.Y = Screen.Height * 0.66 - welcomeHint.Height * 0.5
+ end
+ welcomeHint:parentDidResize()
+
+ local t = 0
+ welcomeHint.object.Tick = function(o, dt)
+ t = t + dt
+ if (t % 0.4) <= 0.2 then
+ o.Color = Color(0.0, 0.8, 0.6)
+ else
+ o.Color = Color(1.0, 1.0, 1.0)
+ end
+ end
+end
+
+function hideWelcomeHint()
+ if welcomeHint == nil then
+ return
+ end
+ welcomeHint:remove()
+ welcomeHint = nil
+end
+
+-- creates loading bubble
+function makeBubble(e)
+ local bubble = MutableShape()
+ bubble:AddBlock(Color.White, 0, 0, 0)
+ bubble:SetParent(World)
+ bubble.Pivot = Number3(0.5, 0, 0.5)
+ bubble.Position = e.pos
+ bubble.Rotation.Y = e.rotY
+ bubble.eid = e.id
+
+ bubble.Tick = function(o, dt)
+ o.Scale.X = o.Scale.X + dt * 2
+ o.Scale.Y = o.Scale.Y + dt * 2
+ if o.text ~= nil then
+ o.text.Position = o.Position
+ o.text.Position.Y = o.Position.Y + o.Height * o.Scale.Y + 1
+ end
+ end
+
+ local t = Text()
+ t:SetParent(World)
+ t.Rotation.Y = e.rotY
+ t.Text = e.m
+ t.Type = TextType.World
+ t.IsUnlit = true
+ t.Tail = true
+ t.Anchor = { 0.5, 0 }
+ t.Position.Y = bubble.Position.Y + bubble.Height * bubble.Scale.Y + 1
+ bubble.text = t
+
+ -- remove after 15 seconds without response
+ Timer(15, function()
+ if bubble then
+ gens[bubble.eid] = nil
+ bubble.Tick = nil
+ if bubble.text then
+ bubble.text:RemoveFromParent()
+ end
+ bubble:RemoveFromParent()
+ end
+ end)
+
+ gens[e.id] = bubble
+end
+
+function splitAtFirst(inputString, delimiter)
+ local pos = string.find(inputString, delimiter, 1, true)
+ if pos then
+ return string.sub(inputString, 1, pos - 1), string.sub(inputString, pos + 1)
+ else
+ return inputString
+ end
+end
+
+-- #### Utility functions #####################
+
+clamp = function(value, min, max)
+ if value < min then
+ return min
+ elseif value > max then
+ return max
+ else
+ return value
+ end
+end
\ No newline at end of file
From dfa62618a5d10e767dd2b9a60d5e1289a0fc5a0b Mon Sep 17 00:00:00 2001
From: Tristan
Date: Wed, 17 Apr 2024 14:58:58 +0200
Subject: [PATCH 2/2] Functional, but basic, action parsing in Lua
---
cubzh/scripts/mod_test.lua | 334 +++++++++++++++++++++++++------------
1 file changed, 226 insertions(+), 108 deletions(-)
diff --git a/cubzh/scripts/mod_test.lua b/cubzh/scripts/mod_test.lua
index dbe0041..47988c4 100644
--- a/cubzh/scripts/mod_test.lua
+++ b/cubzh/scripts/mod_test.lua
@@ -12,9 +12,9 @@ Config = {
}
}
-local CRATE_START_POSITION = Number3(382, 290, 153)
local YOUR_API_TOKEN = "H4gjL-e9kvLF??2pz6oh=kJL497cBnsyCrQFdVkFadUkLnIaEamroYHb91GywMXrbGeDdmTiHxi8EqmJduCKPrDnfqWsjGuF0JJCUTrasGcBfGx=tlJCjq5q8jhVHWL?krIE74GT9AJ7qqX8nZQgsDa!Unk8GWaqWcVYT-19C!tCo11DcLvrnJPEOPlSbH7dDcXmAMfMEf1ZwZ1v1C9?2/BjPDeiAVTRlLFilwRFmKz7k4H-kCQnDH-RrBk!ZHl7"
local API_URL = "https://0074-195-154-25-43.ngrok-free.app"
+local TRIGGER_AREA_SIZE = Number3(60, 30, 60)
-- Client.OnStart is the first function to be called when the world is launched, on each user's device.
Client.OnStart = function()
@@ -27,18 +27,15 @@ Client.OnStart = function()
-- The "ambience" module also accepts
-- custom settings (light colors, angles, etc.)
local ambience = require("ambience")
- ambience:set(ambience.noon)
+ ambience:set(ambience.dusk)
- -- The sfx module can be used to play spatialized sounds in one line calls.
- -- A list of available sounds can be found here:
- -- https://docs.cu.bzh/guides/quick/adding-sounds#list-of-available-sounds
sfx = require("sfx")
-- There's only one AudioListener, but it can be placed wherever you want:
Player.Head:AddChild(AudioListener)
-- Requiring "multi" module is all you need to see other players in your game!
-- (remove this line if you want to be solo)
- require("multi")
+ multi = require("multi")
-- This function drops the local player above the center of the map:
dropPlayer = function()
@@ -55,24 +52,26 @@ Client.OnStart = function()
dialog:setMaxWidth(400)
avatar = require("avatar")
- -- Create an avatarId to avatar mapping
- avatarIdToAvatar = {
- }
+ ease = require("ease")
updateLocationTimer = nil
character = nil
+ engineId = nil
+
+ -- SYNCED ACTIONS
+ multi:onAction("swingRight", function(sender)
+ sender:SwingRight()
+ end)
+ npcDataClient = {}
+
+ timer = Timer(1, false, function()
+ _helpers.createNPCsAndLocations()
+ local e = Event()
+ e.action = "registerEngine"
+ e:SendTo(Server)
+ end)
end
-LocalEvent:Listen(LocalEvent.Name.OnPlayerJoin, function(p)
- print("Player joined")
-end)
-
-LocalEvent:Listen(LocalEvent.Name.Tick, function(dt)
- if _config == nil then
- return
- end
-end)
-
-- jump function, triggered with Action1
-- (space bar on PC, button 1 on mobile)
Client.Action1 = function()
@@ -80,81 +79,12 @@ Client.Action1 = function()
Player.Velocity.Y = 100
sfx("hurtscream_1", {Position = Player.Position, Volume = 0.4})
end
-
- -- Example location registration
- local loc1 = createLocation(
- "Medieval Inn",
- Number3(130, 23, 75),
- "An inn lost in the middle of the forest, where travelers can rest and eat."
- )
- local loc2 = createLocation(
- "Abandoned temple",
- Number3(303, 20, 263),
- "Lost deep inside the woods, this temple features a mysterious altar statue. Fresh fruits and offrands are scattered on the ground."
- )
- local loc3 = createLocation(
- "Lone grave in the woods",
- Number3(142, 20, 258),
- "Inside a small clearing in the forest lies a stone cross, marking the grave of a lost soul."
- )
- local loc4 = createLocation(
- "Rope bridge",
- Number3(26, 20, 301),
- "Near the edge of a cliff, a rope bridge connects the forest to the island. The bridge is old and fragile, but still usable."
- )
- local loc5 = createLocation(
- "Forest entrance",
- Number3(156, 20, 168),
- "The entrance to the forest is marked by a large stone arch. The path is wide and well maintained."
- )
-
- local NPC1 = createNPC("aduermael", "Tall, with green eyes", "Friendly and helpful", "Medieval Inn", Number3(130, 23, 75))
- local NPC2 = createNPC("soliton", "Short, with a big nose", "Grumpy and suspicious", "Abandoned temple", Number3(303, 20, 263))
- local NPC3 = createNPC("caillef", "Tall, with a big beard", "Wise and mysterious", "Lone grave in the woods", Number3(142, 20, 258))
-
- local e = Event()
- e.action = "testRegisterEngine"
- e:SendTo(Server)
-end
-
-
-function createNPC(avatarId, physicalDescription, psychologicalProfile, currentLocationName, currentPosition)
- -- Create the NPC's Object and Avatar
- local NPC = {}
- NPC.object = Object()
- World:AddChild(NPC.object)
- NPC.object.Position = currentPosition or Number3(0, 0, 0)
- print("Placing NPC at position: " .. currentPosition._x .. ", " .. currentPosition._y .. ", " .. currentPosition._z)
- NPC.object.Scale = 0.5
- NPC.object.Physics = PhysicsMode.Dynamic
-
- avatar = require("avatar")
- NPC.avatar = avatar:get(avatarId)
- print("Getting avatar for NPC: " .. avatarId)
- NPC.avatar:SetParent(NPC.object)
- avatarIdToAvatar[avatarId] = NPC.avatar
-
- -- Register it
- local e = Event()
- e.action = "registerNPC"
- e.avatarId = avatarId
- e.physicalDescription = physicalDescription
- e.psychologicalProfile = psychologicalProfile
- e.currentLocationName = currentLocationName
- e:SendTo(Server)
- return NPC
-end
-
-function createLocation(name, position, description)
local e = Event()
- e.action = "registerLocation"
- e.name = name
- e.position = position
- e.description = description
+ e.action = "stepMainCharacter"
+ e.actionType = "JUMP"
e:SendTo(Server)
end
-
-- Function to calculate distance between two positions
local function calculateDistance(pos1, pos2)
local dx = pos1.X - pos2.x
@@ -203,9 +133,9 @@ Client.OnChat = function(payload)
local e = Event()
e.action = "stepMainCharacter"
+ e.actionType = "SAY"
e.content = msg
e:SendTo(Server)
-
end
-- Pointer.Click is called following click/touch down & up events,
@@ -228,9 +158,9 @@ end
Client.DidReceiveEvent = function(e)
- if e.action == "displayDialog" then
- dialog:create(e.content, avatarIdToAvatar[e.avatarId])
- elseif e.action == "characterResponse" then
+ if e.action == "NPCActionResponse" then
+ _helpers.parseAction(e)
+ elseif e.action == "mainCharacterCreated" then
-- Setup a new timer to delay the next update call
characterId = e.character._id
updateLocationTimer = Timer(0.5, true, function()
@@ -241,6 +171,15 @@ Client.DidReceiveEvent = function(e)
e:SendTo(Server)
end)
-- print("Character ID: " .. character._id)
+ elseif e.action == "NPCRegistered" then
+ -- Update NPC in the client side table to add the _id
+ for _, npc in pairs(npcDataClient) do
+ print("Checking NPC " .. npc.name)
+ if npc.name == e.npcName then
+ print("Assigning ID " .. e.npcId .. " to NPC " .. npc.name)
+ npc._id = e.npcId
+ end
+ end
end
end
@@ -264,11 +203,11 @@ Server.DidReceiveEvent = function(e)
registerNPC(e.avatarId, e.physicalDescription, e.psychologicalProfile, e.currentLocationName)
elseif e.action == "registerLocation" then
registerLocation(e.name, e.position, e.description)
- elseif e.action == "testRegisterEngine" then
+ elseif e.action == "registerEngine" then
print("Registering engine...")
registerEngine(e.Sender)
elseif e.action == "stepMainCharacter" then
- stepMainCharacter(character, engineId, npcData["aduermael"]._id, npcData["aduermael"].name, e.content)
+ stepMainCharacter(character, engineId, e.actionType, npcData["aduermael"]._id, npcData["aduermael"].name, e.content)
elseif e.action == "updateCharacterLocation" then
closest = findClosestLocation(e.position, locationData)
-- if closest._id is different from the current location, update the character's location
@@ -286,6 +225,160 @@ end
Server.OnPlayerJoin = function(player)
end
+------------------------------------------------------------------------------------------------
+-- Helpers --------------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------
+_helpers = {}
+
+
+_helpers.lookAt = function(obj, target)
+ if not target then
+ ease:linear(obj, 0.1).Forward = obj.initialForward
+ obj.Tick = nil
+ return
+ end
+ obj.Tick = function(self, _)
+ _helpers.lookAtHorizontal(self, target)
+ end
+end
+
+_helpers.lookAtHorizontal = function(o1, o2)
+ local n3_1 = Number3.Zero
+ local n3_2 = Number3.Zero
+ n3_1:Set(o1.Position.X, 0, o1.Position.Z)
+ n3_2:Set(o2.Position.X, 0, o2.Position.Z)
+ ease:linear(o1, 0.1).Forward = n3_2 - n3_1
+end
+
+_helpers.createNPC = function(avatarId, physicalDescription, psychologicalProfile, currentLocationName, currentPosition)
+ -- Create the NPC's Object and Avatar
+ local NPC = {}
+ NPC.object = Object()
+ World:AddChild(NPC.object)
+ NPC.object.Position = currentPosition or Number3(0, 0, 0)
+ NPC.object.Scale = 0.5
+ NPC.object.Physics = PhysicsMode.Trigger
+ NPC.object.CollisionBox = Box({
+ -TRIGGER_AREA_SIZE.Width * 0.5,
+ math.min(-TRIGGER_AREA_SIZE.Height, NPC.object.CollisionBox.Min.Y),
+ -TRIGGER_AREA_SIZE.Depth * 0.5,
+ }, {
+ TRIGGER_AREA_SIZE.Width * 0.5,
+ math.max(TRIGGER_AREA_SIZE.Height, NPC.object.CollisionBox.Max.Y),
+ TRIGGER_AREA_SIZE.Depth * 0.5,
+ })
+ NPC.object.OnCollisionBegin = function(self, other)
+ if other ~= Player then
+ return
+ end
+ _helpers.lookAt(self.avatarContainer, other)
+ end
+ NPC.object.OnCollisionEnd = function(self, other)
+ if other ~= Player then
+ return
+ end
+ _helpers.lookAt(self.avatarContainer, nil)
+ end
+
+ local container = Object()
+ container.Rotation = NPC.object.Rotation
+ container.initialRotation = NPC.object.Rotation:Copy()
+ container.initialForward = NPC.object.Forward:Copy()
+ container:SetParent(NPC.object)
+ container.Physics = PhysicsMode.Dynamic
+ NPC.object.avatarContainer = container
+
+ local avatar = require("avatar")
+ NPC.avatar = avatar:get(avatarId)
+ NPC.avatar:SetParent(NPC.object.avatarContainer)
+
+ -- Register it
+ local e = Event()
+ e.action = "registerNPC"
+ e.avatarId = avatarId
+ e.physicalDescription = physicalDescription
+ e.psychologicalProfile = psychologicalProfile
+ e.currentLocationName = currentLocationName
+ e:SendTo(Server)
+ return NPC
+end
+
+_helpers.createLocation = function(name, position, description)
+ local e = Event()
+ e.action = "registerLocation"
+ e.name = name
+ e.position = position
+ e.description = description
+ e:SendTo(Server)
+end
+
+_helpers.createNPCsAndLocations = function()
+ -- Example location registration
+ local loc1 = _helpers.createLocation(
+ "Medieval Inn",
+ Number3(130, 23, 75),
+ "An inn lost in the middle of the forest, where travelers can rest and eat."
+ )
+ local loc2 = _helpers.createLocation(
+ "Abandoned temple",
+ Number3(303, 20, 263),
+ "Lost deep inside the woods, this temple features a mysterious altar statue. Fresh fruits and offrands are scattered on the ground."
+ )
+ local loc3 = _helpers.createLocation(
+ "Lone grave in the woods",
+ Number3(142, 20, 258),
+ "Inside a small clearing in the forest lies a stone cross, marking the grave of a lost soul."
+ )
+ local loc4 = _helpers.createLocation(
+ "Rope bridge",
+ Number3(26, 20, 301),
+ "Near the edge of a cliff, a rope bridge connects the forest to the island. The bridge is old and fragile, but still usable."
+ )
+ local loc5 = _helpers.createLocation(
+ "Forest entrance",
+ Number3(156, 20, 168),
+ "The entrance to the forest is marked by a large stone arch. The path is wide and well maintained."
+ )
+
+ local NPC1 = _helpers.createNPC("aduermael", "Tall, with green eyes", "Friendly and helpful", "Medieval Inn", Number3(130, 23, 75))
+ table.insert(npcDataClient, {name = "aduermael", avatar = NPC1.avatar, object = NPC1.object})
+ NPC1.avatar.Animations.SwingRight:Play()
+ local NPC2 = _helpers.createNPC("soliton", "Short, with a big nose", "Grumpy and suspicious", "Abandoned temple", Number3(303, 20, 263))
+ table.insert(npcDataClient, {name = "soliton", avatar = NPC2.avatar, object = NPC2.object})
+ local NPC3 = _helpers.createNPC("caillef", "Tall, with a big beard", "Wise and mysterious", "Lone grave in the woods", Number3(142, 20, 258))
+ table.insert(npcDataClient, {name = "caillef", avatar = NPC3.avatar, object = NPC3.object})
+end
+
+_helpers.findNPCById = function(id)
+ for _, npc in pairs(npcDataClient) do
+ if npc._id == id then
+ return npc
+ end
+ end
+end
+
+_helpers.parseAction = function(action)
+ local npc = _helpers.findNPCById(action.protagonistId)
+ print("Parsing action for NPC with name: " .. npc.name)
+ if action.actionType == "GREET" then
+ -- TODO: face action.target and wave hand
+ dialog:create("", npc.avatar)
+ npc.avatar.Animations.SwingRight:Play()
+ elseif action.actionType == "SAY" then
+ dialog:create(action.content, npc.avatar)
+ elseif action.actionType == "JUMP" then
+ dialog:create("", npc.avatar)
+ npc.object.avatarContainer.Velocity.Y = 50
+ timer = Timer(1, false, function()
+ npc.object.avatarContainer.Velocity.Y = 50
+ end)
+ elseif action.actionType == "MOVE" then
+ -- TODO
+ elseif action.actionType == "FOLLOW" then
+ -- TODO
+ end
+end
+
------------------------------------------------------------------------------------------------
-- Gigax Stuff ---------------------------------------------------------------------------------
------------------------------------------------------------------------------------------------
@@ -304,10 +397,24 @@ function registerNPC(avatarId, physicalDescription, psychologicalProfile, curren
description = "Say smthg out loud",
parameter_types = {"character", "content"}
},
+ -- {
+ -- name = "move",
+ -- description = "Move to a new location",
+ -- parameter_types = {"location"}
+ -- },
+ {
+ name = "greet",
+ description = "Greet a character by waving your hand at them",
+ parameter_types = {"character"}
+ },
+ -- {
+ -- name = "follow",
+ -- description = "Follow a character around for a while",
+ -- parameter_types = {"character"}
+ -- },
{
- name = "move",
- description = "Move to a new location",
- parameter_types = {"location"}
+ name = "jump",
+ description = "Jump in the air",
}
}
}
@@ -368,6 +475,7 @@ function registerEngine(sender)
-- Save the engine_id for future use
engineId = responseData.engine.id
+ print("Engine ID: " .. engineId)
-- Saving all the _ids inside locationData table:
for _, loc in pairs(responseData.locations) do
@@ -377,6 +485,12 @@ function registerEngine(sender)
-- same for characters:
for _, npc in pairs(responseData.NPCs) do
npcData[npc.name]._id = npc._id
+ local e = Event()
+ e.action = "NPCRegistered"
+ e.npcName = npc.name
+ e.npcId = npc._id
+ e.engineId = engineId
+ e:SendTo(sender)
end
@@ -412,24 +526,24 @@ function registerMainCharacter(engineId, locationId, sender)
print("Main character created/fetched successfully.")
character = JSON:Decode(response.Body)
local e = Event()
- e.action = "characterResponse"
+ e.action = "mainCharacterCreated"
e.character = character
e:SendTo(sender)
end)
end
-function stepMainCharacter(character, engineId, targetId, targetName, content)
-
+function stepMainCharacter(character, engineId, actionType, targetId, targetName, content)
-- Now, step the character
local stepUrl = API_URL .. "/api/character/" .. character._id .. "/step-no-ws?engine_id=" .. engineId
local stepActionData = {
character_id = character._id, -- Use the character ID from the creation/fetch response
- action_type = "SAY",
+ action_type = actionType,
target = targetId,
target_name = targetName,
content = content
}
local stepJsonData = JSON:Encode(stepActionData)
+ print("Stepping character with data: " .. stepJsonData)
local headers = {
["Content-Type"] = "application/json",
@@ -445,16 +559,20 @@ function stepMainCharacter(character, engineId, targetId, targetName, content)
local actions = JSON:Decode(stepResponse.Body)
-- Find the target character by id using the "target" field in the response:
for _, action in ipairs(actions) do
+ local e = Event()
+ e.action = "NPCActionResponse"
+ e.actionType = action.action_type
+ e.content = action.content
for _, npc in pairs(npcData) do
if action.character_id == npc._id then
-- Perform the action on the target character
- local e = Event()
- e.action = "displayDialog"
- e.avatarId = npc.name
- e.content = action.content
- e:SendTo(Players)
+ e.protagonistId = npc._id
+ elseif action.target == npc._id then
+ -- Perform the action on the target character
+ e.targetId = npc._id
end
end
+ e:SendTo(Players)
end
end)
end