klopfer commited on
Commit
9333bb7
·
verified ·
1 Parent(s): bd95280

Upload 5 files

Browse files
Files changed (5) hide show
  1. README.md +106 -5
  2. cubzh.html +61 -0
  3. cubzh.json +15 -0
  4. cubzh.lua +468 -0
  5. map.b64 +1 -0
README.md CHANGED
@@ -1,11 +1,112 @@
1
  ---
2
- title: Ainpc
3
- emoji: 🚀
4
- colorFrom: blue
5
- colorTo: yellow
6
  sdk: static
 
7
  pinned: false
8
  license: mit
 
 
 
 
 
9
  ---
10
 
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: NPC Playground
3
+ emoji: 🤖
4
+ colorFrom: indigo
5
+ colorTo: pink
6
  sdk: static
7
+ app_file: ./cubzh.html
8
  pinned: false
9
  license: mit
10
+ disable_embedding: true
11
+ custom_headers:
12
+ cross-origin-embedder-policy: require-corp
13
+ cross-origin-opener-policy: same-origin
14
+ cross-origin-resource-policy: cross-origin
15
  ---
16
 
17
+ # NPC Playground 🕹️🤖
18
+
19
+ [![Join the chat at https://cu.bzh/discord](https://img.shields.io/discord/355905150528913409?color=%237289DA&label=cubzh&logo=discord&logoColor=white)](https://cu.bzh/discord)
20
+
21
+ 3D playground to interact with LLM-powered NPCs. </br>
22
+ Clone and modify `cubzh.lua` file to teach them new skills with a few lines of code!
23
+
24
+ <div align="center">
25
+ <img style="max-width: 800px; width: 80%;" alt="cubzh_gigax_hf" src="https://github.com/soliton-x/ai-npc/assets/33256624/e62dd138-c018-4ecf-bc77-a072fadb5c12">
26
+ </div>
27
+
28
+ [Play](#Play) |
29
+ [Customize](#Customize) |
30
+ [Scripting](#Scripting) |
31
+ [Course](#Course) |
32
+ [Credits](#Credits)
33
+
34
+
35
+ ## Play
36
+
37
+ Just go to [huggingface.co/spaces/cubzh/ai-npcs](https://huggingface.co/spaces/cubzh/ai-npcs).
38
+
39
+ Engage with NPCs and try to trigger some of those pre-installed skills: `move`, `follow`, `jump`, `explode` (you may need to insist for that one 😅).
40
+
41
+ ## Customize
42
+
43
+ 1. Clone [huggingface.co/spaces/cubzh/ai-npcs](https://huggingface.co/spaces/cubzh/ai-npcs) repository. **⚠️ clone needs to be public ⚠️**
44
+ 2. Modify and commit [`world.lua`](https://huggingface.co/spaces/cubzh/ai-npcs/blob/main/world.lua) file to edit NPC skills.
45
+ 3. That's it!
46
+
47
+ ## Scripting
48
+
49
+ ### **Tweaking NPC Behavior**
50
+
51
+ Modify the predifined fields in `world.lua`'s `NPCs` table in order to influence NPC behaviour:
52
+
53
+ ```lua
54
+ local NPCs = {
55
+ {
56
+ name = "npcscientist",
57
+ physicalDescription = "A small sphere with a computer screen for a face",
58
+ psychologicalProfile = "Designed to be helpful to any human it interacts with, this robot viscerally hates squirrels.",
59
+ currentLocationName = "Scientist Island",
60
+ initialReflections = {
61
+ "This NPC is a robot that punctuates all of its answers with electronic noises - as any android would!",
62
+ ...
63
+ },
64
+ },
65
+ ...
66
+ }
67
+ ```
68
+
69
+ ### **Teaching NPCs new skills**
70
+
71
+ Our NPCs have been trained to use any skill you've defined before running the game. This is achieved by training the LLM powering them to do "function calling".
72
+
73
+ Modify `skills` table in `world.lua` to give your NPCs new skills:
74
+
75
+ ```lua
76
+ local skills = {
77
+ {
78
+ name = "SAY",
79
+ description = "Say smthg out loud",
80
+ parameter_types = {"character", "content"},
81
+ callback = function(client, action)
82
+ local npc = client:getNpc(action.character_id)
83
+ if not npc then print("Can't find npc") return end
84
+ dialog:create(action.content, npc.avatar)
85
+ print(string.format("%s: %s", npc.name, action.content))
86
+ end,
87
+ action_format_str = "{protagonist_name} said '{content}' to {target_name}"
88
+ },
89
+ ...
90
+ }
91
+ ```
92
+
93
+ The `callback` function is called whenever an NPC uses the skill, using the parameters defined in the `parameters` field. We've given you some examples in `skills.lua`, feel free to draw inspiration from them!
94
+
95
+ If you want to go deeper with Cubzh scripting API, here's the [documentation](https://docs.cu.bzh), the team and community will also be glad to help you on [Discord](https://cu.bzh/discord).
96
+
97
+ ### Environment Design (👷‍♂️ work in progress 🏗️)
98
+
99
+ Cubzh allows you to modify the 3D environment, by importing community-made voxel assets or creating new ones yourself. It's not yet possible to modify the environment yet though in the context of that specific demo, but we're working on making it possible, stay tuned!
100
+
101
+ ## Course
102
+
103
+ Together with the HuggingFace staff, we've released a new course to teach you how to create your own NPC skills with Lua. You can access it [here](https://huggingface.co/learn/ml-games-course/en/unit3/introduction)
104
+
105
+ ## Credits
106
+
107
+ - [Hugging Face](https://huggingface.co/) 🤗
108
+ - [Gigax](https://github.com/GigaxGames)
109
+ - [Cubzh](https://cu.bzh): A versatile UGC (User-Generated Content) gaming platform.
110
+ - **You !** You're welcome to **duplicate** the repo, share your creations, and submit PRs here :)
111
+
112
+
cubzh.html ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Wrapper</title>
8
+ <style>
9
+ body,
10
+ html {
11
+ margin: 0;
12
+ padding: 0;
13
+ height: 100%;
14
+ overflow: hidden;
15
+ }
16
+
17
+ .fullscreen-iframe {
18
+ position: absolute;
19
+ top: 0;
20
+ left: 0;
21
+ width: 100%;
22
+ height: 100%;
23
+ }
24
+
25
+ iframe {
26
+ width: 100%;
27
+ height: 100%;
28
+ }
29
+ </style>
30
+ </head>
31
+
32
+ <body>
33
+ <div class="fullscreen-iframe">
34
+ <iframe id="dynamic-iframe" frameborder="0" allowfullscreen crossorigin allow="cross-origin-isolated"></iframe>
35
+ </div>
36
+
37
+ <script>
38
+ function onDOMContentLoaded() {
39
+ document.removeEventListener("DOMContentLoaded", onDOMContentLoaded);
40
+
41
+ var currentUrl = window.location.href;
42
+
43
+ var regex = /https:\/\/([\w]+)-([\w-]+)\.static\.hf\.space/;
44
+ var match = currentUrl.match(regex);
45
+
46
+ if (match) {
47
+ var repo = match[1];
48
+ var space = match[2];
49
+ var targetUrl = "https://huggingface.cu.bzh/?script=huggingface.co/spaces/" + repo + "/" + space
50
+ console.log("targetUrl:", targetUrl)
51
+ document.getElementById("dynamic-iframe").src = targetUrl;
52
+ } else {
53
+ console.error("URL pattern does not match.");
54
+ }
55
+ }
56
+ document.addEventListener("DOMContentLoaded", onDOMContentLoaded);
57
+
58
+ </script>
59
+ </body>
60
+
61
+ </html>
cubzh.json ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "script": "cubzh.lua",
3
+ "env": {
4
+ "USER_AUTH": "disabled",
5
+ "CUBZH_MENU": "disabled",
6
+ "CHAT_CONSOLE_DISPLAY": "always"
7
+ },
8
+ "contributors": [
9
+ { "caillef": 0.4 },
10
+ { "tantris": 0.4 },
11
+ { "aduermael": 0.2 }
12
+ ],
13
+ "map": "map.b64",
14
+ "bundle": []
15
+ }
cubzh.lua ADDED
@@ -0,0 +1,468 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ math.randomseed(math.floor(Time.UnixMilli() % 100000))
2
+
3
+ Modules = {
4
+ gigax = "github.com/GigaxGames/integrations/cubzh:9a71b9f",
5
+ pathfinding = "github.com/caillef/cubzh-library/pathfinding:5f9c6bd",
6
+ floating_island_generator = "github.com/caillef/cubzh-library/floating_island_generator:82d22a5",
7
+ easy_onboarding = "github.com/caillef/cubzh-library/easy_onboarding:77728ee",
8
+ }
9
+
10
+ Config = {
11
+ Items = { "pratamacam.squirrel" },
12
+ }
13
+
14
+ -- Function to spawn a squirrel above the player
15
+ function spawnSquirrelAbovePlayer(player)
16
+ local squirrel = Shape(Items.pratamacam.squirrel)
17
+ squirrel:SetParent(World)
18
+ squirrel.Position = player.Position + Number3(0, 20, 0)
19
+ -- make scale smaller
20
+ squirrel.LocalScale = 0.5
21
+ -- remove collision
22
+ squirrel.Physics = PhysicsMode.Dynamic
23
+ -- rotate it 90 degrees to the right
24
+ squirrel.Rotation = { 0, math.pi * 0.5, 0 }
25
+ -- this would make squirrel.Rotation = player.Rotation
26
+ World:AddChild(squirrel)
27
+ return squirrel
28
+ end
29
+
30
+ local SIMULATION_NAME = "Islands" .. tostring(math.random())
31
+ local SIMULATION_DESCRIPTION = "Three floating islands."
32
+
33
+
34
+ local skills = {
35
+ {
36
+ name = "SAY",
37
+ description = "Say smthg out loud",
38
+ parameter_types = { "character", "content" },
39
+ callback = function(client, action)
40
+ local npc = client:getNpc(action.character_id)
41
+ if not npc then
42
+ print("Can't find npc")
43
+ return
44
+ end
45
+ dialog:create(action.content, npc.avatar.Head)
46
+ print(string.format("%s: %s", npc.gameName, action.content))
47
+ end,
48
+ action_format_str = "{protagonist_name} said '{content}' to {target_name}",
49
+ },
50
+ {
51
+ name = "MOVE",
52
+ description = "Move to a new location",
53
+ parameter_types = { "location" },
54
+ callback = function(client, action, config)
55
+ local targetName = action.target_name
56
+ local targetPosition = findLocationByName(targetName, config)
57
+ if not targetPosition then
58
+ print("tried to move to an unknown place", targetName)
59
+ return
60
+ end
61
+ local npc = client:getNpc(action.character_id)
62
+ dialog:create("I'm going to " .. targetName, npc.avatar.Head)
63
+ print(string.format("%s: %s", npc.gameName, "I'm going to " .. targetName))
64
+ local origin = Map:WorldToBlock(npc.object.Position)
65
+ local destination = Map:WorldToBlock(targetPosition) + Number3(math.random(-1, 1), 0, math.random(-1, 1))
66
+ local canMove = pathfinding:moveObjectTo(npc.object, origin, destination)
67
+ if not canMove then
68
+ dialog:create("I can't go there", npc.avatar.Head)
69
+ return
70
+ end
71
+ end,
72
+ action_format_str = "{protagonist_name} moved to {target_name}",
73
+ },
74
+ {
75
+ name = "GREET",
76
+ description = "Greet a character by waving your hand at them",
77
+ parameter_types = { "character" },
78
+ callback = function(client, action)
79
+ local npc = client:getNpc(action.character_id)
80
+ if not npc then
81
+ print("Can't find npc")
82
+ return
83
+ end
84
+
85
+ dialog:create("<Greets you warmly!>", npc.avatar.Head)
86
+ print(string.format("%s: %s", npc.gameName, "<Greets you warmly!>"))
87
+
88
+ npc.avatar.Animations.SwingRight:Play()
89
+ end,
90
+ action_format_str = "{protagonist_name} waved their hand at {target_name} to greet them",
91
+ },
92
+ {
93
+ name = "JUMP",
94
+ description = "Jump in the air",
95
+ parameter_types = {},
96
+ callback = function(client, action)
97
+ local npc = client:getNpc(action.character_id)
98
+ if not npc then
99
+ print("Can't find npc")
100
+ return
101
+ end
102
+
103
+ dialog:create("<Jumps in the air!>", npc.avatar.Head)
104
+ print(string.format("%s: %s", npc.gameName, "<Jumps in the air!>"))
105
+
106
+ npc.object.avatarContainer.Physics = PhysicsMode.Dynamic
107
+ npc.object.avatarContainer.Velocity.Y = 50
108
+ Timer(3, function()
109
+ npc.object.avatarContainer.Physics = PhysicsMode.Trigger
110
+ end)
111
+ end,
112
+ action_format_str = "{protagonist_name} jumped up in the air for a moment.",
113
+ },
114
+ {
115
+ name = "FOLLOW",
116
+ description = "Follow a character around for a while",
117
+ parameter_types = { "character" },
118
+ callback = function(client, action)
119
+ local npc = client:getNpc(action.character_id)
120
+ if not npc then
121
+ print("Can't find npc")
122
+ return
123
+ end
124
+
125
+ dialog:create("I'm following you", npc.avatar.Head)
126
+ print(string.format("%s: %s", npc.gameName, "I'm following you"))
127
+
128
+ followHandler = pathfinding:followObject(npc.object, Player)
129
+ return {
130
+ followHandler = followHandler,
131
+ }
132
+ end,
133
+ onEndCallback = function(_, data)
134
+ data.followHandler:Stop()
135
+ end,
136
+ action_format_str = "{protagonist_name} followed {target_name} for a while.",
137
+ },
138
+ {
139
+ name = "FIRECRACKER",
140
+ description = "Perform a fun, harmless little explosion to make people laugh!",
141
+ parameter_types = { "character" },
142
+ callback = function(client, action)
143
+ local npc = client:getNpc(action.character_id)
144
+ if not npc then
145
+ print("Can't find npc")
146
+ return
147
+ end
148
+
149
+ require("explode"):shapes(npc.avatar)
150
+ dialog:create("*boom*", npc.avatar.Head)
151
+ npc.avatar.IsHidden = true
152
+ Timer(5, function()
153
+ dialog:create("Aaaaand... I'm back!", npc.avatar.Head)
154
+ npc.avatar.IsHidden = false
155
+ end)
156
+ end,
157
+ action_format_str = "{protagonist_name} exploded like a firecracker, with a bang!",
158
+ },--[[
159
+ {
160
+ name = "GIVEAPPLE",
161
+ description = "Give a pice of bread (or a baguette) to someone",
162
+ parameter_types = {"character"},
163
+ callback = function(client, action)
164
+ local npc = client:getNpc(action.character_id)
165
+ if not npc then print("Can't find npc") return end
166
+ local shape = MutableShape()
167
+ shape:AddBlock(Color.Red, 0, 0, 0)
168
+ shape.Scale = 4
169
+ Player:EquipRightHand(shape)
170
+ dialog:create("Here is an apple for you!", npc.avatar.Head)
171
+ end,
172
+ action_format_str = "{protagonist_name} gave you a piece of bread!"
173
+ }, --]]
174
+ {
175
+ name = "GIANT",
176
+ description = "Double your height to become a giant for a few seconds.",
177
+ parameter_types = {"character"},
178
+ callback = function(client, action)
179
+ local npc = client:getNpc(action.character_id)
180
+ if not npc then print("Can't find npc") return end
181
+
182
+ npc.object.Scale = npc.object.Scale * 2
183
+ dialog:create("I am taller than you now!", npc.avatar.Head)
184
+ end,
185
+ action_format_str = "{protagonist_name} doubled his height!"
186
+ },
187
+ {
188
+ name = "GIVEHAT",
189
+ description = "Give a party hat to someone",
190
+ parameter_types = { "character" },
191
+ callback = function(client, action)
192
+ local npc = client:getNpc(action.character_id)
193
+ if not npc then
194
+ print("Can't find npc")
195
+ return
196
+ end
197
+
198
+ Object:Load("claire.party_hat", function(obj)
199
+ require("hierarchyactions"):applyToDescendants(obj, { includeRoot = true }, function(o)
200
+ o.Physics = PhysicsMode.Disabled
201
+ end)
202
+ Player:EquipHat(obj)
203
+ end)
204
+ dialog:create("Let's get the party started!", npc.avatar.Head)
205
+ end,
206
+ action_format_str = "{protagonist_name} gave you a piece of bread!",
207
+ },
208
+ {
209
+ name = "FLYINGSQUIRREL",
210
+ description = "Summon a flying squirrel - only the scientist can do this!!",
211
+ parameter_types = {},
212
+ callback = function(client, action)
213
+ local npc = client:getNpc(action.character_id)
214
+ if not npc then
215
+ print("Can't find npc")
216
+ return
217
+ end
218
+
219
+ local squirrel = spawnSquirrelAbovePlayer(Player)
220
+ dialog:create("Wooh, squirrel!", npc.avatar.Head)
221
+ -- make it disappear after a while
222
+ Timer(5, function()
223
+ squirrel:RemoveFromParent()
224
+ squirrel = nil
225
+ end)
226
+ end,
227
+ action_format_str = "{protagonist_name} summoned a flying squirrel! It's vibrating with excitement!",
228
+ },
229
+ }
230
+
231
+ local locations = {
232
+ {
233
+ name = "Scientist Island",
234
+ description = "A small island with a scientist and its pet chilling.",
235
+ },
236
+ {
237
+ name = "Baker Island",
238
+ description = "A small bakery on a floating island in the sky.",
239
+ },
240
+ {
241
+ name = "Pirate Island",
242
+ description = "A small floating island in the sky with a pirate and its ship.",
243
+ },
244
+ {
245
+ name = "Center",
246
+ description = "Center point between the three islands.",
247
+ },
248
+ }
249
+
250
+ local NPCs = {
251
+ {
252
+ name = "npcscientist",
253
+ gameName = "Scientist",
254
+ physicalDescription = "Short, with a stern expression and sharp eyes",
255
+ psychologicalProfile = "Grumpy but insightful, this scientist loves to surprise his guests with cool tricks! For example, he frequently lights up little firecrackers to create harmless and fun explosions.",
256
+ currentLocationName = "Scientist Island",
257
+ initialReflections = {
258
+ "I just arrived on this island to feed my pet, he loves tulips so much.",
259
+ "Yesterday, you were visited by a newcomer. He asked you to perform fun tricks like increase your size or create little explosions - you complied merrily!",
260
+ "I am a scientist that works on new pets for everyone, so that each individual can have the pet of their dreams",
261
+ "I am a bit allergic to the tulip but Fredo my pet loves it so much, I have to dock here with my vehicle. The pet is placed at the back of my flying scooter when we move to another place.",
262
+ },
263
+ },
264
+ {
265
+ name = "npcbaker",
266
+ gameName = "Baker",
267
+ physicalDescription = "Tall, with a solemn demeanor and thoughtful eyes",
268
+ psychologicalProfile = "Wise and mysterious, calm under pressure",
269
+ currentLocationName = "Baker Island",
270
+ initialReflections = {
271
+ "I am a baker and I make food for everyone that pass by.",
272
+ "I am a bit stressed that the flour didn't arrived yet, my cousin Joe should arrive soon with the delivery but he is late and I worry a bit.",
273
+ "I love living here on these floating islands, the view is amazing from my wind mill.",
274
+ "I like to talk to strangers like the pirate that just arrived or the scientist coming time to time to feed his pet.",
275
+ },
276
+ },
277
+ {
278
+ name = "npcpirate",
279
+ gameName = "Pirate",
280
+ physicalDescription = "Average height, with bright green eyes and a warm smile",
281
+ psychologicalProfile = "Friendly and helpful, quick-witted and resourceful",
282
+ currentLocationName = "Pirate Island",
283
+ initialReflections = {
284
+ "Ahoy, matey! I'm Captain Ruby Storm, a fearless lass from the seven skies.",
285
+ "I've docked me floating ship on this here floating isle to sell me wares (almost legally) retrieved treasures from me last daring adventure.",
286
+ "So, who be lookin' to trade with a swashbuckler like meself?",
287
+ },
288
+ },
289
+ }
290
+
291
+ local gigaxWorldConfig = {
292
+ simulationName = SIMULATION_NAME,
293
+ simulationDescription = SIMULATION_DESCRIPTION,
294
+ startingLocationName = "Center",
295
+ skills = skills,
296
+ locations = locations,
297
+ NPCs = NPCs,
298
+ }
299
+
300
+ findLocationByName = function(targetName, config)
301
+ for _, node in ipairs(config.locations) do
302
+ if string.lower(node.name) == string.lower(targetName) then
303
+ return node.position
304
+ end
305
+ end
306
+ end
307
+
308
+ Client.OnWorldObjectLoad = function(obj)
309
+ if obj.Name == "pirate_ship" then
310
+ obj.Scale = 1
311
+ end
312
+
313
+ local locationsIndexByName = {}
314
+ for k, v in ipairs(gigaxWorldConfig.locations) do
315
+ locationsIndexByName[v.name] = k
316
+ end
317
+ local npcIndexByName = {
318
+ NPC_scientist = 1,
319
+ NPC_baker = 2,
320
+ NPC_pirate = 3,
321
+ }
322
+
323
+ local index = npcIndexByName[obj.Name]
324
+ if index then
325
+ local pos = obj.Position:Copy()
326
+ gigaxWorldConfig.NPCs[index].position = pos
327
+ gigaxWorldConfig.NPCs[index].rotation = obj.Rotation:Copy()
328
+
329
+ local locationName = gigaxWorldConfig.NPCs[index].currentLocationName
330
+ local locationIndex = locationsIndexByName[locationName]
331
+ gigaxWorldConfig.locations[locationIndex].position = pos
332
+ obj:RemoveFromParent()
333
+ end
334
+ end
335
+
336
+ Client.OnStart = function()
337
+ easy_onboarding:startOnboarding(onboardingConfig)
338
+
339
+ require("object_skills").addStepClimbing(Player, {
340
+ mapScale = MAP_SCALE,
341
+ collisionGroups = Map.CollisionGroups,
342
+ })
343
+
344
+ gigaxWorldConfig.locations[4].position = Number3(Map.Width * 0.5, Map.Height - 2, Map.Depth * 0.5) * Map.Scale
345
+
346
+ floating_island_generator:generateIslands({
347
+ nbIslands = 20,
348
+ minSize = 4,
349
+ maxSize = 7,
350
+ safearea = 200, -- min dist of islands from 0,0,0
351
+ dist = 750, -- max dist of islands
352
+ })
353
+
354
+ local ambience = require("ambience")
355
+ ambience:set(ambience.dusk)
356
+
357
+ sfx = require("sfx")
358
+ Player.Head:AddChild(AudioListener)
359
+
360
+ dropPlayer = function()
361
+ Player.Position = Number3(Map.Width * 0.5, Map.Height + 10, Map.Depth * 0.5) * Map.Scale
362
+ Player.Rotation = { 0, 0, 0 }
363
+ Player.Velocity = { 0, 0, 0 }
364
+ end
365
+ World:AddChild(Player)
366
+ dropPlayer()
367
+
368
+ dialog = require("dialog")
369
+ dialog:setMaxWidth(400)
370
+
371
+ pathfinding:createPathfindingMap()
372
+
373
+ gigax:setConfig(gigaxWorldConfig)
374
+
375
+ local randomNames = { "aduermael", "soliton", "gdevillele", "caillef", "voxels", "petroglyph" }
376
+ Player.Avatar:load({ usernameOrId = randomNames[math.random(#randomNames)] })
377
+ end
378
+
379
+ Client.Action1 = function()
380
+ if Player.IsOnGround then
381
+ sfx("hurtscream_1", { Position = Player.Position, Volume = 0.4 })
382
+ Player.Velocity.Y = 100
383
+ if Player.Motion.X == 0 and Player.Motion.Z == 0 then
384
+ -- only play jump action when jumping without moving to avoid wandering around to trigger NPCs
385
+ gigax:action({
386
+ name = "JUMP",
387
+ description = "Jump in the air",
388
+ parameter_types = {},
389
+ action_format_str = "{protagonist_name} jumped up in the air for a moment.",
390
+ })
391
+ end
392
+ end
393
+ end
394
+
395
+ Client.Tick = function(dt)
396
+ if Player.Position.Y < -500 then
397
+ dropPlayer()
398
+ end
399
+ end
400
+
401
+ Client.OnChat = function(payload)
402
+ local msg = payload.message
403
+
404
+ Player:TextBubble(msg, 3, true)
405
+ sfx("waterdrop_2", { Position = Player.Position, Pitch = 1.1 + math.random() * 0.5 })
406
+
407
+ gigax:action({
408
+ name = "SAY",
409
+ description = "Say smthg out loud",
410
+ parameter_types = { "character", "content" },
411
+ action_format_str = "{protagonist_name} said '{content}' to {target_name}",
412
+ content = msg,
413
+ })
414
+
415
+ print("User: " .. payload.message)
416
+ return true
417
+ end
418
+
419
+ onboardingConfig = {
420
+ steps = {
421
+ {
422
+ start = function(onboarding)
423
+ local data = {}
424
+ data.ui = onboarding:createTextStep("1/3 - Hold click and drag to move the camera.")
425
+ data.listener = LocalEvent:Listen(LocalEvent.Name.PointerDrag, function()
426
+ Timer(1, function()
427
+ onboarding:next()
428
+ end)
429
+ data.listener:Remove()
430
+ end)
431
+ return data
432
+ end,
433
+ stop = function(_, data)
434
+ data.ui:remove()
435
+ end,
436
+ },
437
+ {
438
+ start = function(onboarding)
439
+ local data = {}
440
+ data.ui = onboarding:createTextStep("2/3 - Use WASD/ZQSD to move.")
441
+ data.listener = LocalEvent:Listen(LocalEvent.Name.KeyboardInput, function()
442
+ Timer(1, function()
443
+ onboarding:next()
444
+ end)
445
+ data.listener:Remove()
446
+ end)
447
+
448
+ return data
449
+ end,
450
+ stop = function(_, data)
451
+ data.ui:remove()
452
+ end,
453
+ },
454
+ {
455
+ start = function(onboarding)
456
+ local data = {}
457
+ data.ui = onboarding:createTextStep("3/3 - Press Enter in front of the Pirate to chat.")
458
+ Timer(10, function()
459
+ onboarding:next()
460
+ end)
461
+ return data
462
+ end,
463
+ stop = function(_, data)
464
+ data.ui:remove()
465
+ end,
466
+ },
467
+ },
468
+ }
map.b64 ADDED
@@ -0,0 +1 @@
 
 
1
+ AwAAAAAAAAAUQBQAAABjYWlsbGVmLmZvdXJfaXNsYW5kcwLjFAAAOAAMAHZveGVscy5jaGVzdAEABGlkJDE4NWQwYjAwLWUyNGItNDU3Mi1hODYxLWRlN2JlZTc5YzhjZHBvsJFIQ///qUIocVNCcm8AAAAA3A9JQAAAAABzYwAAAD8AAAA/AAAAPxIAdWV2b3hlbC5qdXRlX2JhZzAxAQAEaWQkMTNhOTM0YjctNWI3Zi00YWJmLTgxNzEtOTYzMjQzZDYxNWZhcG+1m2VD//+pQs2YukJybwAAAADkyxZAAAAAAHNj+FjKPvhYyj74WMo+FgBwZXRyb2dseXBoLmNhbm5vbl9iYWxsBAAEaWQkMmNlN2ZlZGMtMmRiZi00Y2VjLWFjYTQtNWI5YzBmMTc0NzUxcG/oJWlDAACqQlQdeUJybwAAAADdD8k+AAAAAHNjAAAAPwAAAD8AAAA/BGlkJDZjZDc5ZDBjLTFhYzAtNDM0Yy1hMWIyLWFhZWY3N2UxODBlY3BvROhqQwEArULs/XNCcm8AAAAA3Q/JPgAAAABzYxcAAD8XAAA/FwAAPwRpZCRlMDQzN2NiYy03MDhkLTRiYWItYjI0Ni03MDEwNzY0YjBmODNwbxI8bEP//6lCDqN6QnJvAAAAAN0PyT4AAAAAc2MAAAA/AAAAPwAAAD8EaWQkNTU1MzVhMGMtYTU1ZS00MDE0LWFiOGItZDgyYWViZjAyZDY4cG8rFmtDAACqQuWMb0JybwAAAADdD8k+AAAAAHNjAAAAPwAAAD8AAAA/FAB2b3hlbHMuZG9vcl95ZWxsb3dfMgEABGlkJGFjYzdjZmJlLTRiNDktNGYwNy1hYWRmLWMzNDdhYWUxYzkwZHBvPNhJQv//qUIiKFpCcm8AAAAA3A/JPwAAAABzY+OLbz/ji28/44tvPw8AZmxhZmlsZXouZXZlbHluAQAEaWQkZGQwNzZjYTctMGIzNy00MTcyLTgwNTYtMWU2YjM3ZWVkNmRhcG9viFNDAACqQpFDakJyb+TLlkDg7a8/AAAAAHNjAAAAPwAAAD8AAAA/DQB1ZXZveGVsLmNoYWlyAQAEaWQkYzVhZWMwNDEtOWQyZS00ODM1LTkxYWUtMjRhYjgyNjNkNGRlcG8taiRCAACqQuCCKUNybwAAAADcD8k/AAAAAHNjMHBJPzBwST8wcEk/EgB2b3hlbHMuY3JhdGVfc21hbGwDAARpZCQ0ZjFhNGJkOC05NmMwLTRlNzgtYTE3OC00NmZlNGU5YzcyOGRwbwu4XUMAAKpC2gmiQnNjAAAAPwAAAD8AAAA/cG0DAAAABWlkJDY3ZTVmNDE0LWJmNTUtNDBhNi05MDA3LTg4MmQwNzQwMmZhZHBvaQRiQwAAoEJZrAZDcm8AAAAAXMfCQAAAAABzYwAAAD8AAAA/AAAAP3BtAwAAAAVpZCQ2MjJmZTliMC1jNDdmLTQwOWUtOTM2MC1lYmY5YTFiMGY1NTVwbwRcnUMAAMJC7AQTQ3JvAAAAAODtr0AAAAAAc2MMAAA/DAAAPwwAAD9wbQMAAAAPAHByYXRhbWFjYW0uYXBwYQEABGlkJDBmYTI4Y2QzLTAwMTEtNDk0Ny04NTg0LTljMmExNTU2ODYwZXBvyGWLQgIApkI2WRZDcm8AAAAA3A/JPwAAAABzYwJBGz8CQRs/AkEbPxMAdm94ZWxzLmNyYXRlX21lZGl1bQIABWlkJGQ4NTIzMDFjLWFmMTUtNDA1OS1iYzNmLTRkYTBmMjdiYWNhM3Bvs/ZmQwQAvkIHho5Ccm8AAAAA5MsWPwAAAABzYxMAAD8TAAA/EwAAP3BtAwAAAAVpZCQ0ZjhmNTY5Ny02NTZmLTRiMWYtOTMyYS0yMDBiZjYzNjA1ZmFwb+U6W0P//6lCeyCHQnJvAAAAAFzHwkAAAAAAc2PMheM+zIXjPsyF4z5wbQMAAAAQAGFkdWVybWFlbC5hdmF0YXIDAAVpZCRmZTk4NjYwZS0zMjBkLTQ2YjktOGI0MS02ZGE0NzIxZDJiN2Zwb3bbSkMAAKBCBsL/QnJvAAAAAOY6ikAAAAAAc2MAAAA/AAAAPwAAAD9uYQpOUENfcGlyYXRlBWlkJGQxYWYwNmQ5LWMzNWItNGFjZS1hYWFjLWRjM2M1NTU4NjEzOHBv1v9/Qv//qUJUEX5Ccm8AAAAA4lwjQAAAAABzYwAAAD8AAAA/AAAAP25hCU5QQ19iYWtlcgVpZCRkZjVmZjIzZi1kMGFmLTRmMmQtOWY1Ny1iNTZiZGM4ZjhiYzJwb3zHWEL//6lCmSAqQ3JvAAAAANwPyT8AAAAAc2MAAAA/AAAAPwAAAD9uYQ1OUENfc2NpZW50aXN0FQBwaWFhLnBpcmF0ZV90ZWxlc2NvcGUBAARpZCRmODA2NGZiZC03MmNmLTQyMjEtYjRiYy0zMDhlMTc3ZjdlNDRwb87MT0MAAKBCZPfrQnJvAAAAAOTLFj8AAAAAc2MAAAA/AAAAPwAAAD8PAHZveGVscy53aW5kbWlsbAEAA2lkJGVjZTRmODMyLWYyOWItNGQ2My04ZWI1LTkzOWRlNWIzZjMzN3BvPLntQQAAqkL9M0dCc2M4ZRRAOGUUQDhlFEASAHVldm94ZWwucHJvc2NpdXR0bwEABGlkJDkxNDhmNjQ1LWQyZmQtNDc0Zi05NzY2LWNhYTY0ZjBiMDJlYnBvztxmQwQAvkI2XalCcm8AAAAAYaWpQAAAAABzY2INlz5iDZc+Yg2XPg0AdWV2b3hlbC5qYW0wMQEABGlkJDViZGFjNmNlLTU4ZDUtNGJhYy1iOTJiLTE1ZTkyZjI1MjRhM3BvM81JQgAAqkIZCDdDcm8AAAAA3Q/JPgAAAABzYxc4nD4XOJw+FzicPhAAd3JkZW4ucnVtX2JhcnJlbAIAA2lkJDdiYjA0NTJlLTk4MGMtNDRmYi05YjIwLWJiYmU5NjI5YzJjZnBv+v4rQwAAoELK7KpCc2MAAAA/AAAAPwAAAD8EaWQkNzkzZDFhMjUtNGViYy00MDBjLTlmNGYtYTJkNjYxZjlhYTNhcG/6/jlDAQCgQsrsqkJybwAAAABcx8JAAAAAAHNjAAAAPwAAAD8AAAA/EAB2b3hlbHMuc3BhY2VzaGlwAQAGaWQkODIxZGVlZmQtMWUyNi00YTg0LTliMGEtMWQyMDFjMDNjMmIzcG/upJFC8720QsvwZUNyb33deDvbD8lA3A9JQHNjMCOTPzAjkz8wI5M/bmEJc3BhY2VzaGlwcG0DAAAAFABwcmF0YW1hY2FtLnNob3J0Y2FrZQEABGlkJDY1OTYzYWRkLWM5OTEtNGYyOC1iMzBkLWQ3MjI0ODg1ZmJmMnBvREg7QjMzqkIHqC9Dcm+Uvjs05MsWP2EQSUBzY73zpT+986U/vfOlPwwAdWV2b3hlbC5idXNoAgAFaWQkYTkyZTM2ODYtMGY1NC00ZTFmLTk5ODEtOTEwNjgyZmZjMjhlcG/EJ7FC//+pQgGFRENybwAAAADh7a8/AAAAAHNjDgAAPw4AAD8OAAA/cG0AAAAABWlkJDU1MjMyOWNjLTVhYTUtNGU4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/6lC4u5NQ3NjPwAAPz8AAD8/AAA/DAB2b3hlbHMuYXBwbGUCAARpZCQ3YzBkZDVmNi0zMzkwLTQ3MDctOWNhNC01ZDI5M2JmMWU2ZDJwb2ZqWkNYOLhC+yKKQnJvAAAAANFT+z8AAAAAc2MAAAA/AAAAPwAAAD8EaWQkZTRjNjBkOTctZDhiNC00MzRjLTgwNGMtN2RjZjQyMGNjYzUwcG8tkl5D/v+pQptPrkJybwAAAADRU3s/AAAAAHNjAAAAPwAAAD8AAAA/EABrb29vdy53aW5lX2JsYWNrAQAEaWQkY2VjODU3MjItYmRhZi00MDY0LThkZjUtMDBiYTIwZWJiMjFicG8bqltDAACqQhB1kkJybwAAAADdD8k+AAAAAHNjUE+OPlBPjj5QT44+EwB2b3hlbHMuYmFybGV5X2NodW5rAwAFaWQkMTVhNmQyMDEtOGEwNS00NzAyLWFmMDAtZGNmZTkzNTliN2Y3cG+gpyxC//+pQiR/XUFybwAAAADSU/s/AAAAAHNjymRsP8pkbD/KZGw/cG0AAAAABWlkJGI3NGRhMzZlLTIyMGUtNGRiNC04NzYzLWVlMDc0MWQ3MGVjY3BvjDhUQgAAqkLYVfFAcm8AAAAA3Q/JPgAAAABzY4UDaD+FA2g/hQNoP3BtAAAAAARpZCQ0ZTllZjdiMC0wNTQ2LTQ5MDMtYmMxYi00NTk1M2JjM2I3NzFwb0L9I0IAAKpCMK2RQHNjmANoP5gDaD+YA2g/cG0AAAAAEABwaWFhLnBpcmF0ZV9zaGlwAQAEaWQkZDNjNzM4NDYtYjZkMS00MDdiLWJkNmQtYzg5ZmFjODcyYzUzcG/G25NDAAB0Qlz5zUJzY9/ZAj/f2QI/39kCP25hC3BpcmF0ZV9zaGlwDgB1ZXZveGVsLmJhc2tldAEABGlkJGFhYTg4YWY5LTYxM2QtNDYxNS1hOGJkLWYyYzBjYjM2MjA2N3BveWIrQgAAqkKwdTRDcm8AAAAA3Q/JPgAAAABzYwAAAD8AAAA/AAAAPxIAa25vc3ZveGVsLm9ha190cmVlAQAEaWQkMDhhMjljNjItYmIzOC00ZDU3LTgwMzAtY2JiZDU1YmExZDA5cG/GSDdCAACqQgiSQ0NybwAAAADcD8k/AAAAAHNj2zCWP9swlj/bMJY/