Upload 5 files
Browse files- README.md +106 -5
- cubzh.html +61 -0
- cubzh.json +15 -0
- cubzh.lua +468 -0
- map.b64 +1 -0
README.md
CHANGED
|
@@ -1,11 +1,112 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: static
|
|
|
|
| 7 |
pinned: false
|
| 8 |
license: mit
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
---
|
| 10 |
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
[](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/
|