aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/abilities.moon5
-rw-r--r--src/channel.moon78
-rw-r--r--src/client.moon197
-rw-r--r--src/client/init.moon140
-rw-r--r--src/clipboard_bridge.js14
-rw-r--r--src/color.moon36
-rw-r--r--src/conf.lua1
-rw-r--r--src/controller_bridge.js38
-rw-r--r--src/ecs.moon184
-rw-r--r--src/ecs/char_controller.moon70
-rw-r--r--src/ecs/client_networked.moon19
-rw-r--r--src/ecs/component.moon16
-rw-r--r--src/ecs/graphics.moon67
-rw-r--r--src/ecs/networked.moon71
-rw-r--r--src/ecs/player_graphic.moon66
-rw-r--r--src/ecs/predicted.moon25
-rw-r--r--src/ecs/script.moon16
-rw-r--r--src/hub.moon184
-rw-r--r--src/js_bridge.js134
-rw-r--r--src/levels/entmaker.moon21
-rw-r--r--src/levels/game.moon7
-rw-r--r--src/levels/lobby.moon111
-rw-r--r--src/log.moon37
-rw-r--r--src/main.lua114
-rw-r--r--src/menu/game.moon208
-rw-r--r--src/menu/input.moon30
-rw-r--r--src/menu/lobby.moon103
-rw-r--r--src/menu/main.moon137
-rw-r--r--src/menu/playername.moon23
-rw-r--r--src/menu/settings.moon58
-rw-r--r--src/menu/tutorial.moon106
-rw-r--r--src/net.moon281
m---------src/party/English-word-lists-parts-of-speech-approximate0
m---------src/party/hc0
-rw-r--r--src/party/qrcodejs/qrcode.js1
-rw-r--r--src/party/qrcodejs/qrcode.min.js1
-rw-r--r--src/poems.moon348
-rw-r--r--src/prefab/cabin.moon79
-rw-r--r--src/prefab/hall.moon21
-rw-r--r--src/prefab/lobby.moon112
-rw-r--r--src/prefab/room.moon10
-rw-r--r--src/prefab/spawn.moon3
-rw-r--r--src/prefab/worldgen.moon58
-rw-r--r--src/preload.lua107
-rw-r--r--src/rng.moon46
-rw-r--r--src/server/init.moon143
-rw-r--r--src/settings.moon19
-rw-r--r--src/shader_shim.moon22
-rw-r--r--src/shaders/lake.frag93
-rw-r--r--src/shaders/lake.moon29
-rw-r--r--src/shaders/lake.vert13
-rw-r--r--src/shaders/land.frag100
-rw-r--r--src/shaders/land.vert26
-rw-r--r--src/shaders/palette.vert26
-rw-r--r--src/shaders/player.frag14
-rw-r--r--src/shaders/player.vert23
-rw-r--r--src/shaders/stars.frag5
-rw-r--r--src/shaders/stars.lua75
-rw-r--r--src/shaders/stars.vert30
-rw-r--r--src/shaders/world.frag29
-rw-r--r--src/shaders/world.frag.back20
-rw-r--r--src/shaders/world.moon356
-rw-r--r--src/shaders/world.moon.back327
-rw-r--r--src/shaders/world.vert26
-rw-r--r--src/shaders/world.vert.back26
-rw-r--r--src/shared/player_movement.moon54
-rw-r--r--src/task.moon25
-rw-r--r--src/textbox_bridge.js74
-rw-r--r--src/ui.moon269
-rw-r--r--src/ui/button.moon133
-rw-r--r--src/ui/checkbox.moon13
-rw-r--r--src/ui/joystick.moon98
-rw-r--r--src/ui/textbox.moon78
-rw-r--r--src/util.lua142
-rw-r--r--src/window.moon11
-rw-r--r--src/world.moon52
76 files changed, 5734 insertions, 0 deletions
diff --git a/src/abilities.moon b/src/abilities.moon
new file mode 100644
index 0000000..d2178fa
--- /dev/null
+++ b/src/abilities.moon
@@ -0,0 +1,5 @@
+x = {}
+
+x["a pawn"] = "You may not reveal"
+
+x
diff --git a/src/channel.moon b/src/channel.moon
new file mode 100644
index 0000000..6f87d46
--- /dev/null
+++ b/src/channel.moon
@@ -0,0 +1,78 @@
+
+-- Implements a channels, an abstraction for sending data
+
+class Channel
+ new: (settings) =>
+ settings = settings or {}
+ --Every channel has at least a buffer, the messages to be read on recv()
+ @buffer = {}
+ for setting_name, setting_value in pairs settings
+ @[setting_name] = setting_value
+ poll: =>
+ error("Channel must implement poll")
+ send: (message) =>
+ error("Channel must implement message")
+ recv: =>
+ error("Channel must implement recv")
+
+class SimpleChannel extends Channel
+ @time = 0
+ new: (settings) =>
+ super(settings)
+ poll: =>
+ #@buffer > 0
+ send: (message) =>
+ table.insert(@buffer, message)
+ recv: =>
+ table.remove(@buffer, 1)
+
+class FaultyChannel extends Channel
+ -- Mock channel for testing
+ @time = 0
+ new: (settings) =>
+ @to_deliver = {}
+ @avg_latency = 0 -- in ms
+ @latency_std = 0
+ -- Latency can never be below 0, but can go up as much as it likes
+ @loss = 0.1 -- between 0 (never) and 1 (always)
+ super(settings)
+ @normal_at: (avg, std, n) ->
+ assert(avg and std and n, string.format("normal(avg, std, n) called with %q %q %q", tostring(avg), tostring(std), tostring(n)))
+ -- Normal curve probability at N
+ (1/ (math.sqrt(2*math.pi) * avg)) * math.exp(-(n - avg)^2 / (2 * (std^2)))
+ @normal: (avg, std) =>
+ -- Box-Muller transform
+ bm = math.sqrt(-2 * math.log(math.random())) * math.cos(2 * math.pi * math.random())
+ -- Box-Muller gives us std = e^-0.5 , avg = 0
+ ((bm / math.exp(-1/2)) * std) + avg
+ poll: =>
+ @pump!
+ #@buffer > 0
+ send: (message) =>
+ -- Do we deliver?
+ rng = math.random()
+ if @loss > rng
+ return
+ -- How long does it take?
+ -- Only uses the positive half of the normal distribution, double the standard deviation?
+ time = @@normal(@avg_latency, @latency_std * 2, math.random())
+ if time < 0 then
+ time = 0 -- We can't deliver messages in the past
+ table.insert(@to_deliver, {message,@@time + time})
+ recv: =>
+ @pump!
+ table.remove(@buffer, 1)
+ pump: =>
+ defrag = 1
+ deliver_len = #@to_deliver
+ for k,tbl in ipairs(@to_deliver)
+ {m, t} = tbl
+ @to_deliver[defrag] = tbl
+ if @@time > t
+ table.insert(@buffer, m)
+ else
+ defrag = defrag + 1
+ for i = defrag, deliver_len do
+ @to_deliver[i] = nil
+
+{:Channel, :SimpleChannel, :FaultyChannel}
diff --git a/src/client.moon b/src/client.moon
new file mode 100644
index 0000000..6228ff8
--- /dev/null
+++ b/src/client.moon
@@ -0,0 +1,197 @@
+-- Hub-and-spoke networking client
+-- Connects to hub and provides router registration for message handling
+
+net = require "net"
+log = require "log"
+world = require "world"
+
+-- Register message types for hub->client communication
+
+net.register_message("to_client", {
+ required: {
+ target: "string"
+ message_type: "string"
+ }
+ optional: {
+ data: "table"
+ }
+})
+
+net.register_message("to_many_clients", {
+ required: {
+ targets: "table"
+ message_type: "string"
+ }
+ optional: {
+ data: "table"
+ }
+})
+
+net.register_message("broadcast", {
+ required: {
+ message_type: "string"
+ }
+ optional: {
+ data: "table"
+ }
+})
+
+class Client
+ new: (name) =>
+ @name = name or "anonymous"
+ @peer = nil
+ @hub_connection = nil
+ @hub_id = nil
+ @connected = false
+ @routes = {} -- message_type -> handler function
+ @on_connect_callbacks = {}
+ @on_disconnect_callbacks = {}
+ @initialized = false
+
+ initialize: =>
+ if @initialized
+ return
+ @peer = net.Peer!
+ log.info("Client peer created: #{@peer.id}", {"client", "net"})
+ @initialized = true
+
+ connect_to_hub: (hub_id) =>
+ if not @initialized
+ @initialize!
+
+ @hub_id = hub_id
+ @hub_connection = @peer\connect(hub_id)
+
+ -- Set up connection handlers
+ @hub_connection\on("open", ->
+ @connected = true
+ log.info("Connected to hub: #{hub_id}", {"client", "net"})
+
+ -- Send registration message using Connection:send(msgname, msg)
+ @hub_connection\send("Join", {name: @name})
+
+ -- Surface client connect/join to the browser for integration tests.
+ if am and am.eval_js and am.to_json
+ js = string.format("window._clientConnectedToHub = true; window._clientJoinPayload = %s;", am.to_json({name: @name}))
+ am.eval_js(js)
+
+ -- Notify connection callbacks
+ for callback in *@on_connect_callbacks
+ callback!
+ )
+
+ @hub_connection\on("data", (msgname, data) ->
+ @handle_message(msgname, data)
+ )
+
+ @hub_connection\on("close", ->
+ @connected = false
+ log.info("Disconnected from hub", {"client", "net"})
+
+ -- Notify disconnect callbacks
+ for callback in *@on_disconnect_callbacks
+ callback!
+ )
+ while not @connected
+ coroutine.yield!
+
+ handle_message: (callback_id, message_data) =>
+ log.info("Client handle_message callback_id=" .. tostring(callback_id) .. " message_data=" .. tostring(message_data), {"net", "client", "debug"})
+ -- message_data is the array [message_type, data] sent by hub
+ if type(message_data) ~= "table" or #message_data < 1
+ log.warn("Received invalid message format: " .. tostring(message_data), {"client", "net"})
+ return
+ if type(message_data[1][1]) != "string"
+ log.warn("Received invalid mesage type: " .. tostring(message_data[1][1]), {"client","net"})
+
+ msg_type = message_data[1][1]
+ msg_data = message_data[1][2] or {}
+
+ log.info("Message type: #{msg_type}", {"net", "client"})
+ if msg_type == "Join"
+ log.error("Client saw Join message in handle_message; this should be hub-only", {"client", "net", "debug"})
+
+ if not msg_type or type(msg_type) ~= "string"
+ log.warn("Received message without valid type:" .. tostring(msg_type), {"client", "net"})
+ return
+
+ world.domain = "client"
+ if @routes[msg_type]
+ -- Route to registered handlers
+ callbacks = @routes[msg_type]
+ for _, callback in pairs(callbacks)
+ callback(@hub_id, msg_data)
+ else
+ log.warn("No handler for message type: " .. tostring(msg_type), {"client", "net"})
+ msg_types = [key for key, _ in pairs(@routes)]
+ if #msg_types > 0
+ log.warn("Registered message types: " .. table.concat(msg_types, ","), {"client", "net"})
+
+ -- Register a router for a specific message type
+ -- callback is a (server-id:string, data:tbl) -> nil
+ listen: (message_type, id, callback) =>
+ assert(type(callback) == "function", "Listened with something that is not a function")
+ @routes[message_type] = @routes[message_type] or {}
+ id = id or #@routes[message_type] + 1
+ @routes[message_type][id] = callback
+ log.info("Router registered for #{message_type}", {"client", "net"})
+ id
+
+ -- Unregister a router
+ defen: (message_type, id) =>
+ if not @routes[message_type] or @routes[message_type][id] == nil
+ log.warn("Removing listener that doesn't exist: #{message_type}", {"client", "net"})
+ return
+ @routes[message_type][id] = nil
+ log.info("Listener removed for #{message_type}", {"client", "net"})
+
+ -- Send message to hub
+ send: (message_type, data) =>
+ if not @connected
+ log.error("Cannot send - not connected to hub", {"client", "net"})
+ return false
+
+ log.info("Client sending #{message_type}", {"net", "client"})
+ @hub_connection\send(message_type, data or {})
+ true
+
+ on_connect: (callback) =>
+ table.insert(@on_connect_callbacks, callback)
+
+ on_disconnect: (callback) =>
+ table.insert(@on_disconnect_callbacks, callback)
+
+ -- Synchronus request/response for use in coroutines.
+ sync: (request, request_data, response) =>
+ returned = nil
+ lid = @listen(response, nil, (peer, data) ->
+ returned = data
+ )
+ @send(request, request_data)
+ tries = 1
+ start = am.current_time!
+ while not returned and tries < 4
+ log.info("Awaiting synchronus response to " .. request, {"net","client"})
+ coroutine.yield!
+ if am.current_time! - start > 4
+ log.info("Async response timeout, requesting again...",{"net","client"})
+ @send(request, request_data)
+ start = am.current_time!
+ tries += 1
+ if tries == 4
+ error("Failed in sync request after 4 tries")
+ @defen(response, lid)
+ return returned
+
+ is_connected: =>
+ @connected
+
+ disconnect: =>
+ if @hub_connection
+ @hub_connection\close!
+ @connected = false
+
+ pump: =>
+ net.pump!
+
+{:Client}
diff --git a/src/client/init.moon b/src/client/init.moon
new file mode 100644
index 0000000..5aae780
--- /dev/null
+++ b/src/client/init.moon
@@ -0,0 +1,140 @@
+
+net = require("net")
+world = require("world")
+ui = require("ui")
+log = require("log")
+ClientNetworkedComponent = require("ecs.client_networked")
+GraphicsComponent = require("ecs.graphics")
+PlayerGraphicComponent = require("ecs.player_graphic")
+CharacterControllerComponent = require("ecs.char_controller")
+PredictedComponent = require("ecs.predicted")
+ScriptComponent = require("ecs.script")
+player_movement = require("shared.player_movement")
+game_menu = require("menu.game")
+
+ecs = require("ecs")
+sprites =require("sprites")
+
+net.register_message("CreatePawn",{
+ peerid: "string"
+})
+x = {}
+
+net.register_message("Notify",{
+ required: {
+ message: "string"
+ }
+})
+
+create_entity = (id, chunk) ->
+ log.info("Creating entity of type" .. tostring(chunk.data.type), {"ecs","net","client"})
+ ent = nil
+ switch chunk.data.type
+ when "level"
+ --ent = ecs.Entity(id)
+ log.info("TODO: What do we refresh with level data?",{"ecs","net","client"})
+ --nents = #world.level.entities
+ --net_comp = ClientNetworkedComponent("net",chunk.data)
+ --ent\add(net_comp,"net")
+ require(chunk.data.level_name).create(unpack(chunk.data.level_data))
+ --assert(#world.level.entities == nents, "Entities created in level loading")
+ when "player"
+ ent = ecs.Entity(id)
+ log.info("Creating player",{"ecs","net","client","player"})
+ net_comp = ClientNetworkedComponent("net",chunk.data)
+ ent\add(net_comp,"net")
+ graphic = GraphicsComponent("graphic",{graphic: sprites.player})
+ ent\add(graphic,"graphic")
+ predicted = PredictedComponent("pred",{acc: {0,0,0}, vel: {0,0,0}, pos:{0,0,0}}, "net", player_movement)
+ ent\add(predicted,"pred")
+ -- If this is "our" pawn, move our view with it
+ if chunk.data.peerid == world.network.peer.id
+ controller = CharacterControllerComponent("controller",player_movement,"net")
+ ent\add(controller,"controller")
+ move_view = ScriptComponent("move_view",{
+ script: () ->
+ loc = predicted.properties.pos
+ world.world_x = loc[1]
+ world.world_y = loc[2]
+ --graphic\moveto(vec3(loc[1],loc[2],loc[3]))
+ })
+ ent\add(move_view,"move_view")
+ move_graphic = ScriptComponent("move_graphic", {
+ script: () ->
+ loc = predicted.properties.pos
+ graphic\moveto(vec3(loc[1],loc[2],loc[3]))
+ })
+ ent\add(move_graphic, "move_graphic")
+
+ else
+ error("Tring to create unknown entity:" .. tostring(id) .. ":" .. tostring(data))
+ --assert(ent.components.net, "Failed to find net component")
+ return ent
+
+x.initialize = () ->
+ assert(world, "World must be available")
+ assert(world.network, "Network must be initialized")
+ localpawns = {}
+ net = nil
+ world.network\listen("CreatePawn", nil, (_, data) ->
+ log.info("Creating pawn:" .. tostring(data), {"net","ecs","client"})
+ unpacked = am.parse_json(data)
+ ent_id = unpacked.id
+ create_entity(ent_id, unpacked)
+ --ent = ecs.Entity(ent_id)
+ --net = ClientNetworkedComponent("net",unpacked.data)
+ --ent\add(net,"net")
+ --graphic = GraphicsComponent("graphic",{
+ --graphic: sprites.player
+ --})
+ --ent\add(graphic,"graphic")
+ --pred = PredictedComponent("pred",{acc:{0,0,0}, vel: {0,0,0}, pos: {0,0,0}}, "net", player_movement)
+ --ent\add(pred, "pred")
+ )
+ world.network\listen("update","client update",(_, data) ->
+ log.info("Updating:" .. tostring(data.id), {"net","ecs","client"})
+ --log.info("Entities were:" .. tostring(world.level.entities),{"net","client","ecs"})
+ log.debug("World entities are:" .. tostring(world.level.entities), {"ecs","client","net"})
+ if not world.level.entities[data.id]
+ log.error("Client instructed to update id " .. data.id .. " but no entity exists! Entities were:" .. tostring(world.level.entities),{"net","client","ecs"})
+ ent = world.level.entities[data.id]
+ net_component = ent\get(data.name)
+ if not net_component
+ error("Client instructed to update id " .. data.id .. " with network component named " .. tostring(data.name) .. " but no such component exists, entity was:" .. tostring(ent))
+ net_component.properties = data
+ if net_component == net
+ print("Updating pawn net component")
+ log.info("Successfully updated entity" .. tostring(data.id), {"ecs","client","net"})
+ )
+ entities_responded = false
+ world.network\listen("RespondEntities","Respond entities",(_, data) ->
+ -- this is a full refresh, we can wipe entities and build them from scratch.
+ for _, ent in pairs(world.level.entities)
+ ent\destroy!
+ log.info("Wiping and re-creating entities:" .. tostring(data), {"ecs","net","client"})
+ assert(type(data) == "table")
+ for id, properties in pairs(data)
+ assert(type(id) == "number")
+ assert(type(properties) == "string")
+ create_entity(id, am.parse_json(properties))
+ log.info("Successfully created entity" .. tostring(id), {"ecs","net","client"})
+ assert(#world.level.entities == #data, "Failed to create entities correctly")
+ )
+ world.network\send("RequestEntities", {})
+ world.network\listen("RespondRole", "Respond role", (_, data) ->
+ -- By this time we're in the game, destory any entities laying around.
+ for _, ent in pairs(world.level.entities)
+ ent\destroy!
+ log.info("Got role from server:" .. tostring(data), {"net","client"})
+ game_menu.create_graphic(data)
+ am.save_state("gameplay", data)
+ -- Kinda hacky, but if we have a role, just disable net pumping
+ -- to prevent errors on net.create()
+ error("Preventing furhter network pumping!")
+ net.pump = () ->
+ log.info("Tried to pump net!",{"net","client"})
+ )
+ --world.network\send("RequestRole",{})
+ log.info("Client fully initialized",{"net","client","graphic"})
+ true
+x
diff --git a/src/clipboard_bridge.js b/src/clipboard_bridge.js
new file mode 100644
index 0000000..6542fe5
--- /dev/null
+++ b/src/clipboard_bridge.js
@@ -0,0 +1,14 @@
+
+window.CLIPBOARD = {
+ get_params: function(){
+ var params = new URLSearchParams(window.location.search);
+ var tbl = {};
+ params.forEach(function(value, key) {
+ tbl[key] = value;
+ });
+ return tbl;
+ },
+ get_path: function(){
+ return window.location.origin + window.location.pathname;
+ }
+};
diff --git a/src/color.moon b/src/color.moon
new file mode 100644
index 0000000..5bebd20
--- /dev/null
+++ b/src/color.moon
@@ -0,0 +1,36 @@
+
+color =
+ background: {116, 78, 68}
+ foreground: {231, 235, 197}
+ shadow: {78, 68, 55}
+ highlight: {145, 174, 134}
+
+am_color = {k, vec4(v[1]/255,v[2]/255,v[3]/255,1) for k,v in pairs(color)}
+--error("Colors were:" .. tostring(am_color))
+
+am.ascii_color_map = {
+ b: am_color.background
+ m: am_color.midground
+ f: am_color.foreground
+ s: am_color.shadow
+ h: am_color.highlight
+ x: am_color.black
+ o: am_color.outline
+}
+
+lake_color =
+ highlight: color.highlight
+ foreground: {113, 224, 214}
+ midground: {90, 172, 188}
+ background: {75, 120, 156}
+ shadow: {51, 78, 120}
+ black: color.black
+
+am_lake_color = {k, vec4(v[1]/255, v[2]/255, v[3]/255,1) for k,v in pairs(lake_color)}
+
+{
+ :color
+ :am_color
+ :lake_color
+ :am_lake_color
+}
diff --git a/src/conf.lua b/src/conf.lua
new file mode 100644
index 0000000..f99a4dd
--- /dev/null
+++ b/src/conf.lua
@@ -0,0 +1 @@
+shortname="ggj26"
diff --git a/src/controller_bridge.js b/src/controller_bridge.js
new file mode 100644
index 0000000..3e559f4
--- /dev/null
+++ b/src/controller_bridge.js
@@ -0,0 +1,38 @@
+
+window.addEventListener("gamepadconnected", function(e) {
+ console.log("Gamepad connected at index %d: %s. %d buttons, %d axes.",
+ e.gamepad.index,
+ e.gamepad.id,
+ e.gamepad.buttons.length,
+ e.gamepad.axes.length
+ );
+ var i;
+ for(i = 0; i < e.gamepad.buttons.length; i++){
+ CONT.last_state.buttons[i] = false;
+ }
+ for(i = 0; i < e.gamepad.axes.length; i++){
+ CONT.last_state.axes[i] = 0;
+ }
+ CONT.gp = navigator.getGamepads()[0];
+});
+
+window.CONT = {
+ messages: [],
+ gp: null,
+ last_state: {
+ on: false,
+ buttons: [],
+ axes: []
+ },
+ loop: function() {
+ var i;
+ if(CONT.gp == null) return;
+ for(i = 0; i < CONT.gp.buttons.length; i++){
+ CONT.last_state.buttons[i] = CONT.gp.buttons[i].pressed;
+ }
+ for(i = 0; i< CONT.gp.axes.length; i++){
+ CONT.last_state.axes[i] = CONT.gp.axes[i];
+ }
+ CONT.last_state.on = true;
+ }
+};
diff --git a/src/ecs.moon b/src/ecs.moon
new file mode 100644
index 0000000..61b73a5
--- /dev/null
+++ b/src/ecs.moon
@@ -0,0 +1,184 @@
+win = require("window")
+log = require("log")
+world = require("world")
+Component = require("ecs.component")
+GraphicsComponent = require("ecs.graphics")
+NetworkedComponent = require("ecs.networked")
+PredictedComponent = require("ecs.predicted")
+ScriptComponent = require("ecs.script")
+CharacterControllerComponent = require("ecs.char_controller")
+shader_world = require("shaders.world")
+
+--win.scene:append(shader_world)
+--pp = am.postprocess({
+ --clear_color: vec4(0.2, 0.2, 0.3, 1),
+ --depth_buffer: true,
+ ----stencil_buffer: true,
+ --width: win.width,
+ --height: win.height,
+--})
+pp = am.group!
+pp\append(shader_world.node)
+GraphicsComponent.node = pp
+ScriptComponent.node = pp
+PredictedComponent.node = pp
+NetworkedComponent.node = pp
+
+-- Base entity of our ECS
+class Entity
+ id: 1
+ new: (id, components) => -- [id], [components]
+ ent_reg = world.domain == "client" and world.level.entities or world.level_sync.ents
+ id_cur = id or #ent_reg + 1
+ @created_at = debug.traceback!
+ assert(id_cur < 10, "Got more then 10 entities")
+ while ent_reg[id_cur] != nil
+ id_cur += 1
+ log.debug("Want to create an entity with id" .. id_cur .. ", registry is:" .. tostring(ent_reg) .. "\n" .. debug.traceback(), {"ecs"})
+ assert(ent_reg[id_cur] == nil, "Attempted to create entity with the same id as one that already exists: " .. tostring(id_cur) .. ", entities were:" .. tostring(ent_reg))
+
+ log.info("Creating entity " .. id_cur .. " domain is: " .. world.domain, {"ecs"})
+ ent_reg[id_cur] = @
+ @reg = ent_reg
+ @id = id_cur
+ -- Bookkeeping for O(1) access for components
+ @c_by_type = {}
+ @c_by_name = {}
+ --Entity is responsible for the component -> entity link
+ @components = components or {}
+ for name, component in pairs(@components)
+ component.entity = @
+ @c_by_type[component.__class] = @c_by_type[component.__class] or {}
+ @c_by_type[component.__class][name] = component
+ for name, component in pairs(@components)
+ assert(component.join, "Component " .. name .. " does not have a join method.")
+ component\join(@)
+ component\post_join(@)
+ add: (component, cid) =>
+ assert(component, "Failed to find component:" .. tostring(component))
+ assert(type(component) == "table", "Component must be a table")
+ assert(component.__class, "Component must have a class")
+ component.entity = @
+ component\join(@, cid)
+ component\post_join(@, cid)
+ if cid == nil
+ cid = #@components + 1
+ while @components[cid]
+ cid += 1
+ assert(@components[cid] == nil, "Already had a component with id" .. tostring(cid))
+ @components[cid] = component
+ @c_by_type[component.__class] = @c_by_type[component.__class] or {}
+ @c_by_type[component.__class][cid] = component
+ assert(@c_by_name[component.name] == nil, "Duplicate components named" .. component.name)
+ @c_by_name[component.name] = component
+ @
+ remove: (cid) =>
+ component = @components[cid]
+ component.entity = nil
+ component\leave(@)
+ component
+ @components[cid] = nil
+ @c_by_type[component.__class][cid] = nil
+ @c_by_name[component.name] = nil
+ destroy: () =>
+ for name, component in pairs(@components)
+ @remove(name)
+ @reg[@id] = nil
+
+ get: (cid) =>
+ @components[cid]
+
+ serialize: () =>
+ components = {}
+ for i, component in pairs(@components)
+ component_name = component.__class.__name
+ if not component.serialize
+ log.warn("Component:" .. component_name .. " does not have a .serialize()", {"ecs"})
+ continue
+ component_data = component\serialize!
+ components[i] = string.format(
+ "%d\0%s%d\0%s",
+ #component_name, component_name,
+ #component_data, component_data
+ )
+ return table.concat(components, "\0")
+
+ --@deserialize: (data) =>
+ --error("Deserialize called")
+ --log.info("Deserializing entity from data of length " .. tostring(data and #data or 0), {"ecs"})
+ --ent = @!
+ ---- Empty payload -> empty entity
+ --if not data or #data == 0
+ --return ent
+ ---- Map serialized component type names back to their classes
+ --component_types = {
+ --[GraphicsComponent.__name]: GraphicsComponent
+ --[NetworkedComponent.__name]: NetworkedComponent
+ --[PredictedComponent.__name]: PredictedComponent
+ --[ScriptComponent.__name]: ScriptComponent
+ --[CharacterControllerComponent.__name]: CharacterControllerComponent
+ --[PhysicsComponent.__name]: PhysicsComponent
+ --}
+ --pointer = 1
+ --len = #data
+ --while pointer <= len
+ ---- Read component name length (digits up to first NUL)
+ --name_len_end = string.find(data, "\0", pointer)
+ --if not name_len_end
+ --break
+ --name_len_str = string.sub(data, pointer, name_len_end - 1)
+ --name_len = tonumber(name_len_str)
+ --if not name_len or name_len <= 0
+ --break
+ --pointer = name_len_end + 1
+ ---- Read component type name
+ --component_name = string.sub(data, pointer, pointer + name_len - 1)
+ --pointer += name_len
+ ---- Read component data length (digits up to next NUL)
+ --data_len_end = string.find(data, "\0", pointer)
+ --if not data_len_end
+ --break
+ --data_len_str = string.sub(data, pointer, data_len_end - 1)
+ --data_len = tonumber(data_len_str)
+ --if not data_len or data_len < 0
+ --break
+ --pointer = data_len_end + 1
+ ---- Read component payload
+ --component_data = string.sub(data, pointer, pointer + data_len - 1)
+ --pointer += data_len
+ ---- Skip inter-component separator if present
+ --if string.sub(data, pointer, pointer) == "\0"
+ --pointer += 1
+ --ctype = component_types[component_name]
+ --if not ctype
+ --log.warn("Unknown component type during deserialize: " .. tostring(component_name), {"ecs"})
+ --continue
+ --if not ctype.deserialize
+ --log.warn("Component type " .. tostring(component_name) .. " does not implement .deserialize()", {"ecs"})
+ --continue
+ ---- Let the component class decide how to interpret its payload
+ --component = ctype.deserialize(component_data)
+ --if component
+ --ent\add(component)
+ --return ent
+
+ __tostring: () =>
+ components_str = tostring(@components)
+ return string.format("%s : {\n%s\n}\t%q",@@__name, components_str\gsub("^","\t"), @created_at)
+
+class PhysicsComponent extends Component
+ new: (name, properties) =>
+ assert(properties and properties.shape, "Failed to find a shape for physics component")
+ super(name, properties)
+
+{
+ :Component
+ :Entity
+ :NetworkedComponent
+ :PredictedComponent
+ :GraphicsComponent
+ :PhysicsComponent
+ :ScriptComponent
+ :CharacterControllerComponent
+ node: pp
+}
diff --git a/src/ecs/char_controller.moon b/src/ecs/char_controller.moon
new file mode 100644
index 0000000..e5e3db7
--- /dev/null
+++ b/src/ecs/char_controller.moon
@@ -0,0 +1,70 @@
+ScriptComponent = require("ecs.script")
+win = require("window")
+log = require("log")
+world = require("world")
+
+class CharacterControllerComponent extends ScriptComponent
+ new: (name, properties, netc_name) =>
+ log.info("Creating new character controller", {"ecs"})
+ properties.script = () ->
+ --log.info("Character controller running", {"ecs"})
+ any_change = false
+ assert(@net.__class.__name == "ClientNetworkedComponent", "Wrong net component, was a " .. @net.__class.__name)
+ assert(@pred,"No predicted component")
+ assert(world.network, "Network must be created before CharacterControllerComponent starts running")
+ if win\key_pressed("w")
+ log.info("Key down w",{"net","ecs","client","player"})
+ @net.properties.acc[2] = 1
+ any_change = true
+ elseif win\key_released("w")
+ log.info("Key up w",{"net","ecs","client","player"})
+ any_change = true
+ @net.properties.acc[2] = 0
+ if win\key_pressed("s")
+ log.info("Key down s",{"net","ecs","client","player"})
+ any_change = true
+ @net.properties.acc[2] = -1
+ elseif win\key_released("s")
+ log.info("Key up s",{"net","ecs","client","player"})
+ any_change = true
+ @net.properties.acc[2] = 0
+ if win\key_pressed("a")
+ log.info("Key down a", {"net","ecs","client","player"})
+ any_change = true
+ @net.properties.acc[1] = -1
+ elseif win\key_released("a")
+ log.info("Key up a", {"net","ecs","client","player"})
+ any_change = true
+ @net.properties.acc[1] = 0
+ if win\key_pressed("d")
+ log.info("Key down d", {"net","ecs","client","player"})
+ any_change = true
+ @net.properties.acc[1] = 1
+ elseif win\key_released("d")
+ log.info("Key up d", {"net","ecs","client","player"})
+ any_change = true
+ @net.properties.acc[1] = 0
+
+ -- We can't actually update the network on the client,
+ -- but we still want to do predicted movement
+ if any_change
+ @net.properties.last_update = am.eval_js("Date.now();")
+ @net.properties.pos = @pred.properties.pos
+ --@net.properties.vel = @pred.properties.vel
+ world.network\send("SuggestPlayerUpdate",{
+ acc: @net.properties.acc
+ vel: @net.properties.vel
+ pos: @pred.properties.pos
+ })
+ return false
+ --print("Running character controller",win\key_pressed("w"))
+ @netc_name = netc_name
+ super(name, properties)
+ join: (entity) =>
+ log.debug("Components was:" .. tostring(entity.components), {"ecs"})
+ @net = entity\get(@netc_name)
+ @pred = entity\get("pred")
+ assert(@net, "Must have added network component: " .. @netc_name .. ".")
+ super(entity)
+
+CharacterControllerComponent
diff --git a/src/ecs/client_networked.moon b/src/ecs/client_networked.moon
new file mode 100644
index 0000000..fc70dc2
--- /dev/null
+++ b/src/ecs/client_networked.moon
@@ -0,0 +1,19 @@
+world = require("world")
+log = require("log")
+NetworkedComponent = require("ecs.networked")
+Component = require("ecs.component")
+
+class ClientNetworkedComponent extends Component
+ new: (name, properties) =>
+ log.info("Creating client networked info", {"client"})
+ super(name, properties)
+ listen_events: () ->
+ assert(wold.network, "world.network needs to be set before hooking events")
+ world.network\listen("create","client_network_component",(hubid, data) ->
+ error("ClientNetworkComponent create received:".. tostring(data))
+ )
+ world.network\listen("update","client_network_component",(hubid, data) ->
+ error("ClientNetworkComponent update received:".. tostring(data))
+ )
+
+ClientNetworkedComponent
diff --git a/src/ecs/component.moon b/src/ecs/component.moon
new file mode 100644
index 0000000..6ce16bd
--- /dev/null
+++ b/src/ecs/component.moon
@@ -0,0 +1,16 @@
+-- Base component class of the ECS
+class Component
+ depends: {}
+ new: (name, properties) =>
+ @name = name
+ @properties = properties or {}
+ join: (e, cid) =>
+ @
+ post_join: (e, cid) =>
+ @
+ leave: (e) =>
+ @
+ __tostring: () =>
+ return string.format("%s<%s> {\n%s\n}",@@__name,@name or "no name given",tostring(@properties))
+
+Component
diff --git a/src/ecs/graphics.moon b/src/ecs/graphics.moon
new file mode 100644
index 0000000..66e5e80
--- /dev/null
+++ b/src/ecs/graphics.moon
@@ -0,0 +1,67 @@
+Component = require("ecs.component")
+sprites = require("sprites")
+gworld = require("shaders.world")
+world = require("world")
+log = require("log")
+
+sd = sprites.floor
+w1 = sprites.wall1_diffuse
+
+panel = {
+ vec3(0, 1, 0.4),
+ vec3(1, 1, 0.4),
+ vec3(1, 0, 0.4),
+ vec3(1, 0, 0.4),
+ vec3(0, 0, 0.4),
+ vec3(0, 1, 0.4)
+}
+
+class GraphicsComponent extends Component
+ new: (name, properties) =>
+ --assert(properties and properties.node , "Failed to find node for graphics component")
+ --assert(@@node, ".node not set for GraphicsComponent")
+ assert(world.geom_view,".geom_view not set for world")
+ assert(world.uv_view, ".uv_view not set for world")
+ --@node = properties.node
+ super(name, properties)
+ static: () =>
+ @@static
+ join: () =>
+ --log.info("Joining with graphics component" .. tostring(@node), {"graphics"})
+ --@@node\append(@node)
+ gworld.add(@)
+ leave: () =>
+ gworld.remove(@)
+ --@@node\remove(@node)
+ node: () =>
+ error("Tried to access graphic component's .node")
+ --@properties.node
+ tris: () =>
+ 2
+ populate_buf: (geom_view, uv_view, offset) =>
+ assert(@properties.graphic, "Graphics component needs a graphic")
+ assert(offset == 1, "Offset was " .. tostring(offset))
+ log.info("Populating:" .. tostring(offset) .. ":" .. tostring(@properties.graphic), {"graphic","ecs"})
+ log.info(debug.traceback(), {"graphic","ecs"})
+ log.info("Populating with:" .. tostring(panel),{"graphic","ecs"})
+ geom_view\set(panel, offset, @tris! * 3)
+ @geom_view = geom_view
+ @uv_view = uv_view
+ @offset = offset
+ uv = @properties.graphic
+ uv_view[1] = vec4(uv.s1,uv.t1,1,1)
+ uv_view[2] = vec4(uv.s1,uv.t2,1,1)
+ uv_view[3] = vec4(uv.s2,uv.t2,1,1)
+ uv_view[4] = vec4(uv.s2,uv.t2,1,1)
+ uv_view[5] = vec4(uv.s2,uv.t1,1,1)
+ uv_view[6] = vec4(uv.s1,uv.t1,1,1)
+ --error("Graphics components must override .populate_buf()")
+ move: (move_off) =>
+ for i, set in ipairs(panel)
+ @geom_view[@offset + i - 1] = @geom_view[@offset + i - 1] + move_off
+ moveto: (loc) =>
+ assert(@offset, "Moveto called before populate_buf")
+ for i, set in ipairs(panel)
+ @geom_view[@offset + i - 1] = loc + set
+
+GraphicsComponent
diff --git a/src/ecs/networked.moon b/src/ecs/networked.moon
new file mode 100644
index 0000000..6673a7d
--- /dev/null
+++ b/src/ecs/networked.moon
@@ -0,0 +1,71 @@
+Component = require("ecs.component")
+world = require("world")
+log = require("log")
+
+network_log = {}
+
+class NetworkedComponent extends Component
+ @q: {}
+
+ new: (name, properties) =>
+ assert(properties.id == nil, "networked component's id needs to be nil")
+ assert(properties.name == nil, "network component's name needs to be nil")
+ properties.id = world.level_sync.entid
+ world.level_sync.entid += 1
+ nwc = @
+ update = (key, value) =>
+ if not world.hub
+ error("Tried to update network on the client")
+ return
+ if not properties[key]
+ error("Trying to set key not defined in properties:" .. key)
+ properties[key] = value
+ properties.last_update = am.eval_js("Date.now();")
+ nwc.__class.q[nwc] = true
+ -- Broadcast update to other peers through client
+ --world.hub\broadcast("update", properties)
+ --actually, we only want to update each network component *at most* once per frame.
+
+ proxy = {}
+ setmetatable(proxy, {
+ __index: properties,
+ __newindex: update
+ })
+ super(name, proxy)
+ @net_properties = properties
+ join: (e, cid) =>
+ @@node\action("Send Updates", ()->
+ @@update_dirty!
+ )
+ @net_properties.id = e.id
+ @net_properties.name = cid
+ log.info("Added networked componenet name " .. @name .. " to entity id " .. e.id, {"net","server","ecs"})
+ -- Send initial create message through client
+ --if world.hub -- Actually, entity creation is hard, just sync each entity individually.
+ --world.hub\broadcast("create", properties)
+ super(e)
+
+ pack: () =>
+ assert(@entity, "Tried to pack on a NetworkedComponent without an Entity")
+ log.info("Packing data from proxy:" .. tostring(@proxy), {"net","ecs"})
+ return am.to_json({
+ id: @entity.id
+ data: @net_properties
+ time: am.current_time!
+ })
+
+ unpack: () ->
+ assert(@entity, "Tried to unpack on a NEtworkedComponent without an Entity")
+
+ @update_dirty: () =>
+ for component, _ in pairs(@q)
+ world.hub\broadcast("update", component.net_properties)
+ @q = {}
+
+ -- Each different kind of entity needs to have it's own create and listen network
+ -- hooks, it's way too complicated and heavy to push every entity with every componenet
+ -- over the network.
+
+assert(NetworkedComponent.q, "No queue found!")
+
+NetworkedComponent
diff --git a/src/ecs/player_graphic.moon b/src/ecs/player_graphic.moon
new file mode 100644
index 0000000..5ad9cab
--- /dev/null
+++ b/src/ecs/player_graphic.moon
@@ -0,0 +1,66 @@
+GraphicsComponent = require("ecs.graphics")
+sprites = require("sprites")
+
+geom_fac = {
+ {-1, -1, 1},
+ {-1, 1, 1},
+ {-1, -1, 1},
+ {1, 1, 1},
+ {1, -1, 1},
+ {-1, -1, 1}
+}
+
+class PlayerGraphicComponent extends GraphicsComponent
+ @player_size = 0.25
+ @static = false
+ --new: (name, properties) =>
+ --print("New PlayerGraphicsComponenet, super is", @@__init)
+ --print("But graphicsComponent's __init is:", GraphicsComponent.__init)
+ --GraphicsComponent.__init(@, name, properties)
+ --super(name, properties)
+ tris: () ->
+ 2
+ populate_buf: (geom_view, uv_view, offset) =>
+ @playerbuf = geom_view
+ h = @@player_size / 2
+ for i, set in ipairs(geom_fac)
+ geom_view[i] = vec3(h*set[1], h*set[2], h*set[3])
+ uv = sprites.player_normal
+ uv_view[1] = vec2(uv.s1,uv.t1)
+ uv_view[2] = vec2(uv.s1,uv.t2)
+ uv_view[3] = vec2(uv.s2,uv.t2)
+ uv_view[4] = vec2(uv.s2,uv.t2)
+ uv_view[5] = vec2(uv.s2,uv.t1)
+ uv_view[6] = vec2(uv.s1,uv.t1)
+ join: (entity) =>
+ super(entity)
+ aspect = win.width / win.height
+ -- inject nodes into the scene graph
+ program = @.node("use_program")
+ pred_component = entity\get("pred")
+ graphic = entity\get("graphic")
+ @node\remove(program)
+ @node\append(am.blend("alpha")\append(am.depth_test("less", true)\append(program)))
+ @node\action(() =>
+ pred_loc = pred_component.properties.pos
+ graphic\move(pred_loc.x, pred_loc.y)
+ )
+ --@.node("bind").highlight = color.am_color.black
+ move: (x,y) =>
+ assert(x, "x required")
+ assert(y, "y required")
+ world.level.move_lamp(@lamp, x + @lamp_offset.x, y + @lamp_offset.y)
+ h = @@player_size / 2
+ z = 0.5
+ @playerbuf[1] = vec3(x-h,y-h,z)
+ @playerbuf[2] = vec3(x-h,y+h,z)
+ @playerbuf[3] = vec3(x+h,y+h,z)
+ @playerbuf[4] = vec3(x+h,y+h,z)
+ @playerbuf[5] = vec3(x+h,y-h,z)
+ @playerbuf[6] = vec3(x-h,y-h,z)
+ --print("Move called", @playerbuf[1])
+ face: (direction) =>
+ --print("direction",direction)
+ @.node("bind").rot = (direction ) % (math.pi * 2)
+
+PlayerGraphicComponent
diff --git a/src/ecs/predicted.moon b/src/ecs/predicted.moon
new file mode 100644
index 0000000..b8bdd28
--- /dev/null
+++ b/src/ecs/predicted.moon
@@ -0,0 +1,25 @@
+Component = require("ecs.component")
+
+class PredictedComponent extends Component
+ new: (name, properties, netc_name, calculate) =>
+ super(name, properties)
+ @netc_name = netc_name
+ assert(calculate and type(calculate) == "table", "Calculate must be a table, was " .. type(calculate))
+ @calculate = calculate
+ join: (entity) =>
+ @net = @entity\get(@netc_name)
+ @node = am.group!
+ @node\action(() ->
+ @forward!
+ )
+ @@node\append(@node)
+ super(entity)
+ leave: (entity) =>
+ @@node\remove(@node)
+ forward: () =>
+ for property, calculation in pairs(@calculate)
+ @properties[property] = calculation(@)
+
+
+
+PredictedComponent
diff --git a/src/ecs/script.moon b/src/ecs/script.moon
new file mode 100644
index 0000000..05869c0
--- /dev/null
+++ b/src/ecs/script.moon
@@ -0,0 +1,16 @@
+Component = require("ecs.component")
+
+class ScriptComponent extends Component
+ new: (name, properties) =>
+ print("Creating new script component")
+ assert(properties and properties.script, "Failed to find script name for script component")
+ super(name, properties)
+ join: (e) =>
+ print("Script component is joining an entity")
+ @node = am.group!
+ @@node\append(@node)
+ @node\action(@properties.script)
+
+ leave: (e) =>
+ print("Script component is leaving an entity")
+ @@node\remove(@node)
diff --git a/src/hub.moon b/src/hub.moon
new file mode 100644
index 0000000..48fcec3
--- /dev/null
+++ b/src/hub.moon
@@ -0,0 +1,184 @@
+-- Hub-and-spoke networking hub
+-- Manages client connections and routes messages between them
+
+net = require "net"
+log = require "log"
+world = require "world"
+
+-- Register message types that clients sends to the hub
+-- Client -> Hub: RequestEntities has no payload (empty table is fine)
+net.register_message("RequestEntities", {})
+
+-- Envelope used for all logical messages routed through the hub
+net.register_message("routed_message", {
+ required: {
+ message_type: "string"
+ from: "string"
+ }
+ optional: {
+ data: "table"
+ }
+})
+
+net.register_message("Join", {
+ required: {
+ name: "string"
+ }
+})
+
+-- Hub -> Client: RespondEntities sends the current entities list (array)
+net.register_message("RespondEntities", {
+ optional: {
+ entities: "table"
+ }
+})
+
+-- Hub -> Client: CreateEntity announces a new entity for a given peerid
+net.register_message("CreateEntity", {
+ required: {
+ peerid: "string"
+ }
+})
+
+class Hub
+ new: =>
+ @peer = nil
+ @clients = {} -- client_id -> connection
+ @client_names = {} -- client_id -> name (optional)
+ @initialized = false
+ @routes = {}
+ @on_connect_callbacks = {}
+ @on_disconnect_callbacks = {}
+
+ initialize: =>
+ if @initialized
+ return
+ --@attempts = 0
+ --create_peer = () ->
+ --@peer = net.Peer!
+ --fail_peer_creation = () ->
+ --@attempts += 1
+ --while @attempts < 4
+ --xpcall(create_peer, fail_peer_creation)
+ --log.info("Creating peer attempt " .. @attempts .. " / 4", {"net","server"})
+ --if @peer == nil
+ --error("Failed to create peer 4 times...")
+ @peer = net.Peer!
+ log.info("Hub peer created: #{@peer.id}", {"server", "net"})
+ -- Listen for incoming connections
+ @peer\on "connection", (peer, message) ->
+ client_id = message.data.dest -- the other side of the connection?
+ log.info("Client connected: " .. tostring(peer) .. ":" .. tostring(message), {"server", "net"})
+ conn = message.data
+ @clients[client_id] = conn
+ -- Set up connection handlers
+ -- When the connection receives data, the callback is invoked with
+ -- (connection, message_array), where message_array = { msgname, msgdata }.
+ -- We only care about the logical client id and the logical message array,
+ -- so forward just those to handle_message. This keeps the JS bridge
+ -- signature Hub.handle_message(from_client, msg) where msg is the
+ -- array { msg_type, msg_data }.
+ conn\on "data", (conn, message) ->
+ log.info("On data got arguments:" .. tostring(conn) .. "," .. tostring(message), {"server","net"})
+ @handle_message(client_id, message)
+
+ conn\on "close", () ->
+ @handle_disconnect(client_id)
+
+ -- Notify connection callbacks
+ for callback in *@on_connect_callbacks
+ callback(client_id)
+
+ @initialized = true
+ require("server.init")
+
+ handle_message: (from_client, message) =>
+ -- message is the array {msg_type, msg_data} that was sent over the wire
+ log.info("Hub received message: #{message} from #{from_client}", {"net", "server"})
+ msg_type = message[1]
+ msg_data = message[2]
+
+ -- Build routed_message-style envelope for validation
+ routed = {
+ message_type: msg_type
+ from: from_client
+ data: msg_data
+ }
+
+ -- Validate envelope
+ ok, err = pcall(net.validate, "routed_message", routed)
+ if not ok
+ log.error("Invalid routed_message from " .. tostring(from_client) .. ": " .. tostring(routed), {"net", "server"})
+ return
+
+ -- Validate payload for the specific message type
+ ok, err = pcall(net.validate, msg_type, msg_data)
+ if not ok
+ log.error("Invalid " .. tostring(msg_type) .. " payload from " .. tostring(from_client) .. ": " .. tostring(err), {"net", "server"})
+ return
+
+ if msg_type == "Join"
+ log.info("Hub handling Join from " .. tostring(from_client) .. " with data=" .. tostring(msg_data), {"net", "server", "debug"})
+ world.domain = "server"
+ if @routes[msg_type]
+ -- routes[msg_type] is a flat map id -> callback(from_client, data)
+ for _, callback in pairs(@routes[msg_type])
+ callback(from_client, msg_data)
+ else
+ log.warn("No routes for " .. tostring(msg_type), {"net","server"})
+ world.domain = "client"
+
+ listen: (message_type, id, callback) =>
+ @routes[message_type] = @routes[message_type] or {}
+ id = id or "default"
+ @routes[message_type][id] = callback
+ log.info("Hub listening for #{message_type}", {"net", "server"})
+ id
+
+ defen: (message_type, id) =>
+ @routes[message_type][id] = nil
+
+ send: (client_id, message_type, data) =>
+ conn = @clients[client_id]
+ if not conn
+ log.warn("Cannot send to unknown client: #{client_id}", {"server", "net"})
+ return
+ log.info("Hub sending #{message_type} to #{client_id}", {"net", "server"})
+ conn\send({message_type, data})
+
+ broadcast: (message_type, data) =>
+ nclients = 0
+ for _, _ in pairs(@clients)
+ nclients += 1
+ log.info("Hub broadcasting #{message_type} to " .. tostring(nclients) .. " clients", {"net", "server"})
+ for clientid, conn in pairs(@clients)
+ conn\send({message_type, data})
+
+ handle_disconnect: (client_id) =>
+ log.info("Client disconnected: #{client_id}", {"server", "net"})
+ @clients[client_id] = nil
+ @client_names[client_id] = nil
+
+ -- Notify disconnect callbacks
+ for callback in *@on_disconnect_callbacks
+ callback(client_id)
+
+ on_connect: (callback) =>
+ table.insert(@on_connect_callbacks, callback)
+
+ on_disconnect: (callback) =>
+ table.insert(@on_disconnect_callbacks, callback)
+
+ get_clients: =>
+ clients = {}
+ for id, conn in pairs @clients
+ table.insert(clients, {
+ id: id
+ name: @client_names[id]
+ })
+ clients
+
+ pump: =>
+ net.pump!
+
+{:Hub}
diff --git a/src/js_bridge.js b/src/js_bridge.js
new file mode 100644
index 0000000..f6a158b
--- /dev/null
+++ b/src/js_bridge.js
@@ -0,0 +1,134 @@
+var s = document.createElement('script');
+s.setAttribute('src','https://unpkg.com/peerjs@1.5.5/dist/peerjs.min.js');
+document.body.appendChild(s);
+function genRanHex(size) {
+ return Array.apply(null,Array(size)).map(function(){
+ return Math.floor(Math.random() * 16).toString(16);
+ }).join('');
+
+}
+window.PEER = {
+ event_queue: [],
+ peers: {},
+ peer_message_queue: [],
+ connection_message_queue: [],
+ creation_queue: [],
+ connections: {},
+ to_connect: [], // Sometimes we have to wait a tick before the connection is ready.
+ create: function(_) {
+ var peer = new Peer({
+ //"host": "cogarr.net",
+ //"path": "/stun/",
+ //"secure": true,
+ //"debug": 3
+ });
+ peer.on("open", function(){
+ console.log("[JS] Open called on peer");
+ PEER.peers[peer.id] = peer;
+ PEER.creation_queue.push(peer.id);
+ });
+ peer.on("error", function(msg){
+ //console.log("[JS ERROR] " + msg);
+ PEER.connection_message_queue.push({"message":msg, "data": {
+ "call": "create",
+ "e": "error",
+ "message": msg
+ }});
+ });
+ },
+ delete_peer: function(tbl) {
+ var name = tbl.name;
+ //console.log("[JS] Deleting peer " + name);
+ delete PEER.peers[name];
+ },
+ on: function(tbl) {
+ var name = tbl.name;
+ var e = tbl.e;
+ var message = tbl.message;
+ //console.log("[JS] Setting hook for " + name + "," + e + "," + message);
+ PEER.peers[name].on(e, function(data) {
+ //console.log("[JS] Peer " + name + " received " + e);
+ if(e == "connection"){
+ // Store the server-side DataConnection under directional key [server, client]
+ PEER.connections[[name,data.peer]] = data;
+ //console.log("[JS] Peer.connections is now");
+ //console.log(PEER.connections);
+ // Rewrite for Lua so net.rewrite_events can build a Hub-side Connection
+ data = [name,data.peer]; // [server, client]
+ }else{
+ //console.log("[JS] Not connection:" + e + ":" + data);
+ }
+ PEER.peer_message_queue.push({"message":message, "data":{
+ "call": "on",
+ "peer": name,
+ "e": e,
+ "data": data
+ }});
+ //console.log("[JS] Message queue is now");
+ //console.log(PEER.peer_message_queue);
+ });
+ },
+ connect: function(tbl) {
+ var source = tbl.source;
+ var dest = tbl.dest;
+ //console.log("[JS] connecting " + source + " to " + dest);
+ var conn = PEER.peers[source].connect(dest);
+ // Store client-side DataConnection under directional key [client, hub]
+ PEER.connections[[source,dest]] = conn;
+ // Send a hello to always establish a connection
+ //console.log("[JS] sending hello");// doesn't seem to show up in the output, but its needed so we don't drop the first message.
+ conn.send("Hello");
+ //console.log("[JS] Connect called, PEER.connections is");
+ //console.log(PEER.connections);
+ return [source,dest];
+ },
+ disconnect: function(tbl) {
+ PEER.peers[tbl.name].disconnect();
+ },
+ reconnect: function(tbl){
+ PEER.peers[tbl.name].reconnect();
+ },
+ destroy: function(tbl){
+ PEER.peers[tbl.name].destroy();
+ },
+ send: function(tbl){
+ var source = tbl.source;
+ var dest = tbl.dest;
+ var data = tbl.data;
+ var key = [source,dest];
+ //console.log("[JS] sending " + data + " over " + key);
+ //console.log(PEER.connections[key]);
+ //console.log(data);
+ PEER.connections[key].send(data);
+ },
+ close: function(tbl){
+ var name = tbl.name;
+ var id = tbl.id;
+ PEER.connections[[name,id]].close();
+ },
+ conn_on: function(tbl){
+ var source = tbl.source;
+ var dest = tbl.dest;
+ var e = tbl.e;
+ var message = tbl.message;
+ //console.log("[JS] Setting hook for [" + source + "," + dest + "] " + e + "," + message);
+ //console.log(PEER.connections[[source,dest]]);
+ //console.log(PEER.connections);
+ PEER.connections[[source,dest]].on(e, function(c) {
+ //console.log("[JS] connection between " + dest + " and " + source + " received " + e);
+ PEER.connection_message_queue.push({"message":message, "data":{
+ "call": "on",
+ "peer": source,
+ "dest": dest,
+ "e": e,
+ "data": c
+ }});
+ });
+ },
+ conn_field: function(tbl){
+ var name = tbl.name;
+ var id = tbl.id;
+ var field = tbl.field;
+ return PEER.connections[[name,id]][field];
+ }
+};
diff --git a/src/levels/entmaker.moon b/src/levels/entmaker.moon
new file mode 100644
index 0000000..da92651
--- /dev/null
+++ b/src/levels/entmaker.moon
@@ -0,0 +1,21 @@
+-- Creates entities from commited messages
+world = require("world")
+log = require("log")
+
+maker = {}
+
+maker.start_peer = () ->
+ -- All modes now use client interface (including host)
+ if world.network
+ -- Receive suggestions from hub
+ world.network\register_router("suggest", (from_id, data) ->
+ -- Handle suggestion
+ )
+
+maker.start_elected = () ->
+ if world.network_mode == "host"
+ -- Hub can handle incoming suggestions from clients
+ -- (Handled inline when clients send messages)
+ log.info("Entity maker started in host mode", {"net"})
+
+maker
diff --git a/src/levels/game.moon b/src/levels/game.moon
new file mode 100644
index 0000000..1e4899c
--- /dev/null
+++ b/src/levels/game.moon
@@ -0,0 +1,7 @@
+
+x = {}
+
+x.create = () ->
+ error("Creating level game")
+
+x
diff --git a/src/levels/lobby.moon b/src/levels/lobby.moon
new file mode 100644
index 0000000..7f2a27b
--- /dev/null
+++ b/src/levels/lobby.moon
@@ -0,0 +1,111 @@
+ecs = require("ecs")
+world = require("world")
+shader = require("shaders.world")
+sprites = require("sprites")
+task = require("task")
+LobbyGraphic = require("prefab.lobby")
+GraphicsComponent = require("ecs.graphics")
+ClientNetworkedComponent = require("ecs.client_networked")
+NetworkedComponent = require("ecs.networked")
+log = require("log")
+
+level = {}
+
+lobby = nil
+
+level.create = (code) ->
+ log.info("Creating level:" .. code,{"level"})
+ -- We can set this even on the client, it just won't get used.
+ world.level_sync.ref = {
+ id: "Level from lobby.moon"
+ get_spawn_location: () ->
+ {0,0,0}
+ }
+ log.info("world.domain was" .. tostring(world.domain),{"level"})
+ if world.domain == "client"
+ lobby = ecs.Entity!
+ lobby_graphic = LobbyGraphic("graphic",{})
+ lobby\add(lobby_graphic,"grapic")
+ lobby\add(ClientNetworkedComponent("net",{
+ type: "level",
+ level_name: "levels.lobby"
+ level_data: {code}
+ }),"net")
+
+ code = world.level_sync.data[1]
+ path = am.eval_js("window.CLIPBOARD.get_path()")
+ url = string.format("%s?i=%s",path,code)
+ --todo: set qr code
+ am.eval_js([[
+var s = document.createElement('div');
+s.setAttribute("id","qrcode");
+s.setAttribute("style","z-index: 1; position:absolute; visibility:hidden;");
+var p = document.getElementById("container");
+p.prepend(s);
+console.log("[JS] Added qrcode", s);
+new QRCode(s, "]] .. url .. [[");
+]])
+ co = coroutine.create(() ->
+ print("Start of coroutine.")
+ imgsrc = ""
+ while imgsrc == ""
+ imgsrc = am.eval_js([[document.getElementById("qrcode").children[1].src;]])
+ print("Waiting on imgsrc...")
+ coroutine.yield!
+ print("Got imgsrc.")
+ b64 = imgsrc\match("^data:image/png;base64,(.+)")
+ print("Found base64 png of qrcode.")
+ qrbuffer = am.base64_decode(b64)
+ print("Got qrbuffer")
+ qrimg = am.decode_png(qrbuffer)
+ print("Created img")
+ qrtex = am.texture2d(qrimg)
+ print("Created texture2d")
+ sprite = {
+ texture: qrtex
+ s1: 0
+ t1: 0
+ s2: 1
+ t2: 1
+ x1: 0
+ y1: 0
+ x2: qrimg.width
+ y2: qrimg.height
+ wdith: qrimg.width
+ height: qrimg.height
+ }
+ qrcode = ecs.Entity!
+ qrcode_texture = {
+ vec4(0,1,1,1),
+ vec4(1,1,1,1),
+ vec4(1,0,1,1),
+ vec4(1,0,1,1),
+ vec4(0,0,1,1),
+ vec4(0,1,1,1)
+ }
+ code_graphic = GraphicsComponent("graphic",{
+ graphic: sprite
+ })
+ qrcode\add(code_graphic)
+ code_graphic\moveto(vec3(-0.5,-0.25,0))
+ )
+ log.info("Creating qrcode coroutine for lobby",{"ui"})
+ require("task").add(co)
+ elseif world.domain == "server"
+ lobby = ecs.Entity!
+ lobby\add(NetworkedComponent("net",{
+ type: "level",
+ level_name: "levels.lobby"
+ level_data: {world.hub.peer.id}
+ }), "net")
+
+ qrcode = ecs.Entity!
+ else
+ error("Unknown domain:" .. world.domain)
+
+level.destroy = () ->
+ if not lobby
+ error("Tried to destory lobby before it was built")
+ lobby\destroy!
+
+level
diff --git a/src/log.moon b/src/log.moon
new file mode 100644
index 0000000..cadfaec
--- /dev/null
+++ b/src/log.moon
@@ -0,0 +1,37 @@
+-- Singleton object to deal with log messages
+class log
+ log: (message, tags, level) ->
+ tags = tags or {}
+ if not level
+ error("Level is required")
+ tag_rev = {tag,true for tag in *tags}
+ chunk =
+ level: level
+ time: os.clock!
+ message: message
+ tags: tag_rev
+ for observer in *log.observers
+ observer(chunk)
+
+ reset: ->
+ log.observers = {}
+ debug: (message, tags) ->
+ --log.log(message, tags, "debug")
+ info: (message, tags) ->
+ log.log(message, tags, "info")
+ warn: (message, tags) ->
+ log.log(message, tags, "warn")
+ error: (message, tags) ->
+ log.log(message, tags, "error")
+ -- We can't call error() here, see preload.lua
+ panic: (message, tags) ->
+ log.log(message, tags, "panic")
+ -- Can't call error() here either.
+ listen: (callback) ->
+ table.insert(log.observers, callback)
+ #log.observers
+ defen: (n) ->
+ table.remove(log.observers, n)
+
+log.reset()
+log
diff --git a/src/main.lua b/src/main.lua
new file mode 100644
index 0000000..aebd83f
--- /dev/null
+++ b/src/main.lua
@@ -0,0 +1,114 @@
+require("preload")
+local color = require("color")
+local win = require("window")
+
+local pp = am.postprocess({
+ clear_color = color.am_color.background,
+ depth_buffer = true,
+ --stencil_buffer = true,
+ width = win.width,
+ height = win.height,
+})
+local world = require("world")
+world.node = pp
+world.node:action(function()
+ if win:key_down("l") then
+ local log = require("log")
+ log.info("My network peerid is:" .. tostring(world.network.peer.id), {"net"})
+ if world.hub then
+ log.info("I'm the host, host peerid is:" .. tostring(world.hub.peer.id), {"net"})
+ end
+ log.info("Entities are:" .. tostring(world.level.entities), {"ecs"})
+ end
+end)
+--local stars = require("shaders.stars")
+--pp:append(stars)
+local shader_world = require("shaders.world")
+--win.scene:append(shader_world.node)
+win.scene:append(shader_world.node)
+
+local ecs = require("ecs")
+pp:append(ecs.node)
+
+win.scene:append(pp)
+
+local ui = require("ui")
+local depth = am.depth_test("always")
+depth:append(ui.node)
+win.scene:append(depth)
+
+-- Initialize world.network, world.network_mode, and world.hub
+-- These will be set by the menu when user chooses Host or Join
+world.network = nil
+world.network_mode = nil
+world.hub = nil
+
+-- Network pump node - pumps net, world.hub, and world.network (client)
+local net = require("net")
+local net_node = am.group()
+net_node:action(function()
+ net.pump()
+ if world.hub then
+ -- If we're hosting, pump the hub
+ world.hub:pump()
+ end
+ if world.network then
+ -- Pump the client (both host and regular clients have one)
+ world.network:pump()
+ end
+end)
+win.scene:append(net_node) -- Pumps the net state machine
+
+task = require("task")
+win.scene:append(task.node) -- Pumps async tasks
+
+--input_menu = require("menu.input")
+--input_menu.initialize()
+--require("worldgen")
+--require("world_test")
+--require("net_test")
+--require("ui_test")
+--require("router_test")
+--require("controller_test")
+game = require("menu.game")
+pp:append(game.node)
+tutorial = require("menu.tutorial")
+win.scene:append(tutorial.node)
+require("menu.main").initialize()
+require("log").listen(function(chunk)
+ --if chunk.tags.ui then
+ --return
+ --end
+ if not chunk.tags.net then
+ return
+ end
+ --if not chunk.tags.ecs then
+ --return
+ --end
+ --if not chunk.tags.graphic then
+ --return
+ --end
+ --if not (chunk.tags.net and chunk.tags.ecs and chunk.tags.client) then
+ --return
+ --end
+ --if not chunk.message:find("Want to create") then
+ --return
+ --end
+ data = {"[",chunk.level:upper(),"]"}
+ if chunk.tags.server then
+ table.insert(data,"[SERVER]")
+ elseif chunk.tags.client then
+ table.insert(data,"[CLIENT]")
+ end
+ table.insert(data, os.date())
+ table.insert(data," > ")
+ table.insert(data,chunk.message)
+ if chunk.level == "error" then
+ print(debug.traceback(table.concat(data)))
+ else
+ print(table.concat(data))
+ end
+end)
+pp.clear_color = require("color").am_color.background
+--am.eval_js(require("js_bridge"))
+--local a,b = pcall(am.eval_js, require("js_bridge"))
diff --git a/src/menu/game.moon b/src/menu/game.moon
new file mode 100644
index 0000000..9b871a4
--- /dev/null
+++ b/src/menu/game.moon
@@ -0,0 +1,208 @@
+settings = require("settings")
+poems = require("poems")
+net = require("net")
+world = require("world")
+log = require("log")
+ui = require("ui")
+win = require("window")
+task = require("task")
+abilities = require("abilities")
+
+x = {}
+
+x.node = am.group!
+net.register_message("RequestRole", {})
+net.register_message("RespondRole", {
+ required: {
+ youare: "string"
+ start: "number"
+ time: "number"
+ }
+ optional: {
+ hint: "string"
+ poem: "string"
+ }
+})
+x.slide_and_fade = (node, delay) ->
+ delay = delay or 0
+ tween = am.ease.sine
+ ease_color = (node) ->
+ end_color = node.color
+ node.color = end_color({a:0})
+ node\action(am.series({
+ am.delay(delay),
+ am.tween(
+ 0.5,
+ {
+ color: end_color
+ },
+ tween
+ )
+ }))
+ for _, text in pairs(node\all("text",true))
+ ease_color(text)
+ for _, sprite in pairs(node\all("sprites",true))
+ ease_color(sprite)
+
+ translate = node\all("translate", true)
+ for _, translate_node in pairs(translate)
+ print("Examining translate node:", translate_node)
+ end_y = translate_node.y
+ translate_node.y += 50
+ translate_node\action(am.series({
+ am.delay(delay),
+ am.tween(
+ 0.5,
+ {
+ y: end_y
+ },
+ tween
+ )
+ }))
+
+-- data contains
+-- > data.youare --("pawn" or "unmasked")
+-- and then either
+-- > data.hint -- (string)
+-- or
+-- > data.poem -- (text)
+x.create_graphic = (data) ->
+ print("Creating mask on screen for data:" .. tostring(data))
+ timer = ui.text(0,400,win.width-40, 100, "00:00")
+ timer.node\tag("timer")
+ alert_minute = false
+ alert_oot = false
+ timer.node\action(() ->
+ now = am.eval_js("Date.now()")
+ end_time = data.start + (data.time * 1000)
+ countdown = math.floor((end_time - now) / 1000)
+ minutes = math.floor(countdown/60)
+ seconds = countdown % 60
+ time_txt = string.format("%02d : %02d",minutes, seconds)
+ timer.node("text").text = time_txt -- assume only 1 line?
+ if countdown < 60 and not alert_minute-- 1 minute
+ log.info("1 minute alert", {"client"})
+ alert_minute = true
+ timer.node\action(am.play(17962709,false,1,1))
+ if countdown < 0 and not alert_oot
+ -- Alert sound
+ log.info("out of time alert", {"client"})
+ alert_oot = true
+ timer.node\action(am.play(96446209,false,1,1))
+ return
+ )
+ click = (n) ->
+ return () ->
+ log.info("Click " .. tostring(n), {"client"})
+ return true
+ timer.node\action(am.series({
+ am.parallel({
+ am.play(68962308,false,1,1)
+ am.delay(1)
+ }),
+ am.parallel({
+ am.play(68962308,false,1,0.75)
+ am.delay(1)
+ }),
+ am.parallel({
+ am.play(68962308,false,1,0.25)
+ am.delay(1)
+ })
+ }))
+ if data.youare == "unmasked"
+ -- Do unmasked stuff
+ youare = ui.text(0,300,win.width-40, 100, "You wear no mask")
+ x.slide_and_fade(youare.node)
+ fools = ui.text(0,200,win.width-40, 100, "These fools, they created an order based on ")
+ x.slide_and_fade(fools.node,0.5)
+ hint = ui.text(0,0,win.width-40, 100, data.hint)
+ x.slide_and_fade(hint.node, 1)
+ keep_looking = ui.text(0,-200,win.width-40,100,"Keep looking 5")
+ keep_looking.node\tag("keep_looking")
+ keep_looking.node\action(() ->
+ now = am.eval_js("Date.now()")
+ end_time = data.start + (5 * 1000)
+ if now < end_time
+ keep_looking.node("text").text = "Keep looking " .. math.ceil((end_time - now) / 1000)
+ else
+ ui.delete(keep_looking)
+
+ )
+ else
+ assert(abilities[data.youare], "No ability hint for role: " .. tostring(data.youare))
+ print("Going into else branch for create_graphic")
+ print("ui.text was", ui.text)
+ print(debug.getinfo(ui.text))
+ youare = ui.text(0,332,win.width-40,100,"You are " .. data.youare)
+ x.slide_and_fade(youare.node)
+ --ability = ui.text(0,264,win.width-40,100,abilities[data.youare])
+ --x.slide_and_fade(ability.node)
+ assert(youare, "Failed to get a node from ui.text")
+ text = ui.text(0,200,win.width-40,100,"You remember the words we spoke at the founding, they were:")
+ x.slide_and_fade(text.node, 0.5)
+ poem = ui.text(0,0,win.width-40,100,data.poem)
+ x.slide_and_fade(poem.node, 1)
+ log.info("Finished creating graphic",{"client"})
+
+
+x.create = () ->
+ if world.hub
+ all_peers = world.hub.clients
+ peers = {} -- masked players
+ for clientid,connection in pairs(all_peers)
+ table.insert(peers, clientid)
+ unmasked = {} -- unmasked players
+ for i = 1, settings.n_unmasked
+ rng = math.random(#peers)
+ table.insert(unmasked, table.remove(peers, rng))
+ poem = poems[math.random(#poems)]
+ hint = poem.hints[math.random(#poem.hints)]
+ start_time = am.eval_js("Date.now()")
+ client_data = {}
+ for _, clientid in ipairs(peers)
+ client_data[clientid] = {
+ youare: "a pawn"
+ poem: poem.text
+ start: start_time
+ time: settings.game_time
+ }
+ --world.hub\send(clientid, "Begin", {
+ --youare: "a pawn"
+ --poem: poem.text
+ --start: start_time
+ --time: settings.game_time
+ --})
+ for _, clientid in ipairs(unmasked)
+ client_data[clientid] = {
+ youare: "unmasked"
+ hint: hint
+ start: start_time
+ time: settings.game_time
+ }
+ --world.hub\send(clientid, "Begin", {
+ --youare: "unmasked"
+ --hint: hint
+ --start: start_time
+ --time: settings.game_time
+ --})
+ world.level_sync.client_data = client_data
+ --world.hub\listen("RequestRole","Request role", (clientid, _) ->
+ --log.info("Responding with role:" .. tostring(client_data[clientid]), {"net","server"})
+ --world.hub\send(clientid, "RespondRole", client_data[clientid])
+ --)
+ world.network\listen("RespondRole", "Respond role", (_, data) ->
+ log.info("Got role from server:" .. tostring(data), {"net","client"})
+ x.create_graphic(data)
+ am.save_state("gameplay", data)
+ )
+ world.network\send("RequestRole",{})
+ --world.network\listen("Begin","Begin game", (hubid, data) ->
+ --log.info("Staring game, data: " .. tostring(data), {"net","client"})
+ --role = data.youare
+ --time_pos = am.translate(vec2(100,20))
+ --x.create_graphic(data)
+ --am.save_state("gameplay", data)
+ --log.info("Finished saving data:" .. tostring(am.load_state("gameplay")), {"net","client"})
+ --)
+
+x
diff --git a/src/menu/input.moon b/src/menu/input.moon
new file mode 100644
index 0000000..f943e6d
--- /dev/null
+++ b/src/menu/input.moon
@@ -0,0 +1,30 @@
+ui = require("ui")
+world = require("world")
+main_menu = require("menu.main")
+input = {}
+
+buttons = {}
+input.initialize = () ->
+ button_mk = ui.button(-630,-100,300,200,"Mouse\nand\nKeyboard")
+ button_touch = ui.button(-300,-250,500,500,"Touch")
+ button_controller = ui.button(250,-64,380,128,"Controller")
+ button_touch.on = () =>
+ world.controller = require("controllers.touch")
+ input.remove!
+ button_mk.on = () =>
+ print("setting mk controller")
+ world.controller = require("controllers.mouse_keyboard")
+ input.remove!
+ require("menu.main").initialize!
+ button_controller.on = () =>
+ world.controller = require("controllers.controller")
+ input.remove!
+ buttons = {button_mk, button_touch, button_controller}
+
+input.remove = () ->
+ print("Removing buttons", buttons)
+ for button in *buttons
+ print("Deleting button",button)
+ ui.delete(button)
+
+input
diff --git a/src/menu/lobby.moon b/src/menu/lobby.moon
new file mode 100644
index 0000000..c20face
--- /dev/null
+++ b/src/menu/lobby.moon
@@ -0,0 +1,103 @@
+ui = require("ui")
+world = require("world")
+log = require("log")
+util = require("util")
+task = require("task")
+net = require("net")
+game = require("menu.game")
+
+menu = {}
+net.register_message("RespondLevel",{
+ name: "string" -- name of the level e.g. "levels.lobby"
+ data: "table" -- sequence to initalize the level
+})
+net.register_message("StartGame",{})
+start_game = nil
+lobby_url = nil
+menu.initialize = () ->
+ log.info("Initializing lobby", {"ui"})
+ game_data = am.load_state("gameplay")
+ now = am.eval_js("Date.now()")
+ --if game_data and (not world.hub) and (now - game_data.start > game_data.time * 1000)
+ --error("Looks like we have a game in progress:" .. tostring(game_data))
+ --menu.destroy!
+ --game.create!
+ --return
+ log.info("Got game data", {"ui","net"})
+ ready = false
+ if world.network_mode == "host"
+ start_game = ui.button(-150,400-128,300,128,"Start!")
+ start_game.on = (e) =>
+ log.info("Starting game!",{"net","server"})
+ world.level_sync.name = "levels.game"
+ for _, ent in pairs(world.level.entities)
+ ent\destroy!
+ -- Actually send the message to start the game
+ log.info("Connected peers were:" .. tostring(world.hub.clients), {"net","server"})
+ world.hub\broadcast("StartGame",{})
+ log.info("Finished creating game, level_sync.name is" .. world.level_sync.name,{"net","server"})
+ else
+ start_game = ui.text(0, 400-64,300,128,"Waiting...")
+ code = nil
+ if world.network_mode == "host"
+ -- For host, use the hub's peer ID (not the local client's peer ID)
+ code = util.peer_to_code(world.hub.peer.id)
+ elseif world.network_mode == "client"
+ params = am.eval_js("window.CLIPBOARD.get_params()")
+ code = params.i
+ else
+ error("world.network must be initialized before creating lobby menu")
+ world.network\listen("StartGame","Lobby start game",() ->
+ log.info("Starting game!",{"net","client"})
+ for _, ent in pairs(world.level.entities)
+ ent\destroy!
+ menu.destroy!
+ game.create!
+ log.info("Finished creating game", {"net","client"})
+ )
+ world.level_sync.data[1] = code
+ path = am.eval_js("window.CLIPBOARD.get_path()")
+ url = string.format("%s?i=%s", path, code)
+ --url_display = string.format("%s\n?i=%s",path,code)
+ url_display = "Copy URL"
+ lobby_url = ui.button(-180,-400+64,360,84,url_display)
+ lobby_url.on = () =>
+ log.info("Clicked button, copying text",{"ui"})
+ transform = am.translate(0,-400+128)
+ copied_text = am.text("Coppied!", vec4(1,1,1,1))
+ transform\append(copied_text)
+ copied_text\action(coroutine.create(() ->
+ i = 1
+ while i > 0
+ i = i - (2/255)
+ copied_text.color = vec4(1,1,1,i)
+ transform.y += i * 3
+ coroutine.yield!
+ lobby_url.node\remove(transform)
+ ))
+ lobby_url.node\append(transform)
+ -- This HAS to be the last action in this function, or else
+ -- javascript thinks this is happening outside of a user interaction.
+ am.eval_js("navigator.clipboard.writeText('" .. url .. "');")
+ log.info("Created lobby buttons", {"ui"})
+ level_loader = coroutine.create(() ->
+ while not world.network.connected
+ log.info("Waiting for network to load level...",{"net","client"})
+ coroutine.yield!
+ --level = world.network\sync("RequestLevel",{},"RespondLevel")
+ --log.info("Got information back from sync" .. tostring(level), {"net","client"})
+ --world.level_sync.name = level.name
+ --world.level_sync.data = level.data
+ --log.info("Loading " .. level.name .. " with data " .. tostring(level.data), {"net","client","level"})
+ --level_mod = assert(require(level.name))
+ --assert(level_mod.create, "Level " .. level.name .. " had no .create()")
+ --level_mod.create(unpack(level.data))
+ )
+ task.add(level_loader)
+
+menu.destroy = () ->
+ ui.delete(lobby_url)
+ if start_game
+ ui.delete(start_game)
+
+menu
diff --git a/src/menu/main.moon b/src/menu/main.moon
new file mode 100644
index 0000000..04f53b0
--- /dev/null
+++ b/src/menu/main.moon
@@ -0,0 +1,137 @@
+ui = require("ui")
+hub_mod = require("hub")
+client_mod = require("client")
+world = require("world")
+log = require("log")
+util = require("util")
+task = require("task")
+server_init = require("server.init")
+client_init = require("client.init")
+menu_lobby = require("menu.lobby")
+NetworkedComponent = require("ecs.networked")
+ecs = require("ecs")
+ScriptComponent = require("ecs.script")
+GraphicsComponent = require("ecs.graphics")
+menu = {}
+am.eval_js(require("party.qrcodejs.qrcode"))
+am.eval_js(require("clipboard_bridge"))
+params = am.eval_js("window.CLIPBOARD.get_params()")
+win = require("window")
+tutorial = require("menu.tutorial")
+
+buttons = {}
+menu.creating = false
+buttons_data = {
+ {
+ text: "Tutorial"
+ on: () =>
+ menu.destroy!
+ tutorial.create!
+ },
+ {
+ text: "Settings"
+ on: () =>
+ log.info("Menu pressed")
+ --menu.destroy!
+ --require("menu.settings").initialize!
+ },
+ {
+ text: "Host"
+ on: () =>
+ log.info("Host pressed")
+ if menu.creating
+ return false-- don't allow the user to click twice
+ menu.creating = true
+ listener = log.listen((chunk) ->
+ if chunk.tags.net or chunk.tags.server
+ @.text.text = chunk.message
+ --s = s .. chunk.message
+ --@.text.text = s
+ )
+ co = coroutine.create(() ->
+ -- Create and initialize the hub
+ hub = hub_mod.Hub!
+ hub\initialize!
+ assert(hub, "Hub was nil")
+ world.hub = hub
+ assert(world.hub, "Failed to set hub correctly")
+ server_init.initialize!
+
+ -- Create and connect the host's client to the hub
+ client = client_mod.Client("host")
+ client\initialize!
+ hub_id = hub.peer.id
+ log.info("Connecting host client to hub: " .. hub_id, {"net"})
+ client\connect_to_hub(hub_id)
+ while not client.connected
+ log.info("Connecting to hub",{"net"})
+ coroutine.yield!
+ -- For integration tests: expose a synthetic Join event to JS so
+ -- the hub/Join flow can be asserted without depending on the
+ -- underlying WebRTC transport.
+ if am and am.eval_js and am.to_json
+ js = string.format("window._hubJoinReceived = true; window._hubJoinData = %s;", am.to_json({name: client.name or "host"}))
+ am.eval_js(js)
+
+ world.network = client
+ world.network_mode = "host"
+ log.info("Hub created with ID: " .. hub_id, {"net"})
+ log.defen(listener)
+ menu_lobby.initialize!
+ client_init.initialize!
+ menu.destroy()
+ menu.creating = false
+ )
+ @.node\action(co)
+
+ }
+
+}
+menu.initialize = () ->
+ if params.i
+ peerid = util.code_to_peer(params.i)
+ co = coroutine.create(() ->
+ while am.eval_js('typeof(Peer) === "undefined"')
+ coroutine.yield!
+ log.info("Found invite param:" .. params.i, {"net"})
+ log.info("Got peer id:" .. tostring(peerid), {"net"})
+ client = client_mod.Client("player")
+ client\initialize!
+ client\connect_to_hub(peerid)
+ world.network = client
+ world.network_mode = "client"
+ log.info("Connected to hub",{"net"})
+ menu.destroy!
+ menu_lobby.initialize!
+ client_init.initialize!
+ )
+ task.add(co)
+ elseif params.dev
+ log.info("Doing game...",{"client"})
+ game = require("menu.game")
+ poems = require("poems")
+ game.create_graphic({
+ youare: "a pawn"
+ poem: poems[2]
+ })
+ --game.create_graphic({
+ --youare: "masked"
+ --poem: "Roses are red, violets are blue, here's a little game for you"
+ --})
+ game.create_graphic({
+ youare: "unmasked"
+ hint: "Roses are red, violets are blue, here's a little game for you"
+ })
+ else
+ print("Creating buttons")
+ starty = 0
+ for i = starty, ((#buttons_data-1) * (82 + 32)) + starty, 64 + 32
+ buttons[#buttons + 1] = ui.button(-150,i,300,82,buttons_data[#buttons + 1].text)
+ buttons[#buttons].on = buttons_data[#buttons].on
+
+menu.destroy = () ->
+ for button in *buttons
+ ui.delete(button)
+ buttons = {}
+
+menu
diff --git a/src/menu/playername.moon b/src/menu/playername.moon
new file mode 100644
index 0000000..82a25f1
--- /dev/null
+++ b/src/menu/playername.moon
@@ -0,0 +1,23 @@
+ui = require("ui")
+world = require("world")
+
+menu = {}
+
+buttons = {}
+menu.initialize = (is_host) ->
+ text = ui.textbox(-100,16,200,32)
+ submit = ui.button(-100,-16,200,32,"Use alias")
+ submit.on = () =>
+ menu.destroy!
+ print("Got name:", text.text.text) -- <textbox class>.<am.text node>.<actual text lookup>
+ if is_host
+ require("worldgen")
+ print("Building player with", world.network, world.controller, text.text.text)
+ player = require("player").Player(world.network, world.controller ,text.text.text)
+ buttons = {text, submit}
+
+menu.destroy = () ->
+ for button in *buttons
+ ui.delete(button)
+
+menu
diff --git a/src/menu/settings.moon b/src/menu/settings.moon
new file mode 100644
index 0000000..009fe82
--- /dev/null
+++ b/src/menu/settings.moon
@@ -0,0 +1,58 @@
+ui = require("ui")
+router = require("router")
+world = require("world")
+settings = require("settings")
+menu = {}
+
+buttons = {}
+buttons_data = {
+ {
+ text: "Done"
+ on: () =>
+ menu.destroy!
+ require("menu.main").initialize!
+ type: "button"
+ }
+ {
+ text: "Streamer"
+ on: (depressed) =>
+ --error("depressed:" .. depressed)
+ settings.streamer = depressed and 0 or 1
+ print("streamer is now:", settings.streamer)
+ type: "boolean"
+ }
+ {
+ text: "Volume"
+ on: (e) =>
+ --require("worldgen")
+ --require("menu.join").initialize!
+ settings.volume = tonumber(@text.text)
+ type: "slider"
+
+ }
+}
+menu.initialize = () ->
+ starty = -200
+ for i = starty, ((#buttons_data-1) * (64 + 32)) + starty, 64 + 32
+ button_data = buttons_data[#buttons + 1]
+ if button_data.type == "boolean"
+ buttons[#buttons + 1] = ui.checkbox(-200,i,400,64,button_data.text)
+ buttons[#buttons].on = button_data.on
+ elseif button_data.type == "slider"
+ buttons[#buttons + 1] = ui.textbox(-200,i,400,64,settings.volume, "volume")
+ buttons[#buttons].on = button_data.on
+ elseif button_data.type == "button"
+ buttons[#buttons + 1] = ui.button(-200,i,400,64,button_data.text)
+ buttons[#buttons].on = button_data.on
+ else
+ error("Unknown button type:" .. button_data.type)
+ print("making button", #buttons + 1)
+
+ print("intalize")
+
+menu.destroy = () ->
+ for button in *buttons
+ ui.delete(button)
+ buttons = {}
+
+menu
diff --git a/src/menu/tutorial.moon b/src/menu/tutorial.moon
new file mode 100644
index 0000000..58a15d7
--- /dev/null
+++ b/src/menu/tutorial.moon
@@ -0,0 +1,106 @@
+log = require("log")
+ui = require("ui")
+sprites = require("sprites")
+game = require("menu.game")
+window = require("window")
+color = require("color")
+
+x = {}
+
+screens = {
+"This is a game of deception",
+"You remember joining the cult, right?
+Of course you do, and more importantly,
+you remember the words spoken at our founding.",
+"We have an uninvited guest here tonight,
+they do not know our phrase,
+but they do have some idea of what it might be.",
+"Our time is short and we must begin
+our work, talk with your fellows to find our
+uninvited guest.",
+"Your host can modify time, roles, and
+the number of uninvited guests in the settings",
+}
+x.node = am.group!
+next_but = nil
+render_frame = () ->
+ outline = am.group!
+ bg = am.rect((-window.width / 2) - 8,(-window.height / 2) - 8, (window.width / 2) + 8, (window.height / 2) + 8, color.am_color.foreground)
+ bg2 = am.rect((-window.width / 2),(-window.height / 2), (window.width / 2), (window.height / 2), color.am_color.background)
+ --top= am.line(vec2(-window.width/2,window.height/2),vec2(window.width,window.height/2),20,color.am_color.foreground)
+ outline\append(bg)
+ outline\append(bg2)
+ outline
+
+x.create = () ->
+ next_but = ui.button(-160, -400+20, 320, 84, "Next")
+ screen_i = 1
+ hint_t = ui.text(0,400,360,600,screens[screen_i])
+ next_but.on = () =>
+ screen_i += 1
+ log.info("Advancing tutorial screen, new text is:" .. tostring(screens[screen_i]), {"ui"})
+ if hint_t
+ ui.delete(hint_t)
+ if not screens[screen_i]
+ x.destroy!
+ hint_t = ui.text(0,400,360,600,screens[screen_i])
+ if screen_i == 2
+ scale = am.scale(0.5)
+ text_pos = am.translate(0,-64)
+ text_pos\tag("tutorial")
+ ui.node\append(text_pos)
+ text_pos\append(scale)
+ oldui = ui.node
+ ui.node = am.group!
+ scale\append(ui.node)
+ ui.node\append(render_frame!)
+ game.create_graphic({
+ youare: "a pawn"
+ poem: "Roses are red, violets are blue, here's a little game for you"
+ time: 600
+ start: am.eval_js("Date.now()")
+ })
+ ui.node("timer").hidden = true
+ ui.node("timer").paused = true
+ ui.node = oldui
+ if screen_i == 3
+ prev_graphic = ui.node("tutorial")
+ if prev_graphic
+ ui.node\remove(prev_graphic)
+ scale = am.scale(0.5)
+ text_pos = am.translate(0,-64)
+ text_pos\tag("tutorial")
+ ui.node\append(text_pos)
+ text_pos\append(scale)
+ oldui = ui.node
+ ui.node = am.group!
+ ui.node\append(render_frame!)
+ scale\append(ui.node)
+ game.create_graphic({
+ youare: "unmasked"
+ hint: "Flowers and fun"
+ time: 600
+ start: am.eval_js("Date.now()")
+ })
+ ui.node("timer").hidden = true
+ ui.node("timer").paused = true
+ ui.node("keep_looking").hidden = true
+ ui.node = oldui
+ if screen_i == 4
+ prev_graphic = ui.node("tutorial")
+ prev_graphic("timer").hidden = false
+ prev_graphic("timer").paused = false
+ if sceen_i == 5
+ prev_graphic = ui.node("tutorial")
+ if prev_graphic
+ ui.node\remove(prev_graphic)
+
+
+x.destroy = () ->
+ prev_graphic = ui.node("tutorial")
+ if prev_graphic
+ ui.node\remove(prev_graphic)
+ ui.delete(next_but)
+ require("menu.main").initialize!
+
+x
diff --git a/src/net.moon b/src/net.moon
new file mode 100644
index 0000000..49461b6
--- /dev/null
+++ b/src/net.moon
@@ -0,0 +1,281 @@
+-- Handles the bridge to javascript to do peer-to-peer connections
+
+log = require("log")
+rng = require("rng")
+util = require("util")
+
+net = {}
+
+initialized = false
+initialize = () ->
+ am.eval_js(require("js_bridge"))
+ initialized = true
+
+net.call = (method, args) ->
+ if not initialized
+ initialize!
+ args = args or {}
+ json_str = am.to_json(args)
+ log.info("Json string sent to javascript:" .. tostring(json_str), {"net"})
+ result = am.eval_js("window.PEER." .. method .. "(" .. am.to_json(args) .. ")")
+ result
+
+net.pull_peers = () ->
+ if not initialized
+ initialize!
+ messages = am.eval_js("window.PEER.peer_message_queue")
+ am.eval_js("window.PEER.peer_message_queue = []")
+ messages
+
+net.pull_connections = () ->
+ if not initialized
+ initialize!
+ messages = am.eval_js("window.PEER.connection_message_queue")
+ am.eval_js("window.PEER.connection_message_queue = []")
+ messages
+
+net.pull_creation = () ->
+ if not initialized
+ initialize!
+ creations = am.eval_js("window.PEER.creation_queue")
+ am.eval_js("window.PEER.creation_queue = []")
+ creations
+
+-- Sequence of function(peer, message) | function(connection, message)
+-- functions of (peer,message) for peer "open","connection","call","close","disconnected","error"
+-- functions of (connection, message) for connection "data","open","close","error"
+callbacks = {}
+callback_info = {}
+-- Map of [string peerid] = Peer
+peers = {}
+
+--Connections are always create js side, this is just it's lua representation
+class Connection
+ @connections = {}
+ @methods = util.reverse({"data","open","close","error"})
+ new: (source, dest) =>
+ @source = source
+ @dest = dest
+ @get: (source, dest) =>
+ key = table.concat({source,dest},",")
+ if @connections[key]
+ return @connections[key]
+ @@connections[key] = Connection(source,dest)
+ @@connections[key]
+ on: (event, callback) =>
+ if not @@methods[event]
+ error("Tried to set an unknown event (" .. event .. ") on a connection")
+ newid = #callbacks + 1
+ callbacks[newid] = callback
+ callback_info[newid] = debug.getinfo(callback)
+ -- Wait until the JS-side directional connection [source,dest] is ready
+ while am.eval_js('window.PEER.connections[["' .. @source .. '","' .. @dest .. '"]] == null')
+ coroutine.yield("Waiting for peer")
+ -- Attach handler to this directional connection (source -> dest)
+ net.call("conn_on", {source: @source, dest: @dest, e: event, message: newid})
+ send: (msgname, msg) =>
+ -- Send as array: [msgname, msg]
+ log.info("Sending",{"net"})
+ res = net.call("send",{source: @source, dest: @dest, data: {msgname, msg}})
+ res
+
+class Peer
+ @methods = util.reverse({"open","connection","call","close","disconnected","error"})
+ @max_attempts = 4
+ @create_timeout = 10
+ new: () =>
+ log.info("Creating peer...", {"net"})
+ net.call("create")
+ creations = {}
+ starttime = am.eval_js("Date.now()")
+ attempts = 0
+ while #creations == 0 and attempts < @@max_attempts
+ creations = net.pull_creation()
+ if #creations > 1
+ error("Created more than 1 peer at a time, we don't know which one we are")
+ messages = net.pull_connections()
+ log.info("Creating peer " .. attempts .. "/" .. tostring(@@max_attempts), {"net"})
+ if #messages > 0
+ if messages[1] and messages[1].data and messages[1].data.message.type == "network"
+ -- Try again
+ net.call("create")
+ attempts += 1
+ starttime = am.eval_js("Date.now()")
+ else
+ error(tostring(messages))
+ if am.eval_js("Date.now()") - starttime > (@@create_timeout * 1000)
+ net.call("create")
+ attempts += 1
+ starttime = am.eval_js("Date.now()")
+ if attempts > @@max_attempts
+ error("Failed to create host after 4 attempts")
+ coroutine.yield!
+ if attempts == @@max_attempts
+ error("Failed to create peer, check https://status.peerjs.com")
+ @id = creations[1]
+ peers[@id] = @
+ log.info("Creating peer: " .. @id, {"net"})
+ generate_id: () =>
+ os.date("%Y%e") .. rng.numstring(4)
+ replace_id: () =>
+ log.info("Regenerating id for peer: " .. @id, {"net"})
+ -- peers[@id] = nil TODO: uncomment, this breaks when running multiple peers from the same tab.
+ net.call("delete_peer",{name: @id})
+ @id = @generate_id!
+ peers[@id] = @
+ net.call("create", {name: @id})
+ on: (event, callback) =>
+ if not @@methods[event]
+ error("Tried to set an unknown event (" .. event .. ") on a peer.")
+ newid = #callbacks + 1
+ callbacks[newid] = callback
+ net.call("on",{name: @id, message:newid, e: event})
+
+ connect: (id, options) =>
+ conn = net.call("connect", {source: @id, dest: id})
+ log.info("Got connection: " .. tostring(conn), {"net"})
+ Connection\get(conn[1],conn[2])
+
+
+net.Peer = Peer
+
+-- A fake peer for testing
+fakepeers = {}
+fakeconnections = {}
+fakecallbacks = {}
+channel = require("channel")
+
+class FakePeer
+ new: (id) =>
+ if id
+ @id = id
+ fakepeers[id] = @
+ on: (event, callback) =>
+ newid = #fakecallbacks + 1
+ fakecallbacks[newid] = callback
+
+ connect: (id, options) =>
+ conn = channel.FaultyChannel({
+ avg_latency: 200
+ latency_std: 100
+ loss: 0.1
+ })
+ conn
+
+
+messages = {}
+formatcache = {}
+message_callbacks = {}
+net.register_message = (name, format) ->
+ assert(type(format) == "table", "Format must be a table")
+ format.required = format.required or {}
+ format.optional = format.optional or {}
+ if not (next(format.required) or next(format.optional))
+ log.warn("Message " .. name .. " registered with no fields.")
+ for set in *({format.required, format.optional})
+ for field, type_ in pairs(set)
+ if type(type_) == "string"
+ key = string.format("%s\0%s\0%s",name,field,type_)
+ if not formatcache[key]
+ formatcache[key] = (any) ->
+ assert(type(any) == type_, string.format("In message %q %q must be a %q, but was a %q", name, field, type_, type(any)))
+ set[field] = formatcache[key]
+ messages[name] = format
+ message_callbacks[name] = {}
+ log.info("Registered message type:" .. name, {"net"})
+
+net.validate = (name, message) ->
+ log.debug("Validating message:" .. tostring(message), {"net"})
+ assert(type(message) == "table", "Message must be a table")
+ format = messages[name]
+ assert(format, "Failed to find a format: " .. name)
+ required = {}
+ for field, validate in pairs(format.required)
+ required[field] = validate
+ for field, value in pairs(message)
+ if format.required[field]
+ required[field](value)
+ required[field] = nil
+ if format.optional[field]
+ format.optional[field](value)
+ missing = next(required)
+ if missing
+ error("Missing required field: " .. missing)
+ true
+
+net.listen = (name, callback, id) ->
+ id = id or {}
+ message_callbacks[name] = message_callbacks[name] or {}
+ message_callbacks[name][id] = callback
+ id
+
+net.defen = (name, id) ->
+ message_callbacks[name][id] = nil
+
+-- net.route = (conn, name, data) ->
+-- if message_callbacks[name]
+-- for id, callback in pairs(message_callbacks[name])
+-- ret = message_callbacks[name](conn,
+
+net.rewrite_events = {
+ connection: (message) ->
+ -- message.data.data is [server, client]; build Hub-side Connection(server->client)
+ conn = Connection\get(message.data.data[1], message.data.data[2])
+ assert(conn, "Failed to build conn?")
+ assert(conn.source and conn.dest)
+ message.data.data = conn
+}
+
+net.pump = () ->
+ msg_ = net.pull_peers!
+ if #msg_ > 0
+ log.info("Processing " .. tostring(#msg_) .. " peer messages", {"net"})
+ for message in *msg_
+ --log.info(tostring(message), {"net", message.data.peer})
+ if net.rewrite_events[message.data.e]
+ log.info("Rewriting data due to " .. message.data.e .. " event", {"net", message.data.peer})
+ net.rewrite_events[message.data.e](message)
+ log.info(tostring(message), {"net", message.data.peer})
+ if not message.data.peer and message.data.e == "open"
+ log.info("Setting peerid for a peer that didn't have one " ..tostring(message), {"net"})
+ peer = peers[message.data.peer]
+ assert(peer, "Failed to find peer:" .. message.data.peer .. " peers:" .. tostring(net.peers!))
+ callback = callbacks[message.message]
+ assert(callback, "Failed to find callback " .. message.message .. " on peer " .. message.data.peer)
+ callback(peer,message.data)
+ msg_ = net.pull_connections!
+ if #msg_ > 0
+ log.info("Processing " .. tostring(#msg_) .. " connection messages", {"net"})
+ for message in *msg_
+ --log.info(tostring(message), {"net", message.data.peer})
+ -- Extra debug for connection routing
+ peer = message.data and message.data.peer or "nil"
+ dest = message.data and message.data.dest or "nil"
+ inner = message.data and message.data.data or nil
+ log.debug("NET connection msg peer=" .. tostring(peer) ..
+ " dest=" .. tostring(dest) ..
+ " inner=" .. tostring(inner), {"net", "debug"})
+ -- For connection events, message.data is a wrapper from JS bridge.
+ -- The actual payload from Connection:send is in message.data.data.
+ payload = inner or message.data
+ -- message.data.peer is the source, message.data.dest is the dest from JS bridge
+ connection = Connection\get(message.data.peer, message.data.dest)
+ callback = callbacks[message.message]
+ if callback
+ wcall = () ->
+ callback(connection, payload)
+ handler = (err) ->
+ info = callback_info[message.message]
+ string.format("Failed to call callback defined at %s:%d:\n%s", info.short_src, info.linedefined, debug.traceback(err))
+ assert(xpcall(wcall, handler))
+ else
+ log.warn("Failed to find callback for message:" .. tostring(message),{"net"})
+ --assert(callback, "Failed to find callback " .. tostring(message.message) .. " for message" .. tostring(message))
+
+net.peers = () ->
+ peers
+
+net.node = am.group!
+initialize!
+
+net
diff --git a/src/party/English-word-lists-parts-of-speech-approximate b/src/party/English-word-lists-parts-of-speech-approximate
new file mode 160000
+Subproject a78e65cb52d65662a99fba2806d2a1109a8506a
diff --git a/src/party/hc b/src/party/hc
new file mode 160000
+Subproject eb1f285cb1cc4d951d90c92b64a4fc85e7ed06b
diff --git a/src/party/qrcodejs/qrcode.js b/src/party/qrcodejs/qrcode.js
new file mode 100644
index 0000000..c750dd4
--- /dev/null
+++ b/src/party/qrcodejs/qrcode.js
@@ -0,0 +1 @@
+var QRCode; !function(){function a(a){this.mode=c.MODE_8BIT_BYTE,this.data=a,this.parsedData=[];for(var b=[],d=0,e=this.data.length;e>d;d++){var f=this.data.charCodeAt(d);f>65536?(b[0]=240|(1835008&f)>>>18,b[1]=128|(258048&f)>>>12,b[2]=128|(4032&f)>>>6,b[3]=128|63&f):f>2048?(b[0]=224|(61440&f)>>>12,b[1]=128|(4032&f)>>>6,b[2]=128|63&f):f>128?(b[0]=192|(1984&f)>>>6,b[1]=128|63&f):b[0]=f,this.parsedData=this.parsedData.concat(b)}this.parsedData.length!=this.data.length&&(this.parsedData.unshift(191),this.parsedData.unshift(187),this.parsedData.unshift(239))}function b(a,b){this.typeNumber=a,this.errorCorrectLevel=b,this.modules=null,this.moduleCount=0,this.dataCache=null,this.dataList=[]}function i(a,b){if(void 0==a.length)throw new Error(a.length+"/"+b);for(var c=0;c<a.length&&0==a[c];)c++;this.num=new Array(a.length-c+b);for(var d=0;d<a.length-c;d++)this.num[d]=a[d+c]}function j(a,b){this.totalCount=a,this.dataCount=b}function k(){this.buffer=[],this.length=0}function m(){return"undefined"!=typeof CanvasRenderingContext2D}function n(){var a=!1,b=navigator.userAgent;return/android/i.test(b)&&(a=!0,aMat=b.toString().match(/android ([0-9]\.[0-9])/i),aMat&&aMat[1]&&(a=parseFloat(aMat[1]))),a}function r(a,b){for(var c=1,e=s(a),f=0,g=l.length;g>=f;f++){var h=0;switch(b){case d.L:h=l[f][0];break;case d.M:h=l[f][1];break;case d.Q:h=l[f][2];break;case d.H:h=l[f][3]}if(h>=e)break;c++}if(c>l.length)throw new Error("Too long data");return c}function s(a){var b=encodeURI(a).toString().replace(/\%[0-9a-fA-F]{2}/g,"a");return b.length+(b.length!=a?3:0)}a.prototype={getLength:function(){return this.parsedData.length},write:function(a){for(var b=0,c=this.parsedData.length;c>b;b++)a.put(this.parsedData[b],8)}},b.prototype={addData:function(b){var c=new a(b);this.dataList.push(c),this.dataCache=null},isDark:function(a,b){if(0>a||this.moduleCount<=a||0>b||this.moduleCount<=b)throw new Error(a+","+b);return this.modules[a][b]},getModuleCount:function(){return this.moduleCount},make:function(){this.makeImpl(!1,this.getBestMaskPattern())},makeImpl:function(a,c){this.moduleCount=4*this.typeNumber+17,this.modules=new Array(this.moduleCount);for(var d=0;d<this.moduleCount;d++){this.modules[d]=new Array(this.moduleCount);for(var e=0;e<this.moduleCount;e++)this.modules[d][e]=null}this.setupPositionProbePattern(0,0),this.setupPositionProbePattern(this.moduleCount-7,0),this.setupPositionProbePattern(0,this.moduleCount-7),this.setupPositionAdjustPattern(),this.setupTimingPattern(),this.setupTypeInfo(a,c),this.typeNumber>=7&&this.setupTypeNumber(a),null==this.dataCache&&(this.dataCache=b.createData(this.typeNumber,this.errorCorrectLevel,this.dataList)),this.mapData(this.dataCache,c)},setupPositionProbePattern:function(a,b){for(var c=-1;7>=c;c++)if(!(-1>=a+c||this.moduleCount<=a+c))for(var d=-1;7>=d;d++)-1>=b+d||this.moduleCount<=b+d||(this.modules[a+c][b+d]=c>=0&&6>=c&&(0==d||6==d)||d>=0&&6>=d&&(0==c||6==c)||c>=2&&4>=c&&d>=2&&4>=d?!0:!1)},getBestMaskPattern:function(){for(var a=0,b=0,c=0;8>c;c++){this.makeImpl(!0,c);var d=f.getLostPoint(this);(0==c||a>d)&&(a=d,b=c)}return b},createMovieClip:function(a,b,c){var d=a.createEmptyMovieClip(b,c),e=1;this.make();for(var f=0;f<this.modules.length;f++)for(var g=f*e,h=0;h<this.modules[f].length;h++){var i=h*e,j=this.modules[f][h];j&&(d.beginFill(0,100),d.moveTo(i,g),d.lineTo(i+e,g),d.lineTo(i+e,g+e),d.lineTo(i,g+e),d.endFill())}return d},setupTimingPattern:function(){for(var a=8;a<this.moduleCount-8;a++)null==this.modules[a][6]&&(this.modules[a][6]=0==a%2);for(var b=8;b<this.moduleCount-8;b++)null==this.modules[6][b]&&(this.modules[6][b]=0==b%2)},setupPositionAdjustPattern:function(){for(var a=f.getPatternPosition(this.typeNumber),b=0;b<a.length;b++)for(var c=0;c<a.length;c++){var d=a[b],e=a[c];if(null==this.modules[d][e])for(var g=-2;2>=g;g++)for(var h=-2;2>=h;h++)this.modules[d+g][e+h]=-2==g||2==g||-2==h||2==h||0==g&&0==h?!0:!1}},setupTypeNumber:function(a){for(var b=f.getBCHTypeNumber(this.typeNumber),c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[Math.floor(c/3)][c%3+this.moduleCount-8-3]=d}for(var c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[c%3+this.moduleCount-8-3][Math.floor(c/3)]=d}},setupTypeInfo:function(a,b){for(var c=this.errorCorrectLevel<<3|b,d=f.getBCHTypeInfo(c),e=0;15>e;e++){var g=!a&&1==(1&d>>e);6>e?this.modules[e][8]=g:8>e?this.modules[e+1][8]=g:this.modules[this.moduleCount-15+e][8]=g}for(var e=0;15>e;e++){var g=!a&&1==(1&d>>e);8>e?this.modules[8][this.moduleCount-e-1]=g:9>e?this.modules[8][15-e-1+1]=g:this.modules[8][15-e-1]=g}this.modules[this.moduleCount-8][8]=!a},mapData:function(a,b){for(var c=-1,d=this.moduleCount-1,e=7,g=0,h=this.moduleCount-1;h>0;h-=2)for(6==h&&h--;;){for(var i=0;2>i;i++)if(null==this.modules[d][h-i]){var j=!1;g<a.length&&(j=1==(1&a[g]>>>e));var k=f.getMask(b,d,h-i);k&&(j=!j),this.modules[d][h-i]=j,e--,-1==e&&(g++,e=7)}if(d+=c,0>d||this.moduleCount<=d){d-=c,c=-c;break}}}},b.PAD0=236,b.PAD1=17,b.createData=function(a,c,d){for(var e=j.getRSBlocks(a,c),g=new k,h=0;h<d.length;h++){var i=d[h];g.put(i.mode,4),g.put(i.getLength(),f.getLengthInBits(i.mode,a)),i.write(g)}for(var l=0,h=0;h<e.length;h++)l+=e[h].dataCount;if(g.getLengthInBits()>8*l)throw new Error("code length overflow. ("+g.getLengthInBits()+">"+8*l+")");for(g.getLengthInBits()+4<=8*l&&g.put(0,4);0!=g.getLengthInBits()%8;)g.putBit(!1);for(;;){if(g.getLengthInBits()>=8*l)break;if(g.put(b.PAD0,8),g.getLengthInBits()>=8*l)break;g.put(b.PAD1,8)}return b.createBytes(g,e)},b.createBytes=function(a,b){for(var c=0,d=0,e=0,g=new Array(b.length),h=new Array(b.length),j=0;j<b.length;j++){var k=b[j].dataCount,l=b[j].totalCount-k;d=Math.max(d,k),e=Math.max(e,l),g[j]=new Array(k);for(var m=0;m<g[j].length;m++)g[j][m]=255&a.buffer[m+c];c+=k;var n=f.getErrorCorrectPolynomial(l),o=new i(g[j],n.getLength()-1),p=o.mod(n);h[j]=new Array(n.getLength()-1);for(var m=0;m<h[j].length;m++){var q=m+p.getLength()-h[j].length;h[j][m]=q>=0?p.get(q):0}}for(var r=0,m=0;m<b.length;m++)r+=b[m].totalCount;for(var s=new Array(r),t=0,m=0;d>m;m++)for(var j=0;j<b.length;j++)m<g[j].length&&(s[t++]=g[j][m]);for(var m=0;e>m;m++)for(var j=0;j<b.length;j++)m<h[j].length&&(s[t++]=h[j][m]);return s};for(var c={MODE_NUMBER:1,MODE_ALPHA_NUM:2,MODE_8BIT_BYTE:4,MODE_KANJI:8},d={L:1,M:0,Q:3,H:2},e={PATTERN000:0,PATTERN001:1,PATTERN010:2,PATTERN011:3,PATTERN100:4,PATTERN101:5,PATTERN110:6,PATTERN111:7},f={PATTERN_POSITION_TABLE:[[],[6,18],[6,22],[6,26],[6,30],[6,34],[6,22,38],[6,24,42],[6,26,46],[6,28,50],[6,30,54],[6,32,58],[6,34,62],[6,26,46,66],[6,26,48,70],[6,26,50,74],[6,30,54,78],[6,30,56,82],[6,30,58,86],[6,34,62,90],[6,28,50,72,94],[6,26,50,74,98],[6,30,54,78,102],[6,28,54,80,106],[6,32,58,84,110],[6,30,58,86,114],[6,34,62,90,118],[6,26,50,74,98,122],[6,30,54,78,102,126],[6,26,52,78,104,130],[6,30,56,82,108,134],[6,34,60,86,112,138],[6,30,58,86,114,142],[6,34,62,90,118,146],[6,30,54,78,102,126,150],[6,24,50,76,102,128,154],[6,28,54,80,106,132,158],[6,32,58,84,110,136,162],[6,26,54,82,110,138,166],[6,30,58,86,114,142,170]],G15:1335,G18:7973,G15_MASK:21522,getBCHTypeInfo:function(a){for(var b=a<<10;f.getBCHDigit(b)-f.getBCHDigit(f.G15)>=0;)b^=f.G15<<f.getBCHDigit(b)-f.getBCHDigit(f.G15);return(a<<10|b)^f.G15_MASK},getBCHTypeNumber:function(a){for(var b=a<<12;f.getBCHDigit(b)-f.getBCHDigit(f.G18)>=0;)b^=f.G18<<f.getBCHDigit(b)-f.getBCHDigit(f.G18);return a<<12|b},getBCHDigit:function(a){for(var b=0;0!=a;)b++,a>>>=1;return b},getPatternPosition:function(a){return f.PATTERN_POSITION_TABLE[a-1]},getMask:function(a,b,c){switch(a){case e.PATTERN000:return 0==(b+c)%2;case e.PATTERN001:return 0==b%2;case e.PATTERN010:return 0==c%3;case e.PATTERN011:return 0==(b+c)%3;case e.PATTERN100:return 0==(Math.floor(b/2)+Math.floor(c/3))%2;case e.PATTERN101:return 0==b*c%2+b*c%3;case e.PATTERN110:return 0==(b*c%2+b*c%3)%2;case e.PATTERN111:return 0==(b*c%3+(b+c)%2)%2;default:throw new Error("bad maskPattern:"+a)}},getErrorCorrectPolynomial:function(a){for(var b=new i([1],0),c=0;a>c;c++)b=b.multiply(new i([1,g.gexp(c)],0));return b},getLengthInBits:function(a,b){if(b>=1&&10>b)switch(a){case c.MODE_NUMBER:return 10;case c.MODE_ALPHA_NUM:return 9;case c.MODE_8BIT_BYTE:return 8;case c.MODE_KANJI:return 8;default:throw new Error("mode:"+a)}else if(27>b)switch(a){case c.MODE_NUMBER:return 12;case c.MODE_ALPHA_NUM:return 11;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 10;default:throw new Error("mode:"+a)}else{if(!(41>b))throw new Error("type:"+b);switch(a){case c.MODE_NUMBER:return 14;case c.MODE_ALPHA_NUM:return 13;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 12;default:throw new Error("mode:"+a)}}},getLostPoint:function(a){for(var b=a.getModuleCount(),c=0,d=0;b>d;d++)for(var e=0;b>e;e++){for(var f=0,g=a.isDark(d,e),h=-1;1>=h;h++)if(!(0>d+h||d+h>=b))for(var i=-1;1>=i;i++)0>e+i||e+i>=b||(0!=h||0!=i)&&g==a.isDark(d+h,e+i)&&f++;f>5&&(c+=3+f-5)}for(var d=0;b-1>d;d++)for(var e=0;b-1>e;e++){var j=0;a.isDark(d,e)&&j++,a.isDark(d+1,e)&&j++,a.isDark(d,e+1)&&j++,a.isDark(d+1,e+1)&&j++,(0==j||4==j)&&(c+=3)}for(var d=0;b>d;d++)for(var e=0;b-6>e;e++)a.isDark(d,e)&&!a.isDark(d,e+1)&&a.isDark(d,e+2)&&a.isDark(d,e+3)&&a.isDark(d,e+4)&&!a.isDark(d,e+5)&&a.isDark(d,e+6)&&(c+=40);for(var e=0;b>e;e++)for(var d=0;b-6>d;d++)a.isDark(d,e)&&!a.isDark(d+1,e)&&a.isDark(d+2,e)&&a.isDark(d+3,e)&&a.isDark(d+4,e)&&!a.isDark(d+5,e)&&a.isDark(d+6,e)&&(c+=40);for(var k=0,e=0;b>e;e++)for(var d=0;b>d;d++)a.isDark(d,e)&&k++;var l=Math.abs(100*k/b/b-50)/5;return c+=10*l}},g={glog:function(a){if(1>a)throw new Error("glog("+a+")");return g.LOG_TABLE[a]},gexp:function(a){for(;0>a;)a+=255;for(;a>=256;)a-=255;return g.EXP_TABLE[a]},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)},h=0;8>h;h++)g.EXP_TABLE[h]=1<<h;for(var h=8;256>h;h++)g.EXP_TABLE[h]=g.EXP_TABLE[h-4]^g.EXP_TABLE[h-5]^g.EXP_TABLE[h-6]^g.EXP_TABLE[h-8];for(var h=0;255>h;h++)g.LOG_TABLE[g.EXP_TABLE[h]]=h;i.prototype={get:function(a){return this.num[a]},getLength:function(){return this.num.length},multiply:function(a){for(var b=new Array(this.getLength()+a.getLength()-1),c=0;c<this.getLength();c++)for(var d=0;d<a.getLength();d++)b[c+d]^=g.gexp(g.glog(this.get(c))+g.glog(a.get(d)));return new i(b,0)},mod:function(a){if(this.getLength()-a.getLength()<0)return this;for(var b=g.glog(this.get(0))-g.glog(a.get(0)),c=new Array(this.getLength()),d=0;d<this.getLength();d++)c[d]=this.get(d);for(var d=0;d<a.getLength();d++)c[d]^=g.gexp(g.glog(a.get(d))+b);return new i(c,0).mod(a)}},j.RS_BLOCK_TABLE=[[1,26,19],[1,26,16],[1,26,13],[1,26,9],[1,44,34],[1,44,28],[1,44,22],[1,44,16],[1,70,55],[1,70,44],[2,35,17],[2,35,13],[1,100,80],[2,50,32],[2,50,24],[4,25,9],[1,134,108],[2,67,43],[2,33,15,2,34,16],[2,33,11,2,34,12],[2,86,68],[4,43,27],[4,43,19],[4,43,15],[2,98,78],[4,49,31],[2,32,14,4,33,15],[4,39,13,1,40,14],[2,121,97],[2,60,38,2,61,39],[4,40,18,2,41,19],[4,40,14,2,41,15],[2,146,116],[3,58,36,2,59,37],[4,36,16,4,37,17],[4,36,12,4,37,13],[2,86,68,2,87,69],[4,69,43,1,70,44],[6,43,19,2,44,20],[6,43,15,2,44,16],[4,101,81],[1,80,50,4,81,51],[4,50,22,4,51,23],[3,36,12,8,37,13],[2,116,92,2,117,93],[6,58,36,2,59,37],[4,46,20,6,47,21],[7,42,14,4,43,15],[4,133,107],[8,59,37,1,60,38],[8,44,20,4,45,21],[12,33,11,4,34,12],[3,145,115,1,146,116],[4,64,40,5,65,41],[11,36,16,5,37,17],[11,36,12,5,37,13],[5,109,87,1,110,88],[5,65,41,5,66,42],[5,54,24,7,55,25],[11,36,12],[5,122,98,1,123,99],[7,73,45,3,74,46],[15,43,19,2,44,20],[3,45,15,13,46,16],[1,135,107,5,136,108],[10,74,46,1,75,47],[1,50,22,15,51,23],[2,42,14,17,43,15],[5,150,120,1,151,121],[9,69,43,4,70,44],[17,50,22,1,51,23],[2,42,14,19,43,15],[3,141,113,4,142,114],[3,70,44,11,71,45],[17,47,21,4,48,22],[9,39,13,16,40,14],[3,135,107,5,136,108],[3,67,41,13,68,42],[15,54,24,5,55,25],[15,43,15,10,44,16],[4,144,116,4,145,117],[17,68,42],[17,50,22,6,51,23],[19,46,16,6,47,17],[2,139,111,7,140,112],[17,74,46],[7,54,24,16,55,25],[34,37,13],[4,151,121,5,152,122],[4,75,47,14,76,48],[11,54,24,14,55,25],[16,45,15,14,46,16],[6,147,117,4,148,118],[6,73,45,14,74,46],[11,54,24,16,55,25],[30,46,16,2,47,17],[8,132,106,4,133,107],[8,75,47,13,76,48],[7,54,24,22,55,25],[22,45,15,13,46,16],[10,142,114,2,143,115],[19,74,46,4,75,47],[28,50,22,6,51,23],[33,46,16,4,47,17],[8,152,122,4,153,123],[22,73,45,3,74,46],[8,53,23,26,54,24],[12,45,15,28,46,16],[3,147,117,10,148,118],[3,73,45,23,74,46],[4,54,24,31,55,25],[11,45,15,31,46,16],[7,146,116,7,147,117],[21,73,45,7,74,46],[1,53,23,37,54,24],[19,45,15,26,46,16],[5,145,115,10,146,116],[19,75,47,10,76,48],[15,54,24,25,55,25],[23,45,15,25,46,16],[13,145,115,3,146,116],[2,74,46,29,75,47],[42,54,24,1,55,25],[23,45,15,28,46,16],[17,145,115],[10,74,46,23,75,47],[10,54,24,35,55,25],[19,45,15,35,46,16],[17,145,115,1,146,116],[14,74,46,21,75,47],[29,54,24,19,55,25],[11,45,15,46,46,16],[13,145,115,6,146,116],[14,74,46,23,75,47],[44,54,24,7,55,25],[59,46,16,1,47,17],[12,151,121,7,152,122],[12,75,47,26,76,48],[39,54,24,14,55,25],[22,45,15,41,46,16],[6,151,121,14,152,122],[6,75,47,34,76,48],[46,54,24,10,55,25],[2,45,15,64,46,16],[17,152,122,4,153,123],[29,74,46,14,75,47],[49,54,24,10,55,25],[24,45,15,46,46,16],[4,152,122,18,153,123],[13,74,46,32,75,47],[48,54,24,14,55,25],[42,45,15,32,46,16],[20,147,117,4,148,118],[40,75,47,7,76,48],[43,54,24,22,55,25],[10,45,15,67,46,16],[19,148,118,6,149,119],[18,75,47,31,76,48],[34,54,24,34,55,25],[20,45,15,61,46,16]],j.getRSBlocks=function(a,b){var c=j.getRsBlockTable(a,b);if(void 0==c)throw new Error("bad rs block @ typeNumber:"+a+"/errorCorrectLevel:"+b);for(var d=c.length/3,e=[],f=0;d>f;f++)for(var g=c[3*f+0],h=c[3*f+1],i=c[3*f+2],k=0;g>k;k++)e.push(new j(h,i));return e},j.getRsBlockTable=function(a,b){switch(b){case d.L:return j.RS_BLOCK_TABLE[4*(a-1)+0];case d.M:return j.RS_BLOCK_TABLE[4*(a-1)+1];case d.Q:return j.RS_BLOCK_TABLE[4*(a-1)+2];case d.H:return j.RS_BLOCK_TABLE[4*(a-1)+3];default:return void 0}},k.prototype={get:function(a){var b=Math.floor(a/8);return 1==(1&this.buffer[b]>>>7-a%8)},put:function(a,b){for(var c=0;b>c;c++)this.putBit(1==(1&a>>>b-c-1))},getLengthInBits:function(){return this.length},putBit:function(a){var b=Math.floor(this.length/8);this.buffer.length<=b&&this.buffer.push(0),a&&(this.buffer[b]|=128>>>this.length%8),this.length++}};var l=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]],o=function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){function g(a,b){var c=document.createElementNS("http://www.w3.org/2000/svg",a);for(var d in b)b.hasOwnProperty(d)&&c.setAttribute(d,b[d]);return c}var b=this._htOption,c=this._el,d=a.getModuleCount();Math.floor(b.width/d),Math.floor(b.height/d),this.clear();var h=g("svg",{viewBox:"0 0 "+String(d)+" "+String(d),width:"100%",height:"100%",fill:b.colorLight});h.setAttributeNS("http://www.w3.org/2000/xmlns/","xmlns:xlink","http://www.w3.org/1999/xlink"),c.appendChild(h),h.appendChild(g("rect",{fill:b.colorDark,width:"1",height:"1",id:"template"}));for(var i=0;d>i;i++)for(var j=0;d>j;j++)if(a.isDark(i,j)){var k=g("use",{x:String(i),y:String(j)});k.setAttributeNS("http://www.w3.org/1999/xlink","href","#template"),h.appendChild(k)}},a.prototype.clear=function(){for(;this._el.hasChildNodes();)this._el.removeChild(this._el.lastChild)},a}(),p="svg"===document.documentElement.tagName.toLowerCase(),q=p?o:m()?function(){function a(){this._elImage.src=this._elCanvas.toDataURL("image/png"),this._elImage.style.display="block",this._elCanvas.style.display="none"}function d(a,b){var c=this;if(c._fFail=b,c._fSuccess=a,null===c._bSupportDataURI){var d=document.createElement("img"),e=function(){c._bSupportDataURI=!1,c._fFail&&_fFail.call(c)},f=function(){c._bSupportDataURI=!0,c._fSuccess&&c._fSuccess.call(c)};return d.onabort=e,d.onerror=e,d.onload=f,d.src="data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==",void 0}c._bSupportDataURI===!0&&c._fSuccess?c._fSuccess.call(c):c._bSupportDataURI===!1&&c._fFail&&c._fFail.call(c)}if(this._android&&this._android<=2.1){var b=1/window.devicePixelRatio,c=CanvasRenderingContext2D.prototype.drawImage;CanvasRenderingContext2D.prototype.drawImage=function(a,d,e,f,g,h,i,j){if("nodeName"in a&&/img/i.test(a.nodeName))for(var l=arguments.length-1;l>=1;l--)arguments[l]=arguments[l]*b;else"undefined"==typeof j&&(arguments[1]*=b,arguments[2]*=b,arguments[3]*=b,arguments[4]*=b);c.apply(this,arguments)}}var e=function(a,b){this._bIsPainted=!1,this._android=n(),this._htOption=b,this._elCanvas=document.createElement("canvas"),this._elCanvas.width=b.width,this._elCanvas.height=b.height,a.appendChild(this._elCanvas),this._el=a,this._oContext=this._elCanvas.getContext("2d"),this._bIsPainted=!1,this._elImage=document.createElement("img"),this._elImage.style.display="none",this._el.appendChild(this._elImage),this._bSupportDataURI=null};return e.prototype.draw=function(a){var b=this._elImage,c=this._oContext,d=this._htOption,e=a.getModuleCount(),f=d.width/e,g=d.height/e,h=Math.round(f),i=Math.round(g);b.style.display="none",this.clear();for(var j=0;e>j;j++)for(var k=0;e>k;k++){var l=a.isDark(j,k),m=k*f,n=j*g;c.strokeStyle=l?d.colorDark:d.colorLight,c.lineWidth=1,c.fillStyle=l?d.colorDark:d.colorLight,c.fillRect(m,n,f,g),c.strokeRect(Math.floor(m)+.5,Math.floor(n)+.5,h,i),c.strokeRect(Math.ceil(m)-.5,Math.ceil(n)-.5,h,i)}this._bIsPainted=!0},e.prototype.makeImage=function(){this._bIsPainted&&d.call(this,a)},e.prototype.isPainted=function(){return this._bIsPainted},e.prototype.clear=function(){this._oContext.clearRect(0,0,this._elCanvas.width,this._elCanvas.height),this._bIsPainted=!1},e.prototype.round=function(a){return a?Math.floor(1e3*a)/1e3:a},e}():function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){for(var b=this._htOption,c=this._el,d=a.getModuleCount(),e=Math.floor(b.width/d),f=Math.floor(b.height/d),g=['<table style="border:0;border-collapse:collapse;">'],h=0;d>h;h++){g.push("<tr>");for(var i=0;d>i;i++)g.push('<td style="border:0;border-collapse:collapse;padding:0;margin:0;width:'+e+"px;height:"+f+"px;background-color:"+(a.isDark(h,i)?b.colorDark:b.colorLight)+';"></td>');g.push("</tr>")}g.push("</table>"),c.innerHTML=g.join("");var j=c.childNodes[0],k=(b.width-j.offsetWidth)/2,l=(b.height-j.offsetHeight)/2;k>0&&l>0&&(j.style.margin=l+"px "+k+"px")},a.prototype.clear=function(){this._el.innerHTML=""},a}();window.QRCode=function(a,b){if(this._htOption={width:256,height:256,typeNumber:4,colorDark:"#000000",colorLight:"#ffffff",correctLevel:d.H},"string"==typeof b&&(b={text:b}),b)for(var c in b)this._htOption[c]=b[c];"string"==typeof a&&(a=document.getElementById(a)),this._android=n(),this._el=a,this._oQRCode=null,this._oDrawing=new q(this._el,this._htOption),this._htOption.text&&this.makeCode(this._htOption.text)},window.QRCode.prototype.makeCode=function(a){this._oQRCode=new b(r(a,this._htOption.correctLevel),this._htOption.correctLevel),this._oQRCode.addData(a),this._oQRCode.make(),this._el.title=a,this._oDrawing.draw(this._oQRCode),this.makeImage()},window.QRCode.prototype.makeImage=function(){"function"==typeof this._oDrawing.makeImage&&(!this._android||this._android>=3)&&this._oDrawing.makeImage()},window.QRCode.prototype.clear=function(){this._oDrawing.clear()},window.QRCode.CorrectLevel=d}();
diff --git a/src/party/qrcodejs/qrcode.min.js b/src/party/qrcodejs/qrcode.min.js
new file mode 100644
index 0000000..d5f3ca8
--- /dev/null
+++ b/src/party/qrcodejs/qrcode.min.js
@@ -0,0 +1 @@
+var QRCode;!function(){function a(a){this.mode=c.MODE_8BIT_BYTE,this.data=a,this.parsedData=[];for(var b=[],d=0,e=this.data.length;e>d;d++){var f=this.data.charCodeAt(d);f>65536?(b[0]=240|(1835008&f)>>>18,b[1]=128|(258048&f)>>>12,b[2]=128|(4032&f)>>>6,b[3]=128|63&f):f>2048?(b[0]=224|(61440&f)>>>12,b[1]=128|(4032&f)>>>6,b[2]=128|63&f):f>128?(b[0]=192|(1984&f)>>>6,b[1]=128|63&f):b[0]=f,this.parsedData=this.parsedData.concat(b)}this.parsedData.length!=this.data.length&&(this.parsedData.unshift(191),this.parsedData.unshift(187),this.parsedData.unshift(239))}function b(a,b){this.typeNumber=a,this.errorCorrectLevel=b,this.modules=null,this.moduleCount=0,this.dataCache=null,this.dataList=[]}function i(a,b){if(void 0==a.length)throw new Error(a.length+"/"+b);for(var c=0;c<a.length&&0==a[c];)c++;this.num=new Array(a.length-c+b);for(var d=0;d<a.length-c;d++)this.num[d]=a[d+c]}function j(a,b){this.totalCount=a,this.dataCount=b}function k(){this.buffer=[],this.length=0}function m(){return"undefined"!=typeof CanvasRenderingContext2D}function n(){var a=!1,b=navigator.userAgent;return/android/i.test(b)&&(a=!0,aMat=b.toString().match(/android ([0-9]\.[0-9])/i),aMat&&aMat[1]&&(a=parseFloat(aMat[1]))),a}function r(a,b){for(var c=1,e=s(a),f=0,g=l.length;g>=f;f++){var h=0;switch(b){case d.L:h=l[f][0];break;case d.M:h=l[f][1];break;case d.Q:h=l[f][2];break;case d.H:h=l[f][3]}if(h>=e)break;c++}if(c>l.length)throw new Error("Too long data");return c}function s(a){var b=encodeURI(a).toString().replace(/\%[0-9a-fA-F]{2}/g,"a");return b.length+(b.length!=a?3:0)}a.prototype={getLength:function(){return this.parsedData.length},write:function(a){for(var b=0,c=this.parsedData.length;c>b;b++)a.put(this.parsedData[b],8)}},b.prototype={addData:function(b){var c=new a(b);this.dataList.push(c),this.dataCache=null},isDark:function(a,b){if(0>a||this.moduleCount<=a||0>b||this.moduleCount<=b)throw new Error(a+","+b);return this.modules[a][b]},getModuleCount:function(){return this.moduleCount},make:function(){this.makeImpl(!1,this.getBestMaskPattern())},makeImpl:function(a,c){this.moduleCount=4*this.typeNumber+17,this.modules=new Array(this.moduleCount);for(var d=0;d<this.moduleCount;d++){this.modules[d]=new Array(this.moduleCount);for(var e=0;e<this.moduleCount;e++)this.modules[d][e]=null}this.setupPositionProbePattern(0,0),this.setupPositionProbePattern(this.moduleCount-7,0),this.setupPositionProbePattern(0,this.moduleCount-7),this.setupPositionAdjustPattern(),this.setupTimingPattern(),this.setupTypeInfo(a,c),this.typeNumber>=7&&this.setupTypeNumber(a),null==this.dataCache&&(this.dataCache=b.createData(this.typeNumber,this.errorCorrectLevel,this.dataList)),this.mapData(this.dataCache,c)},setupPositionProbePattern:function(a,b){for(var c=-1;7>=c;c++)if(!(-1>=a+c||this.moduleCount<=a+c))for(var d=-1;7>=d;d++)-1>=b+d||this.moduleCount<=b+d||(this.modules[a+c][b+d]=c>=0&&6>=c&&(0==d||6==d)||d>=0&&6>=d&&(0==c||6==c)||c>=2&&4>=c&&d>=2&&4>=d?!0:!1)},getBestMaskPattern:function(){for(var a=0,b=0,c=0;8>c;c++){this.makeImpl(!0,c);var d=f.getLostPoint(this);(0==c||a>d)&&(a=d,b=c)}return b},createMovieClip:function(a,b,c){var d=a.createEmptyMovieClip(b,c),e=1;this.make();for(var f=0;f<this.modules.length;f++)for(var g=f*e,h=0;h<this.modules[f].length;h++){var i=h*e,j=this.modules[f][h];j&&(d.beginFill(0,100),d.moveTo(i,g),d.lineTo(i+e,g),d.lineTo(i+e,g+e),d.lineTo(i,g+e),d.endFill())}return d},setupTimingPattern:function(){for(var a=8;a<this.moduleCount-8;a++)null==this.modules[a][6]&&(this.modules[a][6]=0==a%2);for(var b=8;b<this.moduleCount-8;b++)null==this.modules[6][b]&&(this.modules[6][b]=0==b%2)},setupPositionAdjustPattern:function(){for(var a=f.getPatternPosition(this.typeNumber),b=0;b<a.length;b++)for(var c=0;c<a.length;c++){var d=a[b],e=a[c];if(null==this.modules[d][e])for(var g=-2;2>=g;g++)for(var h=-2;2>=h;h++)this.modules[d+g][e+h]=-2==g||2==g||-2==h||2==h||0==g&&0==h?!0:!1}},setupTypeNumber:function(a){for(var b=f.getBCHTypeNumber(this.typeNumber),c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[Math.floor(c/3)][c%3+this.moduleCount-8-3]=d}for(var c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[c%3+this.moduleCount-8-3][Math.floor(c/3)]=d}},setupTypeInfo:function(a,b){for(var c=this.errorCorrectLevel<<3|b,d=f.getBCHTypeInfo(c),e=0;15>e;e++){var g=!a&&1==(1&d>>e);6>e?this.modules[e][8]=g:8>e?this.modules[e+1][8]=g:this.modules[this.moduleCount-15+e][8]=g}for(var e=0;15>e;e++){var g=!a&&1==(1&d>>e);8>e?this.modules[8][this.moduleCount-e-1]=g:9>e?this.modules[8][15-e-1+1]=g:this.modules[8][15-e-1]=g}this.modules[this.moduleCount-8][8]=!a},mapData:function(a,b){for(var c=-1,d=this.moduleCount-1,e=7,g=0,h=this.moduleCount-1;h>0;h-=2)for(6==h&&h--;;){for(var i=0;2>i;i++)if(null==this.modules[d][h-i]){var j=!1;g<a.length&&(j=1==(1&a[g]>>>e));var k=f.getMask(b,d,h-i);k&&(j=!j),this.modules[d][h-i]=j,e--,-1==e&&(g++,e=7)}if(d+=c,0>d||this.moduleCount<=d){d-=c,c=-c;break}}}},b.PAD0=236,b.PAD1=17,b.createData=function(a,c,d){for(var e=j.getRSBlocks(a,c),g=new k,h=0;h<d.length;h++){var i=d[h];g.put(i.mode,4),g.put(i.getLength(),f.getLengthInBits(i.mode,a)),i.write(g)}for(var l=0,h=0;h<e.length;h++)l+=e[h].dataCount;if(g.getLengthInBits()>8*l)throw new Error("code length overflow. ("+g.getLengthInBits()+">"+8*l+")");for(g.getLengthInBits()+4<=8*l&&g.put(0,4);0!=g.getLengthInBits()%8;)g.putBit(!1);for(;;){if(g.getLengthInBits()>=8*l)break;if(g.put(b.PAD0,8),g.getLengthInBits()>=8*l)break;g.put(b.PAD1,8)}return b.createBytes(g,e)},b.createBytes=function(a,b){for(var c=0,d=0,e=0,g=new Array(b.length),h=new Array(b.length),j=0;j<b.length;j++){var k=b[j].dataCount,l=b[j].totalCount-k;d=Math.max(d,k),e=Math.max(e,l),g[j]=new Array(k);for(var m=0;m<g[j].length;m++)g[j][m]=255&a.buffer[m+c];c+=k;var n=f.getErrorCorrectPolynomial(l),o=new i(g[j],n.getLength()-1),p=o.mod(n);h[j]=new Array(n.getLength()-1);for(var m=0;m<h[j].length;m++){var q=m+p.getLength()-h[j].length;h[j][m]=q>=0?p.get(q):0}}for(var r=0,m=0;m<b.length;m++)r+=b[m].totalCount;for(var s=new Array(r),t=0,m=0;d>m;m++)for(var j=0;j<b.length;j++)m<g[j].length&&(s[t++]=g[j][m]);for(var m=0;e>m;m++)for(var j=0;j<b.length;j++)m<h[j].length&&(s[t++]=h[j][m]);return s};for(var c={MODE_NUMBER:1,MODE_ALPHA_NUM:2,MODE_8BIT_BYTE:4,MODE_KANJI:8},d={L:1,M:0,Q:3,H:2},e={PATTERN000:0,PATTERN001:1,PATTERN010:2,PATTERN011:3,PATTERN100:4,PATTERN101:5,PATTERN110:6,PATTERN111:7},f={PATTERN_POSITION_TABLE:[[],[6,18],[6,22],[6,26],[6,30],[6,34],[6,22,38],[6,24,42],[6,26,46],[6,28,50],[6,30,54],[6,32,58],[6,34,62],[6,26,46,66],[6,26,48,70],[6,26,50,74],[6,30,54,78],[6,30,56,82],[6,30,58,86],[6,34,62,90],[6,28,50,72,94],[6,26,50,74,98],[6,30,54,78,102],[6,28,54,80,106],[6,32,58,84,110],[6,30,58,86,114],[6,34,62,90,118],[6,26,50,74,98,122],[6,30,54,78,102,126],[6,26,52,78,104,130],[6,30,56,82,108,134],[6,34,60,86,112,138],[6,30,58,86,114,142],[6,34,62,90,118,146],[6,30,54,78,102,126,150],[6,24,50,76,102,128,154],[6,28,54,80,106,132,158],[6,32,58,84,110,136,162],[6,26,54,82,110,138,166],[6,30,58,86,114,142,170]],G15:1335,G18:7973,G15_MASK:21522,getBCHTypeInfo:function(a){for(var b=a<<10;f.getBCHDigit(b)-f.getBCHDigit(f.G15)>=0;)b^=f.G15<<f.getBCHDigit(b)-f.getBCHDigit(f.G15);return(a<<10|b)^f.G15_MASK},getBCHTypeNumber:function(a){for(var b=a<<12;f.getBCHDigit(b)-f.getBCHDigit(f.G18)>=0;)b^=f.G18<<f.getBCHDigit(b)-f.getBCHDigit(f.G18);return a<<12|b},getBCHDigit:function(a){for(var b=0;0!=a;)b++,a>>>=1;return b},getPatternPosition:function(a){return f.PATTERN_POSITION_TABLE[a-1]},getMask:function(a,b,c){switch(a){case e.PATTERN000:return 0==(b+c)%2;case e.PATTERN001:return 0==b%2;case e.PATTERN010:return 0==c%3;case e.PATTERN011:return 0==(b+c)%3;case e.PATTERN100:return 0==(Math.floor(b/2)+Math.floor(c/3))%2;case e.PATTERN101:return 0==b*c%2+b*c%3;case e.PATTERN110:return 0==(b*c%2+b*c%3)%2;case e.PATTERN111:return 0==(b*c%3+(b+c)%2)%2;default:throw new Error("bad maskPattern:"+a)}},getErrorCorrectPolynomial:function(a){for(var b=new i([1],0),c=0;a>c;c++)b=b.multiply(new i([1,g.gexp(c)],0));return b},getLengthInBits:function(a,b){if(b>=1&&10>b)switch(a){case c.MODE_NUMBER:return 10;case c.MODE_ALPHA_NUM:return 9;case c.MODE_8BIT_BYTE:return 8;case c.MODE_KANJI:return 8;default:throw new Error("mode:"+a)}else if(27>b)switch(a){case c.MODE_NUMBER:return 12;case c.MODE_ALPHA_NUM:return 11;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 10;default:throw new Error("mode:"+a)}else{if(!(41>b))throw new Error("type:"+b);switch(a){case c.MODE_NUMBER:return 14;case c.MODE_ALPHA_NUM:return 13;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 12;default:throw new Error("mode:"+a)}}},getLostPoint:function(a){for(var b=a.getModuleCount(),c=0,d=0;b>d;d++)for(var e=0;b>e;e++){for(var f=0,g=a.isDark(d,e),h=-1;1>=h;h++)if(!(0>d+h||d+h>=b))for(var i=-1;1>=i;i++)0>e+i||e+i>=b||(0!=h||0!=i)&&g==a.isDark(d+h,e+i)&&f++;f>5&&(c+=3+f-5)}for(var d=0;b-1>d;d++)for(var e=0;b-1>e;e++){var j=0;a.isDark(d,e)&&j++,a.isDark(d+1,e)&&j++,a.isDark(d,e+1)&&j++,a.isDark(d+1,e+1)&&j++,(0==j||4==j)&&(c+=3)}for(var d=0;b>d;d++)for(var e=0;b-6>e;e++)a.isDark(d,e)&&!a.isDark(d,e+1)&&a.isDark(d,e+2)&&a.isDark(d,e+3)&&a.isDark(d,e+4)&&!a.isDark(d,e+5)&&a.isDark(d,e+6)&&(c+=40);for(var e=0;b>e;e++)for(var d=0;b-6>d;d++)a.isDark(d,e)&&!a.isDark(d+1,e)&&a.isDark(d+2,e)&&a.isDark(d+3,e)&&a.isDark(d+4,e)&&!a.isDark(d+5,e)&&a.isDark(d+6,e)&&(c+=40);for(var k=0,e=0;b>e;e++)for(var d=0;b>d;d++)a.isDark(d,e)&&k++;var l=Math.abs(100*k/b/b-50)/5;return c+=10*l}},g={glog:function(a){if(1>a)throw new Error("glog("+a+")");return g.LOG_TABLE[a]},gexp:function(a){for(;0>a;)a+=255;for(;a>=256;)a-=255;return g.EXP_TABLE[a]},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)},h=0;8>h;h++)g.EXP_TABLE[h]=1<<h;for(var h=8;256>h;h++)g.EXP_TABLE[h]=g.EXP_TABLE[h-4]^g.EXP_TABLE[h-5]^g.EXP_TABLE[h-6]^g.EXP_TABLE[h-8];for(var h=0;255>h;h++)g.LOG_TABLE[g.EXP_TABLE[h]]=h;i.prototype={get:function(a){return this.num[a]},getLength:function(){return this.num.length},multiply:function(a){for(var b=new Array(this.getLength()+a.getLength()-1),c=0;c<this.getLength();c++)for(var d=0;d<a.getLength();d++)b[c+d]^=g.gexp(g.glog(this.get(c))+g.glog(a.get(d)));return new i(b,0)},mod:function(a){if(this.getLength()-a.getLength()<0)return this;for(var b=g.glog(this.get(0))-g.glog(a.get(0)),c=new Array(this.getLength()),d=0;d<this.getLength();d++)c[d]=this.get(d);for(var d=0;d<a.getLength();d++)c[d]^=g.gexp(g.glog(a.get(d))+b);return new i(c,0).mod(a)}},j.RS_BLOCK_TABLE=[[1,26,19],[1,26,16],[1,26,13],[1,26,9],[1,44,34],[1,44,28],[1,44,22],[1,44,16],[1,70,55],[1,70,44],[2,35,17],[2,35,13],[1,100,80],[2,50,32],[2,50,24],[4,25,9],[1,134,108],[2,67,43],[2,33,15,2,34,16],[2,33,11,2,34,12],[2,86,68],[4,43,27],[4,43,19],[4,43,15],[2,98,78],[4,49,31],[2,32,14,4,33,15],[4,39,13,1,40,14],[2,121,97],[2,60,38,2,61,39],[4,40,18,2,41,19],[4,40,14,2,41,15],[2,146,116],[3,58,36,2,59,37],[4,36,16,4,37,17],[4,36,12,4,37,13],[2,86,68,2,87,69],[4,69,43,1,70,44],[6,43,19,2,44,20],[6,43,15,2,44,16],[4,101,81],[1,80,50,4,81,51],[4,50,22,4,51,23],[3,36,12,8,37,13],[2,116,92,2,117,93],[6,58,36,2,59,37],[4,46,20,6,47,21],[7,42,14,4,43,15],[4,133,107],[8,59,37,1,60,38],[8,44,20,4,45,21],[12,33,11,4,34,12],[3,145,115,1,146,116],[4,64,40,5,65,41],[11,36,16,5,37,17],[11,36,12,5,37,13],[5,109,87,1,110,88],[5,65,41,5,66,42],[5,54,24,7,55,25],[11,36,12],[5,122,98,1,123,99],[7,73,45,3,74,46],[15,43,19,2,44,20],[3,45,15,13,46,16],[1,135,107,5,136,108],[10,74,46,1,75,47],[1,50,22,15,51,23],[2,42,14,17,43,15],[5,150,120,1,151,121],[9,69,43,4,70,44],[17,50,22,1,51,23],[2,42,14,19,43,15],[3,141,113,4,142,114],[3,70,44,11,71,45],[17,47,21,4,48,22],[9,39,13,16,40,14],[3,135,107,5,136,108],[3,67,41,13,68,42],[15,54,24,5,55,25],[15,43,15,10,44,16],[4,144,116,4,145,117],[17,68,42],[17,50,22,6,51,23],[19,46,16,6,47,17],[2,139,111,7,140,112],[17,74,46],[7,54,24,16,55,25],[34,37,13],[4,151,121,5,152,122],[4,75,47,14,76,48],[11,54,24,14,55,25],[16,45,15,14,46,16],[6,147,117,4,148,118],[6,73,45,14,74,46],[11,54,24,16,55,25],[30,46,16,2,47,17],[8,132,106,4,133,107],[8,75,47,13,76,48],[7,54,24,22,55,25],[22,45,15,13,46,16],[10,142,114,2,143,115],[19,74,46,4,75,47],[28,50,22,6,51,23],[33,46,16,4,47,17],[8,152,122,4,153,123],[22,73,45,3,74,46],[8,53,23,26,54,24],[12,45,15,28,46,16],[3,147,117,10,148,118],[3,73,45,23,74,46],[4,54,24,31,55,25],[11,45,15,31,46,16],[7,146,116,7,147,117],[21,73,45,7,74,46],[1,53,23,37,54,24],[19,45,15,26,46,16],[5,145,115,10,146,116],[19,75,47,10,76,48],[15,54,24,25,55,25],[23,45,15,25,46,16],[13,145,115,3,146,116],[2,74,46,29,75,47],[42,54,24,1,55,25],[23,45,15,28,46,16],[17,145,115],[10,74,46,23,75,47],[10,54,24,35,55,25],[19,45,15,35,46,16],[17,145,115,1,146,116],[14,74,46,21,75,47],[29,54,24,19,55,25],[11,45,15,46,46,16],[13,145,115,6,146,116],[14,74,46,23,75,47],[44,54,24,7,55,25],[59,46,16,1,47,17],[12,151,121,7,152,122],[12,75,47,26,76,48],[39,54,24,14,55,25],[22,45,15,41,46,16],[6,151,121,14,152,122],[6,75,47,34,76,48],[46,54,24,10,55,25],[2,45,15,64,46,16],[17,152,122,4,153,123],[29,74,46,14,75,47],[49,54,24,10,55,25],[24,45,15,46,46,16],[4,152,122,18,153,123],[13,74,46,32,75,47],[48,54,24,14,55,25],[42,45,15,32,46,16],[20,147,117,4,148,118],[40,75,47,7,76,48],[43,54,24,22,55,25],[10,45,15,67,46,16],[19,148,118,6,149,119],[18,75,47,31,76,48],[34,54,24,34,55,25],[20,45,15,61,46,16]],j.getRSBlocks=function(a,b){var c=j.getRsBlockTable(a,b);if(void 0==c)throw new Error("bad rs block @ typeNumber:"+a+"/errorCorrectLevel:"+b);for(var d=c.length/3,e=[],f=0;d>f;f++)for(var g=c[3*f+0],h=c[3*f+1],i=c[3*f+2],k=0;g>k;k++)e.push(new j(h,i));return e},j.getRsBlockTable=function(a,b){switch(b){case d.L:return j.RS_BLOCK_TABLE[4*(a-1)+0];case d.M:return j.RS_BLOCK_TABLE[4*(a-1)+1];case d.Q:return j.RS_BLOCK_TABLE[4*(a-1)+2];case d.H:return j.RS_BLOCK_TABLE[4*(a-1)+3];default:return void 0}},k.prototype={get:function(a){var b=Math.floor(a/8);return 1==(1&this.buffer[b]>>>7-a%8)},put:function(a,b){for(var c=0;b>c;c++)this.putBit(1==(1&a>>>b-c-1))},getLengthInBits:function(){return this.length},putBit:function(a){var b=Math.floor(this.length/8);this.buffer.length<=b&&this.buffer.push(0),a&&(this.buffer[b]|=128>>>this.length%8),this.length++}};var l=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]],o=function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){function g(a,b){var c=document.createElementNS("http://www.w3.org/2000/svg",a);for(var d in b)b.hasOwnProperty(d)&&c.setAttribute(d,b[d]);return c}var b=this._htOption,c=this._el,d=a.getModuleCount();Math.floor(b.width/d),Math.floor(b.height/d),this.clear();var h=g("svg",{viewBox:"0 0 "+String(d)+" "+String(d),width:"100%",height:"100%",fill:b.colorLight});h.setAttributeNS("http://www.w3.org/2000/xmlns/","xmlns:xlink","http://www.w3.org/1999/xlink"),c.appendChild(h),h.appendChild(g("rect",{fill:b.colorDark,width:"1",height:"1",id:"template"}));for(var i=0;d>i;i++)for(var j=0;d>j;j++)if(a.isDark(i,j)){var k=g("use",{x:String(i),y:String(j)});k.setAttributeNS("http://www.w3.org/1999/xlink","href","#template"),h.appendChild(k)}},a.prototype.clear=function(){for(;this._el.hasChildNodes();)this._el.removeChild(this._el.lastChild)},a}(),p="svg"===document.documentElement.tagName.toLowerCase(),q=p?o:m()?function(){function a(){this._elImage.src=this._elCanvas.toDataURL("image/png"),this._elImage.style.display="block",this._elCanvas.style.display="none"}function d(a,b){var c=this;if(c._fFail=b,c._fSuccess=a,null===c._bSupportDataURI){var d=document.createElement("img"),e=function(){c._bSupportDataURI=!1,c._fFail&&_fFail.call(c)},f=function(){c._bSupportDataURI=!0,c._fSuccess&&c._fSuccess.call(c)};return d.onabort=e,d.onerror=e,d.onload=f,d.src="data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==",void 0}c._bSupportDataURI===!0&&c._fSuccess?c._fSuccess.call(c):c._bSupportDataURI===!1&&c._fFail&&c._fFail.call(c)}if(this._android&&this._android<=2.1){var b=1/window.devicePixelRatio,c=CanvasRenderingContext2D.prototype.drawImage;CanvasRenderingContext2D.prototype.drawImage=function(a,d,e,f,g,h,i,j){if("nodeName"in a&&/img/i.test(a.nodeName))for(var l=arguments.length-1;l>=1;l--)arguments[l]=arguments[l]*b;else"undefined"==typeof j&&(arguments[1]*=b,arguments[2]*=b,arguments[3]*=b,arguments[4]*=b);c.apply(this,arguments)}}var e=function(a,b){this._bIsPainted=!1,this._android=n(),this._htOption=b,this._elCanvas=document.createElement("canvas"),this._elCanvas.width=b.width,this._elCanvas.height=b.height,a.appendChild(this._elCanvas),this._el=a,this._oContext=this._elCanvas.getContext("2d"),this._bIsPainted=!1,this._elImage=document.createElement("img"),this._elImage.style.display="none",this._el.appendChild(this._elImage),this._bSupportDataURI=null};return e.prototype.draw=function(a){var b=this._elImage,c=this._oContext,d=this._htOption,e=a.getModuleCount(),f=d.width/e,g=d.height/e,h=Math.round(f),i=Math.round(g);b.style.display="none",this.clear();for(var j=0;e>j;j++)for(var k=0;e>k;k++){var l=a.isDark(j,k),m=k*f,n=j*g;c.strokeStyle=l?d.colorDark:d.colorLight,c.lineWidth=1,c.fillStyle=l?d.colorDark:d.colorLight,c.fillRect(m,n,f,g),c.strokeRect(Math.floor(m)+.5,Math.floor(n)+.5,h,i),c.strokeRect(Math.ceil(m)-.5,Math.ceil(n)-.5,h,i)}this._bIsPainted=!0},e.prototype.makeImage=function(){this._bIsPainted&&d.call(this,a)},e.prototype.isPainted=function(){return this._bIsPainted},e.prototype.clear=function(){this._oContext.clearRect(0,0,this._elCanvas.width,this._elCanvas.height),this._bIsPainted=!1},e.prototype.round=function(a){return a?Math.floor(1e3*a)/1e3:a},e}():function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){for(var b=this._htOption,c=this._el,d=a.getModuleCount(),e=Math.floor(b.width/d),f=Math.floor(b.height/d),g=['<table style="border:0;border-collapse:collapse;">'],h=0;d>h;h++){g.push("<tr>");for(var i=0;d>i;i++)g.push('<td style="border:0;border-collapse:collapse;padding:0;margin:0;width:'+e+"px;height:"+f+"px;background-color:"+(a.isDark(h,i)?b.colorDark:b.colorLight)+';"></td>');g.push("</tr>")}g.push("</table>"),c.innerHTML=g.join("");var j=c.childNodes[0],k=(b.width-j.offsetWidth)/2,l=(b.height-j.offsetHeight)/2;k>0&&l>0&&(j.style.margin=l+"px "+k+"px")},a.prototype.clear=function(){this._el.innerHTML=""},a}();QRCode=function(a,b){if(this._htOption={width:256,height:256,typeNumber:4,colorDark:"#000000",colorLight:"#ffffff",correctLevel:d.H},"string"==typeof b&&(b={text:b}),b)for(var c in b)this._htOption[c]=b[c];"string"==typeof a&&(a=document.getElementById(a)),this._android=n(),this._el=a,this._oQRCode=null,this._oDrawing=new q(this._el,this._htOption),this._htOption.text&&this.makeCode(this._htOption.text)},QRCode.prototype.makeCode=function(a){this._oQRCode=new b(r(a,this._htOption.correctLevel),this._htOption.correctLevel),this._oQRCode.addData(a),this._oQRCode.make(),this._el.title=a,this._oDrawing.draw(this._oQRCode),this.makeImage()},QRCode.prototype.makeImage=function(){"function"==typeof this._oDrawing.makeImage&&(!this._android||this._android>=3)&&this._oDrawing.makeImage()},QRCode.prototype.clear=function(){this._oDrawing.clear()},QRCode.CorrectLevel=d}();
diff --git a/src/poems.moon b/src/poems.moon
new file mode 100644
index 0000000..7da5d21
--- /dev/null
+++ b/src/poems.moon
@@ -0,0 +1,348 @@
+poems = {
+ {
+ text: "Three candles flicker in the moonlight, the little winged things burn up mid flight."
+ hints: {
+ "a flying bee, it makes wax",
+ "a moon is that sits overhead",
+ "three of a thing"
+ },
+ },
+ {
+ text: "Along the south shore where fierce waves balk, the shadows lengthen across the sand. The sun sinks beneath the loch and our great work lies close at hand."
+ hints: {
+ "a sunset over a lake",
+ "rough waters lapping the beach",
+ "labor on the beach"
+ },
+ },
+ {
+ text: "We seek an ancient tattered pennon that waves, over rubble that marks antediluvian graves."
+ hints: {
+ "military in ruin",
+ "old architecture",
+ "weathered gravestones"
+ },
+ },
+ {
+ text: "Strange plants grow where the gardener naps, their roots climb in through tiny cracks."
+ hints: {
+ "unusual foliage",
+ "a Sleeping groundskeeper",
+ "creeping plants"
+ },
+ },
+ {
+ text: "This lake knows what the stars forgot. It disarms the snare where others were caught."
+ hints: {
+ "water that remembers",
+ "celestial amnesia",
+ "avoiding a trap"
+ },
+ },
+ {
+ text: "This oaken hall whispers low and deep, the secrets that confessors keep."
+ hints: {
+ "wood that speaks",
+ "murmured revelations",
+ "the sacrament of confession"
+ },
+ },
+ {
+ text: "In a valley where pallid fog creeps thick and vast, we pursue the scholars of ages past."
+ hints: {
+ "mist obscuring view",
+ "ancient research",
+ "the pursuit of knowledge"
+ },
+ },
+ {
+ text: "A wire pushed through a lock to open a door, and a knife pushed through my skin to open more."
+ hints: {
+ "lockpicking",
+ "threshold and entry",
+ "piercing that transforms"
+ }
+ },
+ {
+ text: "When bells toll from a distant spire, we begin the pilgrimage for our heart's desire."
+ hints: {
+ "ringing in the distance",
+ "a tall church tower",
+ "a journey with companions"
+ },
+ },
+ {
+ text: "What dwells in the glassy watered reef, what hides in mirrors and eats belief."
+ hints: {
+ "something beneath",
+ "what lives in reflections",
+ "dispelling conviction"
+ },
+ },
+ {
+ text: "The cobblestones tremble in rousing air, beneath the boulevard where gaslights flare."
+ hints: {
+ "a darkened street",
+ "stones that hold memory",
+ "excited atmosphere"
+ },
+ },
+ {
+ text: "The watchman's lantern casts a pallid gleam, where marble statues run through a terrible dream."
+ hints: {
+ "a light source",
+ "dreaming sculptures",
+ "a hall's custodian"
+ },
+ },
+ {
+ text: "The alders inscribed what none dare say, in cramped attics where moth-eaten scrolls decay."
+ hints: {
+ "rotting documents",
+ "forbidden writing",
+ "a small loft"
+ },
+ },
+ {
+ text: "Where ivy tendrils embrace the arabesque gate, the gardener's shears lie rusted by fate."
+ hints: {
+ "overgrown entrance",
+ "abandoned tools",
+ "oxidized implements"
+ },
+ },
+ {
+ text: "The carrion birds circle the blackened spire, as vesper bells announce the choir."
+ hints: {
+ "scavenging creatures",
+ "evening prayer",
+ "a darkened tower"
+ },
+ },
+ {
+ text: "The architect drew unhallowed planes, in vaulted chambers where torchlight wanes."
+ hints: {
+ "fading illumination",
+ "corrupted geometry ",
+ "tall rooms"
+ },
+ },
+ {
+ text: "The hoarfrost clings to windowpanes and spreads, where ephemeral fingers trace the words unsaid."
+ hints: {
+ "frozen crystal",
+ "old glass",
+ "ghostly writing"
+ },
+ },
+ {
+ text: "We shall glimpse what should not be seen, upon the heath where standing stones convene."
+ hints: {
+ "ancient monument",
+ "moorland expanse",
+ "forbidden sight"
+ },
+ },
+ {
+ text: "The apostate guards his musty tome, in labyrinthine halls that honeycomb."
+ hints: {
+ "an aged book",
+ "a maze of passages",
+ "a religious traitor"
+ },
+ },
+ {
+ text: "The portcullis descends with a rusted groan, sealing fast what we cannot postpone."
+ hints: {
+ "a heavy gate falling",
+ "corroded metal",
+ "getting trapped"
+ },
+ },
+ {
+ text: "The frescoes peel and the shadows fade, in cloistered halls where votaries once prayed."
+ hints: {
+ "enclosed passages",
+ "religious devotees",
+ "crumbling wall paintings"
+ },
+ },
+ {
+ text: "The campanile stands against ashy sky, we ring the bell and await reply."
+ hints: {
+ "a lone bell tower",
+ "heavy grey clouds",
+ "expecting a message"
+ },
+ },
+ {
+ text: "Beneath the causeway where rose waters flow, the ferryman rows to lands below."
+ hints: {
+ "raised pathway",
+ "red current",
+ "descent by boat"
+ },
+ },
+ {
+ text: "The apothecary's vials gleam and luminesce, distilling essences of noblesse."
+ hints: {
+ "glowing beakers",
+ "shining potions",
+ "familial honor"
+ },
+ },
+ {
+ text: "The cartographer's quill trembles as it charts, the borderlands where reason departs."
+ hints: {
+ "mapmaking",
+ "the edges of the known",
+ "whats beyond the edge of the map"
+ },
+ },
+ {
+ text: "In deep ossuaries where relics rest, the sexton points to what we love best."
+ hints: {
+ "bone chambers",
+ "sacred remains",
+ "a graveyard keeper"
+ },
+ },
+ {
+ text: "Where wisteria droops from crumbling eaves, the mourner counts and the actuary grieves."
+ hints: {
+ "hanging flowers",
+ "decaying roofline",
+ "grief tallied"
+ },
+ },
+ {
+ text: "Through colonnades of alabaster stone, processional shadows march to atone."
+ hints: {
+ "pale pillars",
+ "formal darkness",
+ "spectral parade"
+ },
+ },
+ {
+ text: "The reliquary gleams with purple and pall, enshrining whispers of a silenced cabal."
+ hints: {
+ "a sacred container",
+ "expensive colors",
+ "ancient murmurs"
+ },
+ },
+ {
+ text: "In dusty galleries where portraits stare, the subjects step from frame to air."
+ hints: {
+ "hall of paintings",
+ "watching eyes",
+ "art escaping"
+ },
+ },
+ {
+ text: "The mendicant kneels at crossroads there, offering prayers to empty air."
+ hints: {
+ "a beggars devotion",
+ "intersection of paths",
+ "unanswered plea"
+ },
+ },
+ {
+ text: "Where amaranth blooms when the skies are gray, the groundskeeper has lost his way."
+ hints: {
+ "unfading flowers",
+ "colorless skies",
+ "wandering caretaker"
+ },
+ },
+ {
+ text: "The sarcophagus lid shifts with grinding tone, revealing naught of polished bone."
+ hints: {
+ "stone coffin opening",
+ "grating movement",
+ "missing remains"
+ },
+ },
+ {
+ text: "In chancels dim where incense curls, the thurible swings as smoke unfurls."
+ hints: {
+ "an altar space",
+ "rising smoke",
+ "a burning censer"
+ },
+ },
+ {
+ text: "The oubliette waits with patient maw, forgotten by all but ancient law."
+ hints: {
+ "dungeon pit",
+ "hungry opening",
+ "archaic justice"
+ },
+ },
+ {
+ text: "The indentations reveal what the scribes erased, truths beneath truths carefully placed."
+ hints: {
+ "overwritten text",
+ "hidden words",
+ "layered secrets"
+ },
+ },
+ {
+ text: "The cenotaph stands for those not found, their bones scattered on unknown ground."
+ hints: {
+ "an empty tomb",
+ "the missing dead",
+ "a far off land"
+ },
+ },
+ {
+ text: "Through clerestory windows wan light falls, illuminating naught but barren walls."
+ hints: {
+ "high church windows",
+ "feeble radiance",
+ "empty surfaces"
+ },
+ },
+ {
+ text: "The dovecote stands though the doves have fled, roosting now where angels tread."
+ hints: {
+ "bird shelter",
+ "abandoned dwelling",
+ "dead avians"
+ },
+ },
+ {
+ text: "Specimens watch as centuries pass, in vitrines sealed with leaden glass."
+ hints: {
+ "display cases",
+ "preserved creatures",
+ "observing time"
+ },
+ },
+ {
+ text: "The caryatids bear their burden still, as the temple crumbles to dust and nil."
+ hints: {
+ "sculpted maidens",
+ "supporting columns",
+ "a sacred ruin"
+ },
+ },
+ {
+ text: "Where foxglove sways by lichened stone, the healer grinds what we have grown, beneath the skin, beneath the bone."
+ hints: {
+ "poisonous flowers",
+ "a moss-covered rock",
+ "grim remedies"
+ },
+ },
+ {
+ text: "In baptisteries where fonts run dry, the faithful await their final reply."
+ hints: {
+ "christening halls",
+ "empty basins",
+ "unanswered prayers"
+ },
+ },
+}
+
+poems
diff --git a/src/prefab/cabin.moon b/src/prefab/cabin.moon
new file mode 100644
index 0000000..1358c5a
--- /dev/null
+++ b/src/prefab/cabin.moon
@@ -0,0 +1,79 @@
+world = require("world")
+sprites = require("world.sprites")
+ecs = require("ecs")
+
+class CabinGraphicsComponent extends world.GraphicsComponent
+ --
+ -- #####
+ -- # #
+ -- ##/##
+ -- 3x3 floor, 1x3 * 3 walls, 1x1 * 2 door side walls, 1x1 door
+ buf_size: () =>
+ (6*9) + (6*9) + (6* 2) + (6 * 1)
+
+ populate_buf: (geom_view, normal_view, offset) =>
+ z1 = -0.01
+ z2 = -1
+ iuv = sprites.wall_inside_normal
+ ouv = sprites.wall_outside_normal
+ fuv = sprites.floor_normal
+ h1 = sprites.help_1
+ h2 = sprites.help_2
+ h3 = sprites.help_3
+ h1_r = false
+ h2_r = false
+ h3_r = false
+ wall = (geom, uv, offset, start, finish, texture) ->
+ geom[offset + 0] = vec3(start.x, start.y, z1)
+ geom[offset + 1] = vec3(start.x, start.y, z2)
+ geom[offset + 2] = vec3(finish.x, finish.y, z2)
+ geom[offset + 3] = vec3(finish.x, finish.y, z2)
+ geom[offset + 4] = vec3(finish.x, finish.y, z1)
+ geom[offset + 5] = vec3(start.x, start.y, z1)
+ uv[offset+0] = vec2(texture.s1,texture.t1)
+ uv[offset+1] = vec2(texture.s1,texture.t2)
+ uv[offset+2] = vec2(texture.s2,texture.t2)
+ uv[offset+3] = vec2(texture.s2,texture.t2)
+ uv[offset+4] = vec2(texture.s2,texture.t1)
+ uv[offset+5] = vec2(texture.s1,texture.t1)
+
+ floor = (geom, uv, offset, start, finish) ->
+ tuv = fuv
+ if not h1_r
+ tuv = h1
+ h1_r = true
+ elseif not h2_r
+ tuv = h2
+ h2_r = true
+ elseif not h3_r
+ tuv = h3
+ h3_r = true
+ geom[offset + 0] = vec3(start.x,start.y,z1)
+ geom[offset + 1] = vec3(start.x,finish.y,z1)
+ geom[offset + 2] = vec3(finish.x,finish.y,z1)
+ geom[offset + 3] = vec3(finish.x,finish.y,z1)
+ geom[offset + 4] = vec3(finish.x,start.y,z1)
+ geom[offset + 5] = vec3(start.x,start.y,z1)
+ normal_view[offset + 0] = vec2(tuv.s1, tuv.t1)
+ normal_view[offset + 1] = vec2(tuv.s1, tuv.t2)
+ normal_view[offset + 2] = vec2(tuv.s2, tuv.t2)
+ normal_view[offset + 3] = vec2(tuv.s2, tuv.t2)
+ normal_view[offset + 4] = vec2(tuv.s2, tuv.t1)
+ normal_view[offset + 5] = vec2(tuv.s1, tuv.t1)
+
+ --left wall
+ j = 1
+ wall(geom_view, normal_view, j, vec2(-2,-2),vec2(-2,0), sprites.wall_inside_normal)
+ j += 6
+ for floorx = 1,3
+ for floory = 1,3
+ floor(geom_view, normal_view, j, vec2(-2 + (2*floorx), -2 + (2*floory)), vec2(2*floorx, 2*floory))
+ j += 6
+
+
+
+cabin = ecs.Entity("cabin",{
+ graphic: CabinGraphicsComponent("graphic")
+})
+
+cabin
diff --git a/src/prefab/hall.moon b/src/prefab/hall.moon
new file mode 100644
index 0000000..59dbea0
--- /dev/null
+++ b/src/prefab/hall.moon
@@ -0,0 +1,21 @@
+util = require("util")
+
+-- Halls run from one point to another, the start and end points are in the
+-- middle of the hallway.
+-- "floor_gen" can be a string (the sprites texture to use)a
+-- or a function (passed the "Hall" object to generate the texture for that segment.
+class Hall
+ new: (tbl) =>
+ util.typecheck(tbl,
+ "startx", "number",
+ "starty", "number",
+ "endx", "number",
+ "endy", "number",
+ "width", "number"
+ )
+ assert(tbl.floor_gen, "Hall requires a 'floor_gen' attribute")
+ if type(tbl.floor_gen) == "function"
+ @floor_gen = tbl.floor_gen
+ elseif type(tbl.floor_gen) == "string"
+ @floor_gen = () =>
+ tbl.floor_gen
diff --git a/src/prefab/lobby.moon b/src/prefab/lobby.moon
new file mode 100644
index 0000000..a560469
--- /dev/null
+++ b/src/prefab/lobby.moon
@@ -0,0 +1,112 @@
+sprites = require("sprites")
+GraphicsComponent = require("ecs.graphics")
+log = require("log")
+
+sd = sprites.floor
+w1 = sprites.wall
+
+floor = (x, y) ->
+ r = {
+ --floor
+ vec3(x,y,0),
+ vec3(x+1,y,0),
+ vec3(x+1,y-1,0),
+ vec3(x+1,y-1,0),
+ vec3(x,y-1,0),
+ vec3(x,y,0)
+ }
+ r
+
+floor_uv = (x, y) ->
+ r = {
+ vec4(sd.s1,sd.t1,1,1),
+ vec4(sd.s2,sd.t1,1,1),
+ vec4(sd.s2,sd.t2,1,1),
+ vec4(sd.s2,sd.t2,1,1),
+ vec4(sd.s1,sd.t2,1,1),
+ vec4(sd.s1,sd.t1,1,1)
+ }
+ r
+
+
+left_wall = (x,y) ->
+ r = {
+ -- Left wall
+ vec3(x+1,y,1),
+ vec3(x+1,y,0),
+ vec3(x+1,y-1,0),
+ vec3(x+1,y-1,0),
+ vec3(x+1,y-1,1),
+ vec3(x+1,y,1)
+ }
+ r
+
+left_wall_uv = (x,y) ->
+ r = {
+ vec4(w1.s1,w1.t1,1,1),
+ vec4(w1.s1,w1.t2,1,1),
+ vec4(w1.s2,w1.t2,1,1),
+ vec4(w1.s2,w1.t2,1,1),
+ vec4(w1.s2,w1.t1,1,1),
+ vec4(w1.s1,w1.t1,1,1)
+ }
+ r
+
+right_wall = (x,y) ->
+ r = {
+ --Right wall
+ vec3(x,y,0),
+ vec3(x,y,1),
+ vec3(x,y-1,1),
+ vec3(x,y-1,1),
+ vec3(x,y-1,0),
+ vec3(x,y,0)
+ }
+ r
+
+right_wall_uv = (x,y) ->
+ r = {
+ vec4(w1.s2,w1.t2,1,1),
+ vec4(w1.s2,w1.t1,1,1),
+ vec4(w1.s1,w1.t1,1,1),
+ vec4(w1.s1,w1.t1,1,1),
+ vec4(w1.s1,w1.t2,1,1),
+ vec4(w1.s2,w1.t2,1,1)
+ }
+ r
+
+compute = () ->
+ geom = {}
+ uv = {}
+ for x = 0,2
+ for y = 0,2
+ for _,v in ipairs(floor(x,y))
+ table.insert(geom,v)
+ for _,v in ipairs(floor_uv(x,y))
+ table.insert(uv,v)
+ for i = 0,2 -- room's left wall
+ for _,v in ipairs(left_wall(-1,i))
+ table.insert(geom,v)
+ for _,v in ipairs(left_wall_uv(-1,i))
+ table.insert(uv,v)
+ for i = 0,2 -- room's right wall
+ for _,v in ipairs(right_wall(3,i))
+ table.insert(geom,v)
+ for _,v in ipairs(right_wall_uv(3,i))
+ table.insert(uv,v)
+ geom, uv, #geom / 3
+
+
+class LobbyGraphic extends GraphicsComponent
+ new: (name, properties) =>
+ @geom, @uv, @n_tris = compute!
+ properties.graphic = sprites.floor.texture
+ super(name, properties)
+ tris: () =>
+ @n_tris
+ populate_buf: (geom_view, uv_view, offset) =>
+ log.info("Creating lobby graphic" .. tostring(@geom),{"level","graphic","lobby"})
+ geom_view\set(@geom, offset, @n_tris * 3)
+ uv_view\set(@uv, offset, @n_tris * 3)
+
+LobbyGraphic
diff --git a/src/prefab/room.moon b/src/prefab/room.moon
new file mode 100644
index 0000000..c041bb8
--- /dev/null
+++ b/src/prefab/room.moon
@@ -0,0 +1,10 @@
+
+
+class Room
+ new: (x,y,width,height) =>
+ @x = x
+ @y = y
+ @width = width
+ @height = height
+ @hallways = {}
+
diff --git a/src/prefab/spawn.moon b/src/prefab/spawn.moon
new file mode 100644
index 0000000..516c72a
--- /dev/null
+++ b/src/prefab/spawn.moon
@@ -0,0 +1,3 @@
+-- Spawnpoint?
+
+class Spawnpoint extends Room
diff --git a/src/prefab/worldgen.moon b/src/prefab/worldgen.moon
new file mode 100644
index 0000000..1d000a6
--- /dev/null
+++ b/src/prefab/worldgen.moon
@@ -0,0 +1,58 @@
+args = {...}
+require("rng")
+self = args[1]
+
+gen = {}
+
+-- Logical worldgen
+-- Strategy: splatter some rooms on a canvas
+-- rooms are {location, width, height, specialty}
+-- splatter some large rooms first, in a mostly-straight line,
+-- then some medium rooms with a larger spread
+-- then a bunch of small rooms with a large spread
+-- then connect each room with nearby neighbors
+
+room_sizes = {
+ -- avgx, stdx, avgy, stdy
+ large: {
+ avg_w: 40
+ std_w: 10
+ avg_l: 40
+ std_l: 10
+ }
+ medium: {
+ avg_w: 20
+ std_w: 5
+ avg_l: 20
+ std_l: 5
+ }
+ small: {
+ avg_w: 8
+ std_w: 3
+ avg_l: 8
+ std_l: 3
+ }
+}
+level = {
+ avg_w: 1000
+ std_w: 200
+ avg_h: 1000
+ std_h: 200
+}
+gen.level = (seed) ->
+ random_gen = rng.generator(seed)
+ normal = (avg, std, gen) =>
+ -- Box-Muller transform
+ bm = math.sqrt(-2 * math.log(gen())) * math.cos(2 * math.pi * gen())
+ -- Box-Muller gives us std = e^-0.5 , avg = 0
+ ((bm / math.exp(-1/2)) * std) + avg
+ width = random_gen(avg_w, std_w, random_gen)
+ height = random_gen(avg_h, std_h, random_gen)
+ rooms = {}
+ -- Pick a a direction to splatter
+ direction = random_gen() * 2 * math.pi
+ --rooms[0] =
+
+
+
+gen
diff --git a/src/preload.lua b/src/preload.lua
new file mode 100644
index 0000000..1085f63
--- /dev/null
+++ b/src/preload.lua
@@ -0,0 +1,107 @@
+-- Stuff to load before everything else
+
+--[[
+rewrite traceback function to map file names and line numbers to moonscript
+]]
+local require_order = {}
+local old_traceback = debug.traceback
+debug.traceback = function(...)
+ local orig_traceback = old_traceback(...)
+ local noprint = {}
+ return orig_traceback:gsub("([%w/_]+%.lua):(%d+):",function(filename, linenum)
+ --If our file is not moonscript, we won't have a debug file
+ local debugfile = am.load_string(filename .. ".X")
+ if not debugfile then
+ return filename .. ":" .. linenum .. ":"
+ end
+ debugfile = debugfile .. "\n"
+ for line in debugfile:gmatch("([^\n]+)\n") do
+ local _,_,pos,lua,moon = line:find("(%d+)%s+(%d+):%b[] >> (%d+)")
+ if pos and lua and moon and tonumber(linenum) == tonumber(lua) then
+ filename = filename:gsub(".lua$",".moon")
+ linenum = moon
+ break
+ end
+ end
+ return string.format("%s:%d:", filename, linenum)
+ end) --.. "\nRequire order: [" .. table.concat(require_order, ",\n") .. "]"
+end
+
+local old_require = require
+local required = {}
+require = function(...)
+ args = {...}
+ if not required[args[1]] then
+ required[args[1]] = true
+ table.insert(require_order, args[1])
+ end
+ return old_require(...)
+end
+
+--[[
+Display where print statements are comming from
+
+local oldprint = print
+print = function(...)
+ error("Print")
+ oldprint(debug.traceback())
+end
+]]
+
+-- Override tostring to display more info about the table
+local old_tostring = tostring
+local numtabs = 0
+local printed_tables = {}
+local function tostring_helper(el)
+ assert(type(el) == "table", "Tried to call helper with something that was not a table, it was a " .. type(el))
+ local mt = getmetatable(el)
+ if mt and mt.__tostring then
+ return mt.__tostring(el)
+ elseif printed_tables[el] == true then
+ return old_tostring(el)
+ else
+ printed_tables[el] = true
+ numtabs = numtabs + 1
+ local strbuilder = {"{"}
+ for k,v in pairs(el) do
+ local key,value
+ if type(k) == "table" then
+ key = tostring_helper(k)
+ else
+ key = old_tostring(k)
+ end
+ if type(v) == "table" then
+ value = tostring_helper(v)
+ else
+ value = old_tostring(v)
+ end
+ strbuilder[#strbuilder + 1] = string.format("%s%s : %s", string.rep("\t",numtabs), key, value)
+ end
+ strbuilder[#strbuilder + 1] = string.rep("\t",numtabs - 1) .. "}"
+ numtabs = numtabs - 1
+ return table.concat(strbuilder,"\n")
+ end
+
+end
+function tostring(el)
+ printed_tables = {}
+ if type(el) == "table" then
+ return tostring_helper(el)
+ end
+ return old_tostring(el)
+end
+
+-- Override global error function to log errors through the log system
+local old_error = error
+function error(message, level)
+ -- Get the log module if available
+ local log_available, log = pcall(require, "log")
+ if log_available and log and log.error then
+ -- Get full traceback
+ local traceback = debug.traceback(tostring(message), 2)
+ log.error(traceback, {"error"})
+ end
+ -- Call the original error function
+ old_error(message, level or 2)
+end
+
diff --git a/src/rng.moon b/src/rng.moon
new file mode 100644
index 0000000..d7606f9
--- /dev/null
+++ b/src/rng.moon
@@ -0,0 +1,46 @@
+-- Contains pseudo-random number generators, and some helper functions
+
+rng = {}
+totally_random_seed = tonumber(os.date("%Y%H%M%S"))
+math.randomseed(totally_random_seed)
+
+-- same syntax as math.random, if m and n are passed, they are lower and upper bounds
+-- if only m is passed, it is the upper bound
+-- if neither is passed, between 0 and 1
+-- Example:
+-- local rng = require("rng")
+-- local generator1 = rng.generator()
+-- local random1 = generator1()
+-- local generator2 = rng.generator()
+-- local random2 = generator2()
+-- assert(random1, random2)
+rng.generator = (seed, m, n) ->
+ seed = seed or tonumber(os.date("%Y%S"))
+ co = coroutine.wrap(() ->
+ while true
+ math.randomseed(seed)
+ if m and n
+ seed = math.random(m,n)
+ elseif m
+ seed = math.random(m)
+ else
+ seed = math.random()
+ coroutine.yield(seed)
+ )
+ co, seed
+
+rng.randomstring = (charset, length) ->
+ t = {}
+ charset_len = #charset
+ for i = 1, length
+ char = math.random(charset_len)
+ t[i] = charset\sub(char,char)
+ table.concat(t)
+
+rng.hexstring = (length) ->
+ rng.randomstring("0123456789ABCDEF", length)
+
+rng.numstring = (length) ->
+ rng.randomstring("0123456789", length)
+
+rng
diff --git a/src/server/init.moon b/src/server/init.moon
new file mode 100644
index 0000000..9c2c22f
--- /dev/null
+++ b/src/server/init.moon
@@ -0,0 +1,143 @@
+
+ecs = require("ecs")
+log = require("log")
+world = require("world")
+net = require("net")
+NetworkedComponent = require("ecs.networked")
+PredictedComponent = require("ecs.predicted")
+player_movement = require("shared.player_movement")
+
+x = {}
+
+x.initialize = () ->
+ world.domain = "server"
+ if not world.hub
+ log.error("Running server init, but no world hub has been created",{"server"})
+ print("World was:",world)
+ error("World.hub has not been set")
+
+ log.info("Server initalized",{"server"})
+ world.level_sync.name = "levels.lobby"
+
+ pawns = {} -- peerid to entity lookup
+ net.register_message("RequestLevel",{})
+ world.hub\listen("RequestLevel", "RespondLevel", (from_client, data) ->
+ log.info("Got reqeust for level info" .. tostring(from_client), {"net","server"})
+ world.hub\send(from_client, "RespondLevel",{
+ name: world.level_sync.name
+ data: world.level_sync.data
+ })
+ )
+ net.register_message("RequestPeers",{})
+ world.hub\listen("RequestPeers", "RespondPeers", (from_client, data) ->
+ log.info("Got request for player info" .. tostring(from_client), {"net","server"})
+ players = {}
+ for peerid, ent in pairs(world.level_sync.peers)
+ players[peerid] = ent\get("net")\pack!
+ log.info("Got data for player" .. peerid .. ":" .. players[peerid], {"net","server"})
+ world.hub\send(from_client, "RespondPeers", players)
+ )
+ net.register_message("RequestEntities",{})
+ world\check!
+ world.hub\listen("RequestEntities", "RespondEntities", (from_client, data) ->
+ log.info("Got request for entities from " .. tostring(from_client), {"net","server"})
+ log.info("Responding with:" .. tostring(world.level_sync.ents), {"net","server"})
+ log.info("At request for entities, level is " .. tostring(world.level_sync.name), {"net","server"})
+ if world.level_sync.name == "levels.game"
+ return -- Don't sync here, instead send a R
+ world\check!
+ -- Simplified way to send entities
+ nents = {}
+ for i, entity in pairs(world.level_sync.ents)
+ ent_net = entity\get("net")
+ if not ent_net
+ error("Server entity does not have a net component:" .. tostring(entity))
+ if not ent_net.properties.type
+ error("Failed to find net entity type for " .. tostring(net_ent) .. " net component was: " .. tostring(ent_net.net_properties))
+ nents[i] = ent_net\pack!
+ world.hub\send(from_client, "RespondEntities", nents)
+ )
+ world.hub\listen("Join", "CreatePawn", (from_client, data) ->
+ assert(world.hub, "Join should only be called on the server")
+ log.info("Got player joining:" .. tostring(from_client), {"net","server"})
+ --pawn = ecs.Entity!
+ --spawn_loc = assert(world.level_sync.ref\get_spawn_location!, "Failed to find inital spawn location")
+ --net = NetworkedComponent("net",{
+ --type: "player"
+ --pos: spawn_loc
+ --ang: 0
+ --vel: {0,0,0}
+ --acc: {0,0,0}
+ --player_name: "name"
+ --last_update: am.eval_js("Date.now()")
+ --peerid: from_client
+ --})
+ --pawn\add(net, "net")
+ --pred = PredictedComponent("pred",{acc:{0,0,0}, vel: {0,0,0}, pos: {0,0,0}}, "net", player_movement)
+ --pawn\add(pred, "pred")
+ --world.level_sync.peers[from_client] = pawn
+
+ --world.hub\broadcast("CreatePawn", net\pack!)
+
+ -- Surface Join events to the browser for integration tests.
+ -- Guarded so it is a no-op in non-HTML environments.
+ if am and am.eval_js and am.to_json
+ js = string.format("window._hubJoinReceived = true; window._hubJoinData = %s;", am.to_json(data or {}))
+ am.eval_js(js)
+ )
+ require("levels.lobby")
+ lobby = ecs.Entity!
+ lobby\add(ecs.NetworkedComponent("lobby_peer",{
+ type: "level",
+ level_name: world.level_sync.name
+ level_data: {world.hub.peer.id}
+ }), "net")
+ world.level_sync.ref = {
+ id: "level from server/init.moon"
+ get_spawn_location: () ->
+ {0,0,0}
+ }
+ net.register_message("SuggestPlayerUpdate",{
+ optional: {
+ pos: "table" -- x, y, z
+ ang: "number" -- 0->360
+ vel: "table" -- x, y, z
+ acc: "table" -- x, y, z
+ player_name: "string"
+ last_update: "number"
+ }
+ })
+ world.hub\listen("SuggestPlayerUpdate","UpdateName",(from_client, data) ->
+ log.debug("Got player update from " .. from_client .. ":" .. tostring(data), {"net","server","player"})
+ net = world.level_sync.peers[from_client]\get("net")
+ if not net
+ error("Got message from client" .. tostring(from_client) .. " but no such client exists!")
+ if data.player_name
+ net.properties.player_name = data.player_name
+ if data.pos
+ net.properties.pos = data.pos
+ if data.vel
+ net.properties.vel = data.vel
+ if data.last_update
+ net.properties.last_update = data.last_update
+ if data.acc
+ for i = 1,3
+ if not (data.acc[i] and type(data.acc[i] == "number"))
+ log.warn("Peer " .. from_client .. " sent bad acceleration", {"net","player"})
+ return
+ v = vec3(data.acc[1], data.acc[2], data.acc[3])
+ if math.length(v) > 1
+ log.warn("Peer " .. from_client .. " sent too much acceleration: " .. tostring(math.length(v)),{"net","player"})
+ return
+ net.properties.acc = data.acc
+ )
+ world.hub\listen("RequestRole","Request role", (clientid, _) ->
+ client_data = world.level_sync.client_data
+ log.info("Responding with role:" .. tostring(client_data[clientid]), {"net","server"})
+ world.hub\send(clientid, "RespondRole", client_data[clientid])
+ )
+
+
+ world.domain = "client"
+
+x
diff --git a/src/settings.moon b/src/settings.moon
new file mode 100644
index 0000000..e2a5452
--- /dev/null
+++ b/src/settings.moon
@@ -0,0 +1,19 @@
+settings = {}
+
+settings.n_unmasked = 1
+settings.game_time = 600 --in seconds
+
+-- Ideas for extra roles that can be turned on:
+-- Executioner (can reveal if anyone is killed)
+-- Sleeper Agent (reveal once someone says a codeword)
+-- Founder (Reveal any time, peek at someone's mask)
+-- Tardy (Reveal in the last minute of gameplay)
+-- Secret Police (Voting out does not count against the number of votes the cult has)
+-- Enforcer (Reveal any time, kill any other player)
+-- Recruiter (Knows 2 other players that are not the masked player)
+-- Fool (Tries to be killed, all other players lose)
+-- Necromancer (Reveals after a player is killed and brings a dead player back to life (allow 1 more wrong guess))
+-- True Beliver (Takes on the role of any other cultist after they are killed)
+-- Exceptional sacrifice (Reveals after players run out of attempts to find the unmasked, cultists get 1 more guess)
+
+settings
diff --git a/src/shader_shim.moon b/src/shader_shim.moon
new file mode 100644
index 0000000..76e0aec
--- /dev/null
+++ b/src/shader_shim.moon
@@ -0,0 +1,22 @@
+-- Sometimes we want to compile shaders, use the syntax
+-- {variable} to adress a variable
+win = require("window")
+inputs = {
+ "@width": win.width
+ "@height": win.height
+}
+
+shaders = setmetatable({},{
+ __index:(self, key) ->
+ vert_name = "shaders/" .. key .. ".vert"
+ frag_name = "shaders/" .. key .. ".frag"
+ vert = assert(am.load_string(vert_name), "Failed to find " .. vert_name)
+ frag = assert(am.load_string(frag_name), "Failed to find " .. frag_name)
+ vert_subbed = vert\gsub("@%b{}",(n) -> tostring(inputs[n]))
+ frag_subbed = frag\gsub("@%b{}",(n) -> tostring(inputs[n]))
+ succ, program = pcall(am.program, vert_subbed, frag_subbed)
+ if not succ
+ error(string.format("Failed compiling shader %q: %s vertex shader: %s fragment shader: %s", key, program, vert_subbed, frag_subbed))
+ am.use_program(am.program(vert_subbed, frag_subbed))
+})
+shaders
diff --git a/src/shaders/lake.frag b/src/shaders/lake.frag
new file mode 100644
index 0000000..c1c23f0
--- /dev/null
+++ b/src/shaders/lake.frag
@@ -0,0 +1,93 @@
+precision mediump float;
+uniform vec4 black;
+uniform vec4 outline;
+uniform vec4 highlight;
+uniform vec4 foreground;
+uniform vec4 midground;
+uniform vec4 shadow;
+uniform vec4 background;
+uniform vec4 lamp1; //vec3 position, float strength
+uniform vec4 lamp2;
+uniform vec4 lamp3; // max 3 lamps per shaded vertex
+uniform vec4 lamp4;
+uniform vec4 lamp5;
+uniform vec4 lamp6;
+uniform vec4 lamp7;
+uniform vec4 lamp8;
+uniform float streamer; // turn off the noise in the light
+uniform float time; //used for noise
+uniform sampler2D atlas;
+uniform sampler2D previous;
+uniform float nlamps;
+varying vec2 worldxy;
+varying vec2 norm;
+
+// Author @patriciogv - 2015
+float random (vec2 st) {
+ return fract(
+ sin(
+ dot(st.xy,vec2(12.9898,78.233))
+ ) *
+ 43758.5453123
+ );
+}
+
+// stolen from https://www.shadertoy.com/view/Msf3WH
+float noise( in vec2 p )
+{
+ const float K1 = 0.366025404; // (sqrt(3)-1)/2;
+ const float K2 = 0.211324865; // (3-sqrt(3))/6;
+
+ vec2 i = floor( p + (p.x+p.y)*K1 );
+ vec2 a = p - i + (i.x+i.y)*K2;
+ float m = step(a.y,a.x);
+ vec2 o = vec2(m,1.0-m);
+ vec2 b = a - o + K2;
+ vec2 c = a - 1.0 + 2.0*K2;
+ vec3 h = max( 0.5-vec3(dot(a,a), dot(b,b), dot(c,c) ), 0.0 );
+ vec3 n = h*h*h*h*vec3( dot(a,random(i+0.0)), dot(b,random(i+o)), dot(c,random(i+1.0)));
+ return dot( n, vec3(70.0) );
+}
+
+void main() {
+ vec4 coord = gl_FragCoord + vec4(worldxy * 256., 0, 0);
+ /*
+ coord.x -= worldxy.x;
+ coord.y -= worldxy.y;
+ */
+ //coord = coord / 1000.;
+ // calculate color at this pixel
+ vec4 normal = texture2D(atlas, norm);
+ float color = 0.;
+ vec4 lamp1_norm = lamp1 * 256.;
+ color += lamp1_norm.w - distance(lamp1_norm.xy - worldxy, coord.xy);
+ color = max(color,(lamp2.w * 256.) - distance((lamp2.xy * 256.) - worldxy, coord.xy));
+ color = max(color,(lamp3.w * 256.) - distance((lamp3.xy * 256.) - worldxy, coord.xy));
+ // divide to get a normalized color
+ //color /= (256. * max(max(lamp1.w, lamp2.w), lamp3.w));
+ color /= 256. * 5.;
+ //color = sqrt(color / 2046.);
+ // Sett the normal texture under our lamplight
+ color = dot(vec4(color),normal) / 1.;
+ // make the colors fuzzy near the border (or don't if we're streaming)
+ float rng = random(vec2(coord.x, coord.y) + vec2(color, time));
+ color -= (pow(rng / 1.3, 10. * color)) * streamer;
+ // add noise to the water
+ /* */
+ if(color > 1.)
+ gl_FragColor = highlight * normal.a;
+ else if(color > 0.8)
+ gl_FragColor = foreground * normal.a;
+ else if(color > 0.6)
+ gl_FragColor = midground * normal.a;
+ else if(color > 0.4)
+ gl_FragColor = background * normal.a;
+ else if(color > 0.2)
+ gl_FragColor = shadow * normal.a;
+ else
+ gl_FragColor = black * normal.a;
+ /*
+ gl_FragColor = normal* vec4(color , color, color,1.);
+ */
+ //gl_FragColor = normal* vec4(color , color / 10., color / 1024.,1.);
+}
diff --git a/src/shaders/lake.moon b/src/shaders/lake.moon
new file mode 100644
index 0000000..bac2c42
--- /dev/null
+++ b/src/shaders/lake.moon
@@ -0,0 +1,29 @@
+
+shader_shim = require("shader_shim")
+win = require("window")
+world = require("world")
+
+node = shader_shim.lake\append(am.bind({
+ MV: mat4(
+ 1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, 0,
+ (-win.width / 2), (-win.height/2), 0, 1
+ ),
+ P: mat4(1)
+ lake: am.vec3_array({})
+ light1: am.vec4_array({})
+ light2: am.vec4_array({})
+ light3: am.vec4_array({})
+ world_x: 0
+ world_y: 0
+ time: am.current_time()
+}))\append(am.draw("triangles"))
+
+node\action((self) ->
+ self("bind").time = am.current_time!
+ self("bind").world_x = world.world_x
+ self("bind").world_y = world.world_y
+)
+
+node
diff --git a/src/shaders/lake.vert b/src/shaders/lake.vert
new file mode 100644
index 0000000..2745cf9
--- /dev/null
+++ b/src/shaders/lake.vert
@@ -0,0 +1,13 @@
+precision highp float;
+attribute vec3 lake;
+attribute vec4 lamp1; //position, strength
+attribute vec4 lamp2;
+attribute vec4 lamp3; // max 3 lamps per shaded vertex
+uniform float time; //used for noise
+uniform float world_x;
+uniform float world_y;
+uniform mat4 MV;
+uniform mat4 P;
+void main() {
+ gl_Position = P * MV * vec4(lake.x - world_x, lake.y - world_y, 0., 1.0);
+}
diff --git a/src/shaders/land.frag b/src/shaders/land.frag
new file mode 100644
index 0000000..1eb8d93
--- /dev/null
+++ b/src/shaders/land.frag
@@ -0,0 +1,100 @@
+precision mediump float;
+uniform vec4 black;
+uniform vec4 outline;
+uniform vec4 highlight;
+uniform vec4 foreground;
+uniform vec4 midground;
+uniform vec4 shadow;
+uniform vec4 background;
+uniform vec4 lamp1; //vec3 position, float strength
+uniform vec4 lamp2;
+uniform vec4 lamp3; // max 3 lamps per shaded vertex
+uniform vec4 lamp4;
+uniform vec4 lamp5;
+uniform vec4 lamp6;
+uniform vec4 lamp7;
+uniform vec4 lamp8;
+uniform float streamer; // turn off the noise in the light
+uniform float time; //used for noise
+uniform sampler2D atlas;
+uniform float nlamps;
+uniform float water;
+varying vec2 worldxy;
+varying vec2 norm;
+
+// Author @patriciogv - 2015
+float random (vec2 st) {
+ return fract(
+ sin(
+ dot(st.xy,vec2(12.9898,78.233))
+ ) *
+ 43758.5453123
+ );
+}
+
+// stolen from https://www.shadertoy.com/view/Msf3WH
+vec2 hash( vec2 p ) // replace this by something better
+{
+ p = vec2( dot(p,vec2(127.1,311.7)), dot(p,vec2(269.5,183.3)) );
+ return -1.0 + 2.0*fract(sin(p)*43758.5453123);
+}
+float noise( in vec2 p )
+{
+ const float K1 = 0.366025404; // (sqrt(3)-1)/2;
+ const float K2 = 0.211324865; // (3-sqrt(3))/6;
+
+ vec2 i = floor( p + (p.x+p.y)*K1 );
+ vec2 a = p - i + (i.x+i.y)*K2;
+ float m = step(a.y,a.x);
+ vec2 o = vec2(m,1.0-m);
+ vec2 b = a - o + K2;
+ vec2 c = a - 1.0 + 2.0*K2;
+ vec3 h = max( 0.5-vec3(dot(a,a), dot(b,b), dot(c,c) ), 0.0 );
+ vec3 n = h*h*h*h*vec3( dot(a,hash(i+0.0)), dot(b,hash(i+o)), dot(c,hash(i+1.0)));
+ return dot( n, vec3(70.0) );
+}
+
+void main() {
+ vec4 coord = gl_FragCoord + vec4(worldxy * 256., 0, 0);
+ /*
+ coord.x -= worldxy.x;
+ coord.y -= worldxy.y;
+ */
+ //coord = coord / 1000.;
+ // calculate color at this pixel
+ vec4 normal = texture2D(atlas, norm);
+ float color = 0.;
+ vec4 lamp1_norm = lamp1 * 256.;
+ color += lamp1_norm.w - distance(lamp1_norm.xy - worldxy, coord.xy);
+ color = max(color,(lamp2.w * 256.) - distance((lamp2.xy * 256.) - worldxy, coord.xy));
+ color = max(color,(lamp3.w * 256.) - distance((lamp3.xy * 256.) - worldxy, coord.xy));
+ // divide to get a normalized color
+ //color /= (256. * max(max(lamp1.w, lamp2.w), lamp3.w));
+ color /= 256. * 5.;
+ //color = sqrt(color / 2046.);
+ // see the normal texture under the color
+ color = dot(vec4(color),normal) / 1.;
+ // make the colors fuzzy near the border (or don't if we're streaming)
+ float rng = random(vec2(coord.x, coord.y) + vec2(color, time));
+ color -= (pow(rng / 1.3, 10. * color)) * streamer;
+ // add noise to water
+ if(water > 1.)
+ color += (noise(coord.xy + vec2(time, time)) - 0.0) * 0.1;
+ /* */
+ if(color > 1.)
+ gl_FragColor = highlight * normal.a;
+ else if(color > 0.8)
+ gl_FragColor = foreground * normal.a;
+ else if(color > 0.6)
+ gl_FragColor = midground * normal.a;
+ else if(color > 0.4)
+ gl_FragColor = background * normal.a;
+ else if(color > 0.2)
+ gl_FragColor = shadow * normal.a;
+ else
+ gl_FragColor = black * normal.a;
+ /*
+ gl_FragColor = normal* vec4(color , color, color,1.);
+ */
+ //gl_FragColor = normal* vec4(color , color / 10., color / 1024.,1.);
+}
diff --git a/src/shaders/land.vert b/src/shaders/land.vert
new file mode 100644
index 0000000..1c9b9f3
--- /dev/null
+++ b/src/shaders/land.vert
@@ -0,0 +1,26 @@
+precision highp float;
+attribute vec3 land;
+attribute vec2 landnormal;
+uniform float rot;
+uniform float world_x;
+uniform float world_y;
+uniform mat4 MV;
+uniform mat4 P;
+varying vec2 worldxy;
+varying mat4 pre;
+varying vec2 norm;
+void main() {
+ norm = landnormal;
+ mat2 rotate = mat2(
+ cos(rot), -sin(rot),
+ sin(rot), cos(rot)
+ );
+ worldxy = vec2(world_x, world_y);
+ pre = P * MV;
+ vec2 local = (land.xy - worldxy) * rotate;
+ float z_scale = 0.5;
+ // clamp so that everything becomes orthographic once we move away
+ float xoff = clamp(land.z * local.x * z_scale, -0.5, 0.5);
+ float yoff = clamp(land.z * local.y * z_scale, -0.5, 0.5);
+ gl_Position = P * MV * vec4(local.xy - vec2(xoff, yoff), land.z, 1.0);
+}
diff --git a/src/shaders/palette.vert b/src/shaders/palette.vert
new file mode 100644
index 0000000..5937253
--- /dev/null
+++ b/src/shaders/palette.vert
@@ -0,0 +1,26 @@
+precision highp float;
+attribute vec3 world; // position
+attribute vec2 texuv;
+attribute float r; // for round objects, 0 for non-round
+varying vec2 textureuv;
+varying float radius;
+varying mat3 light1;
+varying vec4 v_color;
+uniform float world_x;
+uniform float world_y;
+uniform mat4 MV;
+uniform mat4 P;
+void main() {
+ v_color = vec4(world.xyz,1.);
+ vec2 vxy = vec2(world.x - world_x, world.y - world_y);
+ float z_scale = 0.5;
+ float max_parallax = 0.5;
+ float xoff = clamp(world.z * vxy.x * z_scale, -max_parallax, max_parallax);
+ float yoff = clamp(world.z * vxy.y * z_scale, -max_parallax, max_parallax);
+ textureuv=texuv;
+ //radius = r;
+ // if z > 0 then
+ // xoff = ceil(xoff, 0)
+ // add to the z coord so we don't intersect with the ui
+ gl_Position = P * MV * vec4(vxy.x + xoff, vxy.y + yoff, world.z, 1.0);
+}
diff --git a/src/shaders/player.frag b/src/shaders/player.frag
new file mode 100644
index 0000000..5d329fa
--- /dev/null
+++ b/src/shaders/player.frag
@@ -0,0 +1,14 @@
+precision mediump float;
+varying vec2 textureuv; // uv
+uniform sampler2D textures;
+uniform sampler2D emissives;
+uniform sampler2D normals;
+varying mat3 light1; // position, color, intensity-fadetime-?
+varying vec4 v_color;
+void main() {
+ vec2 uv = textureuv;
+ //gl_FragColor = texture2D(textures,uv);// + vec4(uv.xy / 4.,0.,1.);
+ gl_FragColor = vec4(uv.xy / 1., 0., max(uv.x, uv.y));
+ //gl_FragColor = texture2D(textures,screen_intersection.xy);
+
+}
diff --git a/src/shaders/player.vert b/src/shaders/player.vert
new file mode 100644
index 0000000..ce53bf5
--- /dev/null
+++ b/src/shaders/player.vert
@@ -0,0 +1,23 @@
+precision highp float;
+attribute vec3 player;
+attribute vec2 texuv;
+varying vec2 textureuv;
+attribute vec4 lamp1; //vec3 position, float strength
+attribute vec4 lamp2;
+attribute vec4 lamp3; // max 3 lamps per shaded player
+uniform float time; //used for noise
+uniform float world_x;
+uniform float world_y;
+uniform float dir;
+uniform mat4 MV;
+uniform mat4 P;
+void main() {
+ textureuv=texuv;
+ mat2 rotate = mat2(
+ cos(dir), -sin(dir),
+ sin(dir), cos(dir)
+ );
+ vec2 world = vec2(world_x, world_y);
+ vec2 local = (player.xy - world) * rotate;
+ gl_Position = P * MV * vec4(local.xy, -2, 1.0);
+}
diff --git a/src/shaders/stars.frag b/src/shaders/stars.frag
new file mode 100644
index 0000000..d45917e
--- /dev/null
+++ b/src/shaders/stars.frag
@@ -0,0 +1,5 @@
+precision mediump float;
+uniform vec4 color;
+void main() {
+ gl_FragColor = color;
+}
diff --git a/src/shaders/stars.lua b/src/shaders/stars.lua
new file mode 100644
index 0000000..3e4138e
--- /dev/null
+++ b/src/shaders/stars.lua
@@ -0,0 +1,75 @@
+local win = require("window")
+local color = require("color")
+local world = require("world")
+local shim = require("shader_shim")
+local numstars = 500 -- we might have as many as 4 over
+local genned_stars = 0
+local period_x = 3
+local period_y = 3
+local stars = {}
+local tries = 0
+aspect = win.width / win.height
+while genned_stars < numstars and tries < 100000 do
+ local rngx = math.random()
+ local xpos = rngx * win.width --* (period_x - 1)
+ local rngy = math.random()
+ local ypos = rngy * win.height --* (period_y - 1)
+ local blinks = math.random() > 0.3 and (math.random() * 2 * math.pi) or 0
+ --if math.distance(vec2(rngx,rngy), vec2(0.53,0.5)) > 0.5 then
+ local off = vec2(math.abs(rngx - 0.50) * aspect, math.abs(rngy-0.5))
+ if math.length(off) > 0.5 then
+ stars[#stars+1] = vec3(xpos, ypos, blinks)
+ genned_stars = genned_stars + 1
+ if xpos < win.width then
+ -- duplicate on the last screen
+ stars[#stars+1] = vec3(xpos + (win.width * (period_x-2)), ypos, blinks)
+ genned_stars = genned_stars + 1
+ end
+ if ypos < win.height then
+ stars[#stars+1] = vec3(xpos, ypos + (win.height * (period_y-2)), blinks)
+ genned_stars = genned_stars + 1
+ end
+ if xpos < win.width and ypos < win.height then
+ stars[#stars+1] = vec3(xpos + (win.width * (period_x-2)), ypos+(win.height * (period_y-2)),blinks)
+ genned_stars = genned_stars + 1
+ end
+ end
+ tries = tries + 1
+end
+assert(genned_stars == numstars, "Failed to generate stars")
+local node = am.blend("premult") ^ shim.stars
+^ am.bind({
+ MV = mat4(
+ 1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, 0,
+ (-win.width / 2), (-win.height/2), 0, 1
+ ),
+ color = color.am_color.highlight,
+ stars = am.vec3_array(stars),
+ world_x = am.current_time(),
+ world_x_period = (period_x - 2) * win.width,
+ world_y = am.current_time(),
+ world_y_period = (period_y - 2) * win.height,
+ time = am.current_time(),
+ lamp1 = vec3(0),
+ lamp2 = vec3(0),
+ lamp3 = vec3(0),
+ lamp4 = vec3(0),
+ lamp5 = vec3(0),
+ lamp6 = vec3(0),
+ lamp7 = vec3(0),
+ lamp8 = vec3(0)
+})
+^ am.draw("points")
+node:action(function(self)
+ self("bind").time = am.current_time()
+ self("bind").world_x = world.world_x
+ self("bind").world_y = world.world_y
+ local lamps = world.level.lamps_on_screen()
+ for i,v in pairs(lamps) do
+ print("Setting lamp", i, "to", v)
+ self("bind")["lamp" .. tostring(i)] = v
+ end
+end)
+return node
diff --git a/src/shaders/stars.vert b/src/shaders/stars.vert
new file mode 100644
index 0000000..8737482
--- /dev/null
+++ b/src/shaders/stars.vert
@@ -0,0 +1,30 @@
+precision highp float;
+attribute vec3 stars;
+uniform float time;
+uniform float world_x;
+uniform float world_y;
+uniform float world_x_period;
+uniform float world_y_period;
+uniform vec4 lamp1;
+uniform vec4 lamp2;
+uniform vec4 lamp3;
+uniform vec4 lamp4;
+uniform vec4 lamp5;
+uniform vec4 lamp6;
+uniform vec4 lamp7;
+uniform vec4 lamp8;
+uniform mat4 MV;
+uniform mat4 P;
+void main() {
+ float world_x_off = mod(world_x, world_x_period);
+ float world_y_off = mod(world_y, world_y_period);
+ vec4 pos = P * MV * vec4(stars.x - world_x_off, stars.y - world_y_off, -0.1, 1.0);
+ gl_Position = pos;
+ float intensity = sin(stars.z + time) * cos(time) + 1.;
+ /*
+ if(distance(pos.xyz, lamp1.xyz) < 80.)
+ intensity = 0.;
+ */
+ gl_PointSize = pow(intensity, 2.) * stars.z * 0.3;
+ //gl_PointSize = distance(pos.xyz, lamp1.xyz);
+}
diff --git a/src/shaders/world.frag b/src/shaders/world.frag
new file mode 100644
index 0000000..9cc1bc9
--- /dev/null
+++ b/src/shaders/world.frag
@@ -0,0 +1,29 @@
+precision mediump float;
+varying vec2 textureuv; // uv
+varying float radius;
+uniform sampler2D textures;
+uniform sampler2D emissives;
+uniform sampler2D normals;
+varying mat3 light1; // position, color, intensity-fadetime-?
+uniform float time;
+varying vec4 v_color;
+void main() {
+
+ vec2 uv = textureuv;
+ //vec2 uv = gl_FragCoord.xy;
+ //vec3 view_origin = vec3(0., 0., -3.);
+ //vec3 view_direction = vec3(uv, 3);
+ //vec3 screen_intersection = vec3(uv.x, uv.y, 0.);
+ vec4 raw = texture2D(textures,uv);
+ gl_FragColor = raw;
+ //gl_FragColor = texture2D(textures,uv);// + vec4(uv.xy / 4.,0.,1.);
+ //if(raw.r == 1.0 && raw.g == 1.0 && raw.b == 1.0){
+ //gl_FragColor = vec4(0.9058, 0.9215, 0.7725, 1);
+ //} else if(raw.r == 0.0 && raw.g == 0.0 && raw.b == 0.0){
+ //gl_FragColor = vec4(0.298, 0.267, 0.216, 1);
+ //} else {
+ //gl_FragColor = raw;
+ //}
+}
+ //gl_FragColor = vec4(gl_FragCoord.z, gl_FragCoord.z, gl_FragCoord.z, 1.);
+ //gl_FragColor = texture2D(textures,screen_intersection.xy);
diff --git a/src/shaders/world.frag.back b/src/shaders/world.frag.back
new file mode 100644
index 0000000..15d7b27
--- /dev/null
+++ b/src/shaders/world.frag.back
@@ -0,0 +1,20 @@
+precision mediump float;
+varying vec2 textureuv; // uv
+varying float radius;
+uniform sampler2D textures;
+uniform sampler2D emissives;
+uniform sampler2D normals;
+varying mat3 light1; // position, color, intensity-fadetime-?
+uniform float time;
+varying vec4 v_color;
+void main() {
+
+ vec2 uv = textureuv;
+ //vec2 uv = gl_FragCoord.xy;
+ //vec3 view_origin = vec3(0., 0., -3.);
+ //vec3 view_direction = vec3(uv, 3);
+ //vec3 screen_intersection = vec3(uv.x, uv.y, 0.);
+ gl_FragColor = texture2D(textures,uv);// + vec4(uv.xy / 4.,0.,1.);
+ //gl_FragColor = texture2D(textures,screen_intersection.xy);
+
+}
diff --git a/src/shaders/world.moon b/src/shaders/world.moon
new file mode 100644
index 0000000..87dba90
--- /dev/null
+++ b/src/shaders/world.moon
@@ -0,0 +1,356 @@
+win = require("window")
+color = require("color")
+world = require("world")
+sprites = require("sprites")
+shader_shim = require("shader_shim")
+hc = require("party.hc.init")
+log = require("log")
+assert(world.world_x, "No world_x" .. debug.traceback())
+-- Process the world into buffers to send to the shader
+view_angle = math.pi / 4
+near_plane = 0.01
+far_plane = 100
+aspect = win.width / win.height
+-- 2.5D model-view matrix: scale down and offset camera
+s_mv = mat4(
+ 1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, 0,
+ 0, 0, -2, 1
+)
+p_mv = math.perspective(math.rad(85), aspect, near_plane, far_plane)
+
+sd = sprites.floor
+w1 = sprites.wall
+
+-- Each point needs:
+-- vec3 position (x,y,z)
+-- vec2 (u,v)
+up_down_hall = (x,y) ->
+ r = {
+ --floor
+ vec3(x,y,0),
+ vec3(x+1,y,0),
+ vec3(x+1,y-1,0),
+ vec3(x+1,y-1,0),
+ vec3(x,y-1,0),
+ vec3(x,y,0),
+
+ -- Left wall
+ vec3(x,y,1),
+ vec3(x,y,0),
+ vec3(x,y-1,0),
+ vec3(x,y-1,0),
+ vec3(x,y-1,1),
+ vec3(x,y,1),
+
+ --Right wall
+ vec3(x+1,y,0),
+ vec3(x+1,y,1),
+ vec3(x+1,y-1,1),
+ vec3(x+1,y-1,1),
+ vec3(x+1,y-1,0),
+ vec3(x+1,y,0),
+ }
+ r
+
+room = (x,y,w,h,left_holes,right_holes,top_holes,bottom_holes) ->
+ left_holes\sort()
+ right_holes\sort()
+ top_holes\sort()
+ bottom_holes\sort()
+ r = {
+ --floor
+ vec3(x,y,0)
+ vec3(x+w,y,0),
+ vec3(x+w,y-h,0),
+ vec3(x+w,y-h,0),
+ vec3(x,y-h,0),
+ vec3(x,y,0),
+
+ --left wall
+ }
+ r
+
+barrel = (x,y,w,h) ->
+ tris = 18
+ rad = (w/2)
+ l = x - (w/2)
+ j = x + (w/2)
+ t = h
+ b = 0
+ f = y - (w/2)
+ n = y + (w/2)
+ r = {
+ --top
+ vec3(l,f,h),
+ vec3(l,n,h),
+ vec3(j,n,h),
+ vec3(j,n,h),
+ vec3(j,f,h),
+ vec3(l,f,h),
+ }
+ step = (2*math.pi)/tris
+ for i = 0,2*math.pi,step
+ r[#r+1] =vec3(x + math.cos(i)*rad,y + math.sin(i)*n,h)
+ r[#r+1] =vec3(x + math.cos(i+step)*rad,math.sin(i+step)*n,h)
+ r[#r+1] =vec3(x + math.cos(i+step)*rad,math.sin(i+step)*n,0)
+ r[#r+1] =vec3(x + math.cos(i+step)*rad,math.sin(i+step)*n,0)
+ r[#r+1] =vec3(x + math.cos(i)*rad,math.sin(i)*n,0)
+ r[#r+1] =vec3(x + math.cos(i)*rad,math.sin(i)*n,h)
+ r
+
+barrel_uv = (x,y,w,h) ->
+ tris = 18
+ r = {
+ vec4(w1.s1,w1.t1,1,1),
+ vec4(w1.s1,w1.t2,1,1),
+ vec4(w1.s2,w1.t2,1,1),
+ vec4(w1.s2,w1.t2,1,1),
+ vec4(w1.s2,w1.t1,1,1),
+ vec4(w1.s1,w1.t1,1,1),
+ }
+ step = (2*math.pi)/tris
+ for i = 0,2*math.pi,step
+ perc = (i / (2*math.pi))
+ nextperc = ((i+step) / (2*math.pi))
+ srange = w1.s2 - w1.s1
+ trange = w1.t2 - w1.t1
+ sstart = w1.s1 + (srange * perc)
+ send = w1.s1 + (srange * nextperc)
+ tstart = w1.t1
+ tend = w1.t2
+ r[#r+1] = vec4(sstart ,tstart,1,1)
+ r[#r+1] = vec4(sstart ,tend,1,1)
+ r[#r+1] = vec4(send ,tend,1,1)
+ r[#r+1] = vec4(send ,tend,1,1)
+ r[#r+1] = vec4(send ,tstart,1,1)
+ r[#r+1] = vec4(sstart ,tstart,1,1)
+ r
+
+barrel_r = (x,y,w,h) ->
+ r = {-1,-1,1,1,1,-1,0,0,0,0,0,0}
+ r
+
+
+-- uvs are s,t,smult, tmult
+up_down_hall_uv = (x,y) ->
+ r = {
+ --floor
+ vec4(sd.s1,sd.t1,1,1),
+ vec4(sd.s2,sd.t1,1,1),
+ vec4(sd.s2,sd.t2,1,1),
+ vec4(sd.s2,sd.t2,1,1),
+ vec4(sd.s1,sd.t2,1,1),
+ vec4(sd.s1,sd.t1,1,1),
+ -- left wall
+ vec4(w1.s1,w1.t1,1,1),
+ vec4(w1.s1,w1.t2,1,1),
+ vec4(w1.s2,w1.t2,1,1),
+ vec4(w1.s2,w1.t2,1,1),
+ vec4(w1.s2,w1.t1,1,1),
+ vec4(w1.s1,w1.t1,1,1),
+ -- right wall
+ vec4(w1.s2,w1.t2,1,1),
+ vec4(w1.s2,w1.t1,1,1),
+ vec4(w1.s1,w1.t1,1,1),
+ vec4(w1.s1,w1.t1,1,1),
+ vec4(w1.s1,w1.t2,1,1),
+ vec4(w1.s2,w1.t2,1,1),
+
+ }
+ r
+
+up_down_hall_r = (x,y) ->
+ r = {
+ 0,0,0,0,0,0
+ 0,0,0,0,0,0
+ 0,0,0,0,0,0
+ 0,0,0,0,0,0
+ 0,0,0,0,0,0
+ 0,0,0,0,0,0
+ }
+ r
+
+-- Barrel?
+
+
+add_verts = (tbl, new) ->
+ for i = 1, #new
+ tbl[#tbl+1] = new[i]
+
+world_geom = {}
+world_uv = {}
+world_r = {}
+add_verts(world_geom, up_down_hall(0,-1))
+add_verts(world_uv, up_down_hall_uv(0,0))
+add_verts(world_r, up_down_hall_r(0,0))
+add_verts(world_geom, up_down_hall(0,1))
+add_verts(world_uv, up_down_hall_uv(0,0))
+add_verts(world_r, up_down_hall_r(0,0))
+add_verts(world_geom, up_down_hall(0,0))
+add_verts(world_uv, up_down_hall_uv(0,0))
+add_verts(world_r, up_down_hall_r(0,0))
+add_verts(world_geom, barrel(-1,0,0.5,0.5))
+add_verts(world_uv, barrel_uv(-1,0,0.5,0.5))
+add_verts(world_r, barrel_r(-1,0,0.5,0.5))
+--add_verts(world_geom, barrel(0.5,0.5,0.5,0.5))
+--add_verts(world_uv, barrel_uv(0.5,0.5,0.5,0.5))
+--add_verts(world_r, barrel_r(0.5,0.5,0.5,0.5))
+--sprites["diffuse"].texture.wrap = "repeat"
+--sprites["normals"].texture.wrap = "repeat"
+
+test_world = {
+ vec3(0,0,0),
+ vec3(1,0,0),
+ vec3(0,1,0)
+}
+test_uvs = {
+ vec4(sd.s1,sd.t1,1,1),
+ vec4(sd.s2,sd.t1,1,1),
+ vec4(sd.s2,sd.t2,1,1),
+}
+test_r = {
+ 0,0,0
+}
+
+-- How big should our buffer for the world be?
+-- Let's call it at up to 100 players
+--MAX_PLAYERS = 128
+MAX_PLAYERS = 8
+--MAX_LEVEL_TRIS = 1 * 1024 * 1024 -- 1M level triangles?
+MAX_LEVEL_TRIS = 1024 -- testing?
+buffer_tris = (MAX_PLAYERS * 2) + MAX_LEVEL_TRIS
+geom_buffer = am.buffer(buffer_tris * 3 * 12) --am.vec3_array
+uv_buffer = am.buffer(buffer_tris * 3 * 16) --am.vec2_array
+geom_view = geom_buffer\view("vec3")
+uv_view = uv_buffer\view("vec4")
+for i = 1, #world_geom
+ geom_view[i] = world_geom[i]
+
+for i = 1, #world_uv
+ uv_view[i] = world_uv[i]
+
+world.geom_view = geom_view
+world.uv_view = uv_view
+
+buf_cursor = #world_geom -- the vertex number
+shimworld = shader_shim.world
+depth_test = am.depth_test("less")
+shimworld\append(depth_test)
+--cull = am.cull_face("back")
+--depth_test\append(cull)
+
+--{
+ --MV: s_mv
+ --P: mat4(1)
+ --world_x: math.sin(am.current_time!) * 2
+ --world_y: math.cos(am.current_time!) * 2
+ ----world_x: 0,
+ ----world_y: 0,
+ --world: geom_view,
+ --texuv: uv_view,
+ ----world: am.vec3_array(world_geom)
+ ----texuv: am.vec4_array(world_uv)
+ ----r: am.float_array(world_r)
+ ----world:am.vec3_array(test_world)
+ ----texuv: am.vec4_array(test_uvs)
+ ----r: am.float_array(test_r)
+ --time: am.current_time(),
+ --textures: sprites.floor.texture
+--}
+
+-- In the example we have
+-- am.group (buildings_group)
+-- ^ many am.bind(...)
+-- ^ am.draw("triangles")
+
+
+bind = am.group!
+depth_test\append(bind)
+
+add = (n) ->
+ assert(n.tris)
+ assert(n.populate_buf)
+ assert(n.properties)
+ assert(n.properties.graphic)
+ --assert(buf_cursor + n_tris < buffer_tris, "Not enough tris! Had " .. buf_cursor .. " and wanted " .. n_tris .. " so we sould end up with " .. (buf_cursor + n_tris) .. " but we only have " .. buffer_tris)
+ n_tris = n\tris!
+ log.info(string.format("Adding %d tris to buffer at %d",n_tris, buf_cursor),{"graphics"})
+ geom_buffer = am.buffer(n_tris * 3 * 12) --am.vec3_array
+ uv_buffer = am.buffer(n_tris * 3 * 16) --am.vec2_array
+ geom_view = geom_buffer\view("vec3")
+ uv_view = uv_buffer\view("vec4")
+ texture = n.properties.graphic.texture
+ n\populate_buf(geom_view, uv_view, 1)
+ tbind = am.bind({
+ MV: s_mv
+ P: p_mv
+ world_x: math.sin(am.current_time!) * 2
+ world_y: math.cos(am.current_time!) * 2
+ world: geom_view,
+ texuv: uv_view,
+ time: am.current_time!
+ textures: texture
+ })
+ tbind\append(am.draw("triangles"))
+ bind\append(tbind)
+ n.node = tbind
+ --buf_cursor += n_tris * 6
+
+remove = (n) ->
+ bind\remove(n.node)
+
+clear = () ->
+ log.info("Clearing shader!")
+ geom_view\set(vec3(0,0,0),1,buf_cursor)
+ uv_view\set(vec4(0,0,0,0),1,buf_cursor)
+ buf_cursor = 1
+
+
+--draw = am.draw("triangles", 1, buf_cursor)
+--bind\append(draw)
+
+--world.geom = binds.geom
+--world.texuv = binds.texuv
+
+shimworld\action(() =>
+ binds = bind\all("bind")
+ binds.time = am.current_time!
+ --bind.world_x = math.sin(am.current_time!) * 2
+ --bind.world_y = math.cos(am.current_time!) * 2
+ binds.world_x = world.world_x
+ binds.world_y = world.world_y
+)
+node = shimworld
+
+--node = shader_shim.world\append(am.depth_test("less")\append(am.cull_face("front")\append(am.bind({ -- should cull front
+ --MV: s_mv
+ --P: mat4(1)
+ --color: color.am_color.highlight,
+ --world_x: 0,
+ --world_y: 0,
+ --world: am.vec3_array(world_geom)
+ --texuv: am.vec4_array(world_uv)
+ --r: am.float_array(world_r)
+ ----world:am.vec3_array(test_world)
+ ----texuv: am.vec4_array(test_uvs)
+ ----r: am.float_array(test_r)
+ --time: am.current_time(),
+ --textures: sprites.floor1_diffuse.texture
+--})\append(am.draw("triangles")))))
+--node\action(() =>
+ --bind = self("bind")
+ --bind.time = am.current_time!
+ --bind.world_x = math.sin(am.current_time!) * 2
+ --bind.world_y = math.cos(am.current_time!) * 2
+ ----bind.world_x = world.world_x
+ ----bind.world_y = world.world_y
+--)
+{
+ :add
+ :clear
+ :remove
+ node: node
+ bind: node("bind")
+}
diff --git a/src/shaders/world.moon.back b/src/shaders/world.moon.back
new file mode 100644
index 0000000..7950f51
--- /dev/null
+++ b/src/shaders/world.moon.back
@@ -0,0 +1,327 @@
+win = require("window")
+color = require("color")
+world = require("world")
+sprites = require("sprites")
+shader_shim = require("shader_shim")
+hc = require("party.hc.init")
+log = require("log")
+PlayerGraphicComponent = require("ecs.player_graphic")
+assert(world.world_x, "No world_x" .. debug.traceback())
+-- Process the world into buffers to send to the shader
+
+view_angle = math.pi / 4
+near_plane = 1
+far_plane = 2
+aspect = win.width / win.height
+s_mv = mat4(
+ 1, 0, 0, 0,
+ 0, aspect, 0, 0,
+ 0, 0, 1, 0,
+ -0.0, 0.5, 0, 4
+ )
+p_mv = mat4(
+ 1 / ((win.width / win.height) * math.tan(view_angle / 2)), 0, 0, 0,
+ 0, 1/math.tan(view_angle/2), 0, 0,
+ 0, 0, far_plane / (far_plane - near_plane), 1,
+ 0, 0,(-far_plane * near_plane)/(far_plane - near_plane), 0
+)
+
+sd = sprites.floor
+w1 = sprites.wall
+
+-- Each point needs:
+-- vec3 position (x,y,z)
+-- vec2 (u,v)
+up_down_hall = (x,y) ->
+ r = {
+ --floor
+ vec3(x,y,0),
+ vec3(x+1,y,0),
+ vec3(x+1,y-1,0),
+ vec3(x+1,y-1,0),
+ vec3(x,y-1,0),
+ vec3(x,y,0),
+
+ -- Left wall
+ vec3(x,y,1),
+ vec3(x,y,0),
+ vec3(x,y-1,0),
+ vec3(x,y-1,0),
+ vec3(x,y-1,1),
+ vec3(x,y,1),
+
+ --Right wall
+ vec3(x+1,y,0),
+ vec3(x+1,y,1),
+ vec3(x+1,y-1,1),
+ vec3(x+1,y-1,1),
+ vec3(x+1,y-1,0),
+ vec3(x+1,y,0),
+ }
+ r
+
+room = (x,y,w,h,left_holes,right_holes,top_holes,bottom_holes) ->
+ left_holes\sort()
+ right_holes\sort()
+ top_holes\sort()
+ bottom_holes\sort()
+ r = {
+ --floor
+ vec3(x,y,0)
+ vec3(x+w,y,0),
+ vec3(x+w,y-h,0),
+ vec3(x+w,y-h,0),
+ vec3(x,y-h,0),
+ vec3(x,y,0),
+
+ --left wall
+ }
+ r
+
+barrel = (x,y,w,h) ->
+ tris = 18
+ rad = (w/2)
+ l = x - (w/2)
+ j = x + (w/2)
+ t = h
+ b = 0
+ f = y - (w/2)
+ n = y + (w/2)
+ r = {
+ --top
+ vec3(l,f,h),
+ vec3(l,n,h),
+ vec3(j,n,h),
+ vec3(j,n,h),
+ vec3(j,f,h),
+ vec3(l,f,h),
+ }
+ step = (2*math.pi)/tris
+ for i = 0,2*math.pi,step
+ r[#r+1] =vec3(x + math.cos(i)*rad,y + math.sin(i)*n,h)
+ r[#r+1] =vec3(x + math.cos(i+step)*rad,math.sin(i+step)*n,h)
+ r[#r+1] =vec3(x + math.cos(i+step)*rad,math.sin(i+step)*n,0)
+ r[#r+1] =vec3(x + math.cos(i+step)*rad,math.sin(i+step)*n,0)
+ r[#r+1] =vec3(x + math.cos(i)*rad,math.sin(i)*n,0)
+ r[#r+1] =vec3(x + math.cos(i)*rad,math.sin(i)*n,h)
+ r
+
+barrel_uv = (x,y,w,h) ->
+ tris = 18
+ r = {
+ vec4(w1.s1,w1.t1,1,1),
+ vec4(w1.s1,w1.t2,1,1),
+ vec4(w1.s2,w1.t2,1,1),
+ vec4(w1.s2,w1.t2,1,1),
+ vec4(w1.s2,w1.t1,1,1),
+ vec4(w1.s1,w1.t1,1,1),
+ }
+ step = (2*math.pi)/tris
+ for i = 0,2*math.pi,step
+ perc = (i / (2*math.pi))
+ nextperc = ((i+step) / (2*math.pi))
+ srange = w1.s2 - w1.s1
+ trange = w1.t2 - w1.t1
+ sstart = w1.s1 + (srange * perc)
+ send = w1.s1 + (srange * nextperc)
+ tstart = w1.t1
+ tend = w1.t2
+ r[#r+1] = vec4(sstart ,tstart,1,1)
+ r[#r+1] = vec4(sstart ,tend,1,1)
+ r[#r+1] = vec4(send ,tend,1,1)
+ r[#r+1] = vec4(send ,tend,1,1)
+ r[#r+1] = vec4(send ,tstart,1,1)
+ r[#r+1] = vec4(sstart ,tstart,1,1)
+ r
+
+barrel_r = (x,y,w,h) ->
+ r = {-1,-1,1,1,1,-1,0,0,0,0,0,0}
+ r
+
+
+-- uvs are s,t,smult, tmult
+up_down_hall_uv = (x,y) ->
+ r = {
+ --floor
+ vec4(sd.s1,sd.t1,1,1),
+ vec4(sd.s2,sd.t1,1,1),
+ vec4(sd.s2,sd.t2,1,1),
+ vec4(sd.s2,sd.t2,1,1),
+ vec4(sd.s1,sd.t2,1,1),
+ vec4(sd.s1,sd.t1,1,1),
+ -- left wall
+ vec4(w1.s1,w1.t1,1,1),
+ vec4(w1.s1,w1.t2,1,1),
+ vec4(w1.s2,w1.t2,1,1),
+ vec4(w1.s2,w1.t2,1,1),
+ vec4(w1.s2,w1.t1,1,1),
+ vec4(w1.s1,w1.t1,1,1),
+ -- right wall
+ vec4(w1.s2,w1.t2,1,1),
+ vec4(w1.s2,w1.t1,1,1),
+ vec4(w1.s1,w1.t1,1,1),
+ vec4(w1.s1,w1.t1,1,1),
+ vec4(w1.s1,w1.t2,1,1),
+ vec4(w1.s2,w1.t2,1,1),
+
+ }
+ r
+
+up_down_hall_r = (x,y) ->
+ r = {
+ 0,0,0,0,0,0
+ 0,0,0,0,0,0
+ 0,0,0,0,0,0
+ 0,0,0,0,0,0
+ 0,0,0,0,0,0
+ 0,0,0,0,0,0
+ }
+ r
+
+-- Barrel?
+
+
+add_verts = (tbl, new) ->
+ for i = 1, #new
+ tbl[#tbl+1] = new[i]
+
+world_geom = {}
+world_uv = {}
+world_r = {}
+add_verts(world_geom, up_down_hall(0,-1))
+add_verts(world_uv, up_down_hall_uv(0,0))
+add_verts(world_r, up_down_hall_r(0,0))
+add_verts(world_geom, up_down_hall(0,1))
+add_verts(world_uv, up_down_hall_uv(0,0))
+add_verts(world_r, up_down_hall_r(0,0))
+add_verts(world_geom, up_down_hall(0,0))
+add_verts(world_uv, up_down_hall_uv(0,0))
+add_verts(world_r, up_down_hall_r(0,0))
+add_verts(world_geom, barrel(-1,0,0.5,0.5))
+add_verts(world_uv, barrel_uv(-1,0,0.5,0.5))
+add_verts(world_r, barrel_r(-1,0,0.5,0.5))
+--add_verts(world_geom, barrel(0.5,0.5,0.5,0.5))
+--add_verts(world_uv, barrel_uv(0.5,0.5,0.5,0.5))
+--add_verts(world_r, barrel_r(0.5,0.5,0.5,0.5))
+--sprites["diffuse"].texture.wrap = "repeat"
+--sprites["normals"].texture.wrap = "repeat"
+
+test_world = {
+ vec3(0,0,0),
+ vec3(1,0,0),
+ vec3(0,1,0)
+}
+test_uvs = {
+ vec4(sd.s1,sd.t1,1,1),
+ vec4(sd.s2,sd.t1,1,1),
+ vec4(sd.s2,sd.t2,1,1),
+}
+test_r = {
+ 0,0,0
+}
+
+-- How big should our buffer for the world be?
+-- Let's call it at up to 100 players
+--MAX_PLAYERS = 128
+MAX_PLAYERS = 8
+--MAX_LEVEL_TRIS = 1 * 1024 * 1024 -- 1M level triangles?
+MAX_LEVEL_TRIS = 1024 -- testing?
+buffer_tris = (MAX_PLAYERS * PlayerGraphicComponent.tris!) + MAX_LEVEL_TRIS
+geom_buffer = am.buffer(buffer_tris * 3 * 12) --am.vec3_array
+uv_buffer = am.buffer(buffer_tris * 3 * 16) --am.vec2_array
+geom_view = geom_buffer\view("vec3")
+uv_view = uv_buffer\view("vec4")
+for i = 1, #world_geom
+ geom_view[i] = world_geom[i]
+
+for i = 1, #world_uv
+ uv_view[i] = world_uv[i]
+
+world.geom_view = geom_view
+world.uv_view = uv_view
+
+buf_cursor = #world_geom -- the vertex number
+add = (n) ->
+ assert(n.tris)
+ assert(n.populate_buf)
+ n_tris = n\tris!
+ assert(buf_cursor + n_tris < buffer_tris, "Not enough tris! Had " .. buf_cursor .. " and wanted " .. n_tris .. " so we sould end up with " .. (buf_cursor + n_tris) .. " but we only have " .. buffer_tris)
+ log.info(string.format("Adding %d tris to buffer at %d",n_tris, buf_cursor),{"graphics"})
+ n\populate_buf(geom_view, uv_view, buf_cursor)
+ buf_cursor += n_tris * 6
+
+clear = () ->
+ log.info("Clearing shader!")
+ geom_view\set(vec3(0,0,0),1,buf_cursor)
+ uv_view\set(vec4(0,0,0,0),1,buf_cursor)
+ buf_cursor = 1
+
+shimworld = shader_shim.world
+depth_test = am.depth_test("less")
+shimworld\append(depth_test)
+cull = am.cull_face("front")
+depth_test\append(cull)
+binds = {
+ MV: s_mv
+ P: mat4(1)
+ color: color.am_color.highlight,
+ world_x: 0,
+ world_y: 0,
+ world: geom_view,
+ texuv: uv_view,
+ --world: am.vec3_array(world_geom)
+ --texuv: am.vec4_array(world_uv)
+ --r: am.float_array(world_r)
+ --world:am.vec3_array(test_world)
+ --texuv: am.vec4_array(test_uvs)
+ --r: am.float_array(test_r)
+ time: am.current_time(),
+ textures: sprites.floor.texture
+}
+bind = am.bind(binds)
+cull\append(bind)
+draw = am.draw("triangles", 1, buf_cursor)
+bind\append(draw)
+
+world.geom = binds.geom
+world.texuv = binds.texuv
+
+shimworld\action(() =>
+ bind = self("bind")
+ bind.time = am.current_time!
+ --bind.world_x = math.sin(am.current_time!) * 2
+ --bind.world_y = math.cos(am.current_time!) * 2
+ bind.world_x = world.world_x
+ bind.world_y = world.world_y
+)
+node = shimworld
+
+--node = shader_shim.world\append(am.depth_test("less")\append(am.cull_face("front")\append(am.bind({ -- should cull front
+ --MV: s_mv
+ --P: mat4(1)
+ --color: color.am_color.highlight,
+ --world_x: 0,
+ --world_y: 0,
+ --world: am.vec3_array(world_geom)
+ --texuv: am.vec4_array(world_uv)
+ --r: am.float_array(world_r)
+ ----world:am.vec3_array(test_world)
+ ----texuv: am.vec4_array(test_uvs)
+ ----r: am.float_array(test_r)
+ --time: am.current_time(),
+ --textures: sprites.floor1_diffuse.texture
+--})\append(am.draw("triangles")))))
+--node\action(() =>
+ --bind = self("bind")
+ --bind.time = am.current_time!
+ --bind.world_x = math.sin(am.current_time!) * 2
+ --bind.world_y = math.cos(am.current_time!) * 2
+ ----bind.world_x = world.world_x
+ ----bind.world_y = world.world_y
+--)
+{
+ :add
+ :clear
+ node: node
+ bind: node("bind")
+}
diff --git a/src/shaders/world.vert b/src/shaders/world.vert
new file mode 100644
index 0000000..5937253
--- /dev/null
+++ b/src/shaders/world.vert
@@ -0,0 +1,26 @@
+precision highp float;
+attribute vec3 world; // position
+attribute vec2 texuv;
+attribute float r; // for round objects, 0 for non-round
+varying vec2 textureuv;
+varying float radius;
+varying mat3 light1;
+varying vec4 v_color;
+uniform float world_x;
+uniform float world_y;
+uniform mat4 MV;
+uniform mat4 P;
+void main() {
+ v_color = vec4(world.xyz,1.);
+ vec2 vxy = vec2(world.x - world_x, world.y - world_y);
+ float z_scale = 0.5;
+ float max_parallax = 0.5;
+ float xoff = clamp(world.z * vxy.x * z_scale, -max_parallax, max_parallax);
+ float yoff = clamp(world.z * vxy.y * z_scale, -max_parallax, max_parallax);
+ textureuv=texuv;
+ //radius = r;
+ // if z > 0 then
+ // xoff = ceil(xoff, 0)
+ // add to the z coord so we don't intersect with the ui
+ gl_Position = P * MV * vec4(vxy.x + xoff, vxy.y + yoff, world.z, 1.0);
+}
diff --git a/src/shaders/world.vert.back b/src/shaders/world.vert.back
new file mode 100644
index 0000000..42276fe
--- /dev/null
+++ b/src/shaders/world.vert.back
@@ -0,0 +1,26 @@
+precision highp float;
+attribute vec3 world; // position
+attribute vec2 texuv;
+attribute float r; // for round objects, 0 for non-round
+varying vec2 textureuv;
+varying float radius;
+varying mat3 light1;
+uniform vec4 color;
+varying vec4 v_color;
+uniform float world_x;
+uniform float world_y;
+uniform mat4 MV;
+uniform mat4 P;
+void main() {
+ v_color = vec4(world.xyz,1.);
+ vec2 vxy = vec2(world.x - world_x, world.y - world_y);
+ float z_scale = 0.5;
+ float xoff = clamp(world.z * vxy.x * z_scale, -32., 32.);
+ float yoff = clamp(world.z * vxy.y * z_scale, -32., 32.);
+ textureuv=texuv;
+ //radius = r;
+ // if z > 0 then
+ // xoff = ceil(xoff, 0)
+ // add to the z coord so we don't intersect with the ui
+ gl_Position = P * MV * vec4(vxy.x + xoff, vxy.y + yoff, -world.z -1., 1.0);
+}
diff --git a/src/shared/player_movement.moon b/src/shared/player_movement.moon
new file mode 100644
index 0000000..31aada4
--- /dev/null
+++ b/src/shared/player_movement.moon
@@ -0,0 +1,54 @@
+-- we want to find the location based on inital velocity and position, constant acceleration, and delta time
+-- Each function should be called with a `PredictedComponent` as `self`.
+
+-- In a normal simulation, velocity adds
+-- acceleration * delta time
+-- every tick, minus some friction:
+-- coefficient * current velocity
+-- i.e. velocity = (acceleration * delta) - (friction * velocity)
+-- every tick
+-- velocity[tick] = (acceleration * delta[tick]) - (friction * velocity[tick - 1])
+-- velocity[4] = (acceleration * delta[4]) - (friction * velocity[3])
+-- = (acceleration * delta[4]) - (friction * ((acceleration * delta[3]) - (friction * velocity[2])))
+-- = (acceleration * delta[4]) - (friction * ((acceleration * delta[3]) - (friction * ((acceleration * delta[2]) - (friction * velocity[inital])))))
+-- = (acceleration * delta[4]) - (friction * ((acceleration * delta[3]) - ((friction * acceleration * delta[2]) - (friction * friction * velocity[inital]))))
+-- = (acceleration * delta[4]) - (friciton * ((acceleration * delta[3]) - (friction * acceleration * delta[2]) + (friction^2 * velocity[inital])))
+-- = (acceleration * delta[4]) - ((friction * acceleration * delta[3]) - (friction * friction * acceleration * delta[2]) + (friction^3 * velocity[inital]))
+-- = (acceleration * delta[4]) - (friction * acceleration * delta[3]) + (friction^2 * acceleration * delta[2]) - (friction^3 * velocity[inital])
+-- as delta approaches 0 (high fidelity simulation), the middle components become e^(-friction * delta), and acceleration needs to be divided by friction
+-- Position is a second layer on top
+-- position[tick] = position[tick-1] + velocity[tick]
+-- position[2] = position[inital] + velocity[2]
+-- = position[inital] + (acceleration * delta[2]) - (friction * velocity[inital])
+-- position[delta] = (delta * (acceleration / friction) ) - ((1 / friction) * (velocity[inital] - (acceleratin / friction)) * e^(-friction * delta) + position[inital]
+
+friction = 0.3
+{
+ acc:() =>
+ acc = vec3(unpack(@net.properties.acc))
+ movement_speed = 1/1000 -- @net.properties.move_speed?
+ freeze = 1 -- @net.properties.frozen?
+ newacc = acc * (movement_speed) * (freeze)
+ {newacc.x, newacc.y, newacc.z}
+ vel: () =>
+ acc = vec3(unpack(@properties.acc))
+ vel = vec3(unpack(@net.properties.vel))
+ now = am.eval_js("Date.now();")
+ --print("Net is ", @net.properties)
+ delta = (now - @net.properties.last_update) / 1000
+ newvel = (acc / friction) + ((vel - (acc / friction)) * math.exp(-friction * delta))
+ {newvel.x, newvel.y, newvel.z}
+ pos: () =>
+ now = am.eval_js("Date.now();")
+ delta = (now - @net.properties.last_update) / 1000
+ vel = vec3(unpack(@properties.vel))
+ pos = vec3(unpack(@properties.pos))
+ acc = vec3(unpack(@properties.acc))
+ friction_loss = acc / friction
+ -- when delta = 0 (up to date)
+ -- pos = (1/friction) * (velocity - friction_loss) * 1 + position
+ -- = 2 * (2 - 2) * 1 + position
+ -- = position
+ newpos = (friction_loss * delta) - ((1/friction) * (vel - friction_loss) * (math.exp(-friction * delta))) + pos
+ {newpos.x, newpos.y, newpos.z}
+}
diff --git a/src/task.moon b/src/task.moon
new file mode 100644
index 0000000..342255e
--- /dev/null
+++ b/src/task.moon
@@ -0,0 +1,25 @@
+task = {}
+tasks = {}
+
+task.add = (co) ->
+ tasks[co] = true
+
+task.pump = () ->
+ for task, _ in pairs tasks
+ if coroutine.status(task) ~= "dead"
+ succ, err = coroutine.resume(task)
+ if not succ
+ error(debug.traceback(task, err))
+ else
+ tasks[task] = nil
+
+task.await = (co) ->
+ if tasks[co]
+ coroutine.yield!
+
+task.node = am.group!
+task.node\action(() ->
+ task.pump!
+)
+
+task
diff --git a/src/textbox_bridge.js b/src/textbox_bridge.js
new file mode 100644
index 0000000..c52902e
--- /dev/null
+++ b/src/textbox_bridge.js
@@ -0,0 +1,74 @@
+
+var i = 0;
+/* Detour SDL.receiveEvent to we can focus textboxes
+ */
+
+var oldReceive = SDL.receiveEvent;
+SDL.receiveEvent = function(e){
+ console.log("Intercepting event!");
+ return oldReceive(e);
+};
+window.TEXTBOX = {
+ create_textbox: function(tbl) {
+ var value = tbl.value;
+ var palceholder = tbl.placeholder;
+ var s = document.createElement('input');
+ s.setAttribute("type","text");
+ s.setAttribute("id","textbox" + i);
+ s.setAttribute("style","z-index: 1; position:absolute; visibility:hidden;");
+ var p = document.getElementById("container");
+ var noop = function(){};
+ p.prepend(s);
+ console.log("[JS] Added textbox" + i, s);
+ /* None of these work, amulet intercepts the keys */
+ /*
+ s.addEventListener("keypress",function(e) {
+ console.log("Keypress on the textbox");
+ });
+ s.addEventListener("keyup",function(e) {
+ console.log("keyup on the textbox");
+ });
+ s.addEventListener("keydown",function(e) {
+ console.log("keydown on the textbox");
+ });
+ */
+ s.addEventListener("focusin",function(e){
+ e.preventDefault = noop;
+ });
+ s.addEventListener("focusout",function(e){
+ e.preventDefault = noop;
+ });
+ // When we get an event, stop amulet from doing .preventDefault()
+ s.addEventListener("keypress",function(e){
+ e.preventDefault = noop;
+ });
+ s.addEventListener("keyup",function(e){
+ e.preventDefault = noop;
+ });
+ s.addEventListener("keydown",function(e){
+ e.preventDefault = noop;
+ });
+ i++;
+ return i;
+ },
+ focus: function(tbl) {
+ var id = tbl.id;
+ var e = document.getElementById("textbox" + id);
+ e.setAttribute("style","z-index: 1; position:absolute;");
+ console.log("[JS] Clicking element", e);
+ e.focus();
+ },
+ blur: function(tbl) {
+ var id = tbl.id;
+ var e = document.getElementById("textbox" + id);
+ e.setAttribute("style","position:absolute; visibility:hidden;");
+ console.log("[JS] Bluring element", e);
+ e.blur();
+ },
+ get_text: function(tbl) {
+ var id = tbl.id;
+ var e = document.getElementById("textbox" + id);
+ console.log("Getting text",e.text);
+ return e.value;
+ }
+};
diff --git a/src/ui.moon b/src/ui.moon
new file mode 100644
index 0000000..25847bd
--- /dev/null
+++ b/src/ui.moon
@@ -0,0 +1,269 @@
+hc = require("party.hc.init")
+win = require("window")
+log = require("log")
+util = require("util")
+Button = require("ui.button")
+Joystick = require("ui.joystick")
+Textbox = require("ui.textbox")
+sprites = require("sprites")
+color = require("color")
+
+ui_world = hc.new(64)
+
+am.eval_js(require("controller_bridge"))
+
+ui = {}
+ui.events = {
+ touch: {}
+ mouse: {}
+ controller: {}
+ keyboard: {}
+}
+ui.button = (x,y,width,height,text,controller_binds,font) ->
+ font = font or sprites.yataghan64
+ log.info(string.format("Creating button at (%d, %d) with size (%d, %d) and text %q",x,y,width,height,text),{"ui"})
+ controller_binds = controller_binds or {}
+ assert(x and type(x) == "number", "x must be anumber")
+ assert(y and type(y) == "number", "y must be anumber")
+ assert(width and type(width) == "number", "width must be anumber")
+ assert(height and type(height) == "number", "height must be anumber")
+ button = Button(x,y,width,height,text, font)
+ ui.node\append(button.node)
+ bounds = ui_world\rectangle(x,y,width,height)
+ ui.events.touch[bounds] = button
+ ui.events.mouse[bounds] = button
+ for bind in *controller_binds
+ ui.events.controller[bind] = button
+ button
+
+ui.click = (x,y) ->
+ ui_world\shapesAt(x,y)
+
+ui.joystick = (x,y,r,controller_binds) ->
+ controller_binds = controller_binds or {}
+ joystick = Joystick(x,y,r)
+ ui.node\append(joystick.node)
+ bounds = ui_world\circle(x,y,r)
+ ui.events.touch[bounds] = joystick
+ for bind in *controller_binds
+ ui.events.controller[bind] = joystick
+ joystick
+
+ui.text = (x,y,width,height,text) ->
+ line_height = 16
+ text = text or ""
+ rope = {
+ raw: text
+ width: 0
+ height: 0
+ lines: {} --list of line, a line is:
+ -- {
+ -- raw: string
+ -- tokens: token[]
+ -- width: number (pixels)
+ -- height: number (pixels)
+ -- }
+ -- A token is:
+ -- {
+ -- raw: string
+ -- width: number (pixels)
+ -- height: number (pixels)
+ -- }
+ }
+ current_line = {
+ raw: ""
+ tokens: {}
+ width: 0
+ height: 0
+ }
+ for word in text\gmatch("(%S+)")
+ t = am.text(sprites.yataghan32, " " .. word)
+ tps = t.width
+ if tps + current_line.width > width -- create a new line
+ rope.width = math.max(rope.width, current_line.width)
+ rope.height += line_height + current_line.height
+ if #rope.lines == 0 -- no line height for the first line
+ rope.height -= line_height
+ table.insert(rope.lines, current_line)
+ t = am.text(sprites.yataghan32, word)
+ token = {
+ raw: word
+ width: t.width
+ height: t.height
+ }
+ current_line = {
+ raw: word
+ tokens: {
+ token
+ }
+ width: t.width
+ height: t.height
+ }
+ else -- append token to this line
+ t = am.text(sprites.yataghan32, word)
+ token = {
+ raw: word
+ width: t.width
+ height: t.height
+ }
+ current_line.raw = current_line.raw .. " " .. word
+ current_line.width += tps
+ current_line.height = math.max(current_line.height, t.height)
+ table.insert(current_line.tokens, token)
+ if #current_line.tokens > 0
+ table.insert(rope.lines, current_line)
+ group = am.group!
+ y_cursor = -line_height -- no line height for first line
+ for i = 1, #rope.lines
+ y_cursor -= line_height
+ line_pos = am.translate(x, y + y_cursor)
+ line_text = am.text(sprites.yataghan32, rope.lines[i].raw,color.am_color.foreground)
+ line_pos\append(line_text)
+ group\append(line_pos)
+ y_cursor -= rope.lines[i].height
+ rope.height = -y_cursor
+ if rope.height == 0
+ rope.height = 1
+ if rope.width == 0
+ rope.width = 1
+ bounds = ui_world\rectangle(x,y,rope.width, rope.height)
+ ui.node\append(group)
+ element = {
+ node: group
+ rope: rope
+ }
+ -- No events?
+ element
+
+ui.textbox = (x,y,width,height,value,placeholder) ->
+ value = value or ""
+ placeholder = placeholder or ""
+ textbox = Textbox(x,y,width,height,value,placeholder)
+ ui.node\append(textbox.node)
+ bounds = ui_world\rectangle(x,y,width,height)
+ ui.events.mouse[bounds] = textbox
+ ui.events.keyboard[textbox] = true
+ textbox
+
+ui.delete = (element) ->
+ ui.node\remove(element.node)
+ for b,e in pairs(ui.events.mouse)
+ if e == element
+ ui.events.mouse[b] = nil
+ if ui.events.keyboard[element]
+ ui.events.keyboard[element] = nil
+ for b,e in pairs(ui.events.touch)
+ if e == element
+ ui.events.touch[b] = nil
+ for b,e in pairs(ui.events.controller)
+ if e == element
+ ui.events.controller[b] = nil
+
+ui.node = am.group!
+
+has_fire = (obj) ->
+ assert(obj.fire, obj.__class.__name .. " doesn't have a .fire method")
+
+--ui.dbg = am.translate(0,0)\append(am.circle(vec2(0,0),5,vec4(0,0,0,1)))\append(am.text("Hello, world!"))
+--ui.node\append(ui.dbg)
+
+ui.node\action(() ->
+ pos = win\mouse_position()
+ down = win\mouse_pressed("left")
+ up = win\mouse_released("left")
+ wheel = win\mouse_wheel_delta()
+ keys = win\keys_pressed()
+ am.eval_js("CONT.loop()")
+ cont_state = am.eval_js("CONT.last_state")
+ -- Debugging for mouse position:
+ --ui.dbg.position2d = pos
+ mo_tbl =
+ event: "mouse_over"
+ data: pos
+ md_tbl =
+ event: "mouse_down"
+ data: pos
+ mu_tbl =
+ event: "mouse_up"
+ data: pos
+ for collider,_ in pairs(ui_world\shapesAt(pos.x, pos.y))
+ match = ui.events.mouse[collider]
+ if match
+ has_fire(match)
+ --log.info("Found button under mouse:" .. tostring(match), {"ui","mouseover"})
+ match\fire(mo_tbl)
+ if down
+ log.info("Found button under mouse:" .. tostring(match), {"ui","mousedown"})
+ match\fire(md_tbl)
+ if up
+ log.info("Found button under mouse:" .. tostring(match), {"ui","mouseup"})
+ match\fire(mu_tbl)
+ if math.length(wheel) > 0
+ etbl =
+ event: "mouse_scroll"
+ data: wheel
+ for collider, uiobj in pairs(ui.events.mouse)
+ has_fire(uiobj)
+ uiobj\fire(etbl)
+ if #keys > 0
+ --print("Got keys:" .. tostring(keys))
+ etbl =
+ event: "keys_pressed"
+ data: keys
+ shift: win\key_down("lshift") or win\key_down("rshift")
+ for uiobj, _ in pairs(ui.events.keyboard)
+ has_fire(uiobj)
+ if uiobj\fire(etbl)
+ break -- allow any keyboard listener to "trap" the signal by returning true
+ if cont_state.on
+ for axis,value in pairs(cont_state.axes)
+ name = "axis" .. axis
+ uiobj = ui.events.controller[name]
+ if uiobj and has_fire(uiobj)
+ etbl =
+ event: "controller_axis"
+ data: value
+ uiobj\fire(etbl)
+ for button,value in pairs(cont_state.buttons)
+ name = "button" .. button
+ uiobj = ui.events.controller[name]
+ if uiobj and has_fire(uiobj)
+ etbl =
+ event: "controller_pressed"
+ data: value
+ uiobj\fire(etbl)
+
+-- in_touch_events = {
+-- "active_touch"
+-- "touches_began"
+-- }
+-- for touch in *win\active_touches!
+-- tpos = win\touch_position(touch)
+-- etbl =
+-- event: "active_touch"
+-- data: tpos
+-- for collider,_ in pairs(ui_world\shapesAt(tpos.x, tpos.y))
+-- print("Touched collider:", collider)
+-- match = ui.events.touch[collider]
+-- if match
+-- has_fire(match)
+-- match\fire(etbl)
+-- delta = win\touch_delta(touch)
+-- if math.length(delta) > 0
+-- dtbl =
+-- event: "touch_delta"
+-- data: delta
+-- for _, uiobj in pairs(ui.events.touch)
+-- has_fire(uiobj)
+-- uiobj\fire(dtbl)
+-- for touch in *win\touches_ended!
+-- etbl =
+-- event: "touches_ended"
+-- data: win\touch_position(touch)
+-- for _,uiobj in pairs(ui.events.touch)
+-- has_fire(uiobj)
+-- uiobj\fire(etbl)
+ -- todo: expand this with controller support.
+)
+
+ui
diff --git a/src/ui/button.moon b/src/ui/button.moon
new file mode 100644
index 0000000..9b55a44
--- /dev/null
+++ b/src/ui/button.moon
@@ -0,0 +1,133 @@
+
+s = require("sprites")
+util = require("util")
+color = require("color")
+world = require("world")
+sprites = require("sprites")
+states = {"up","down"}
+rows = {"upper","mid","lower"}
+cols = {"left","mid","right"}
+class Button
+ --am.sprite() only works once the window is created
+ @initialized = false
+ @initialize: =>
+ for _, state, _, row, _, col in util.cartesian(states, rows, cols)
+ name = table.concat({state,row,col},"_")
+ assert(s["button_" .. name], "Failed to find sprite:" .. name)
+ @[name] = am.sprite(s["button_" .. name],"left","top")
+ @initialized = true
+ new: (x,y,w,h,text,font)=>
+ if not @@initialized
+ @@initialize!
+ @em = 16 -- width of a character
+ text = text or ""
+ assert(w > 15, "Button must have at least width 15")
+ @node = am.group!
+ position = am.translate(x,y+h)\tag("position")
+ @up_sprites = am.group!
+ position\append(@up_sprites)
+ @down_sprites = am.group!
+ @node\append(position)
+ @up_sprites\append(@@up_upper_left)
+ @up_sprites\append(
+ am.translate(@@up_upper_left.width,0)\append(
+ am.scale(w - @@up_upper_left.width - @@up_upper_right.width,1)\append(
+ @@up_upper_mid
+ )))
+ @up_sprites\append(
+ am.translate(w - @@up_upper_right.width, 0)\append(
+ @@up_upper_right
+ ))
+ mid_height = h - @@up_upper_left.height - @@up_lower_left.height
+ @up_sprites\append(
+ am.translate(0,-@@up_upper_left.height)\append(
+ am.scale(1,mid_height)\append(
+ @@up_mid_left
+ )))
+ @up_sprites\append(
+ am.translate(@@up_upper_left.width, -@@up_upper_left.height)\append(
+ am.scale(w - @@up_mid_left.width - @@up_mid_right.width, h - @@up_upper_mid.height - @@up_lower_mid.height)\append(
+ @@up_mid_mid
+ )))
+ @up_sprites\append(
+ am.translate(w - @@up_mid_right.width,-@@up_upper_right.height)\append(
+ am.scale(1,h - @@up_upper_right.height - @@up_lower_right.height)\append(
+ @@up_mid_right
+ )))
+ @up_sprites\append(
+ am.translate(0,-(h - @@up_lower_left.height))\append(
+ @@up_lower_left
+ ))
+ @up_sprites\append(
+ am.translate(@@up_lower_left.width,-(h - @@up_lower_mid.height))\append(
+ am.scale(w - @@up_lower_left.width - @@up_lower_right.width,1)\append(
+ @@up_lower_mid
+ )))
+ @up_sprites\append(
+ am.translate(w - @@up_lower_right.width, -(h - @@up_lower_right.height))\append(
+ @@up_lower_right
+ ))
+ @down_sprites\append(@@down_upper_left)
+ @down_sprites\append(
+ am.translate(@@down_upper_left.width,0)\append(
+ am.scale(w - @@down_upper_left.width - @@down_upper_right.width,1)\append(
+ @@down_upper_mid
+ )))
+ @down_sprites\append(
+ am.translate(w - @@down_upper_right.width, 0)\append(
+ @@down_upper_right
+ ))
+ mid_height = h - @@down_upper_left.height - @@down_lower_left.height
+ @down_sprites\append(
+ am.translate(0,-@@down_upper_left.height)\append(
+ am.scale(1,mid_height)\append(
+ @@down_mid_left
+ )))
+ @down_sprites\append(
+ am.translate(@@down_upper_left.width, -@@down_upper_left.height)\append(
+ am.scale(w - @@down_mid_left.width - @@down_mid_right.width, h - @@down_upper_mid.height - @@down_lower_mid.height)\append(
+ @@down_mid_mid
+ )))
+ @down_sprites\append(
+ am.translate(w - @@down_mid_right.width,-@@down_upper_right.height)\append(
+ am.scale(1,h - @@down_upper_right.height - @@down_lower_right.height)\append(
+ @@down_mid_right
+ )))
+ @down_sprites\append(
+ am.translate(0,-(h - @@down_lower_left.height))\append(
+ @@down_lower_left
+ ))
+ @down_sprites\append(
+ am.translate(@@down_lower_left.width,-(h - @@down_lower_mid.height))\append(
+ am.scale(w - @@down_lower_left.width - @@down_lower_right.width,1)\append(
+ @@down_lower_mid
+ )))
+ @down_sprites\append(
+ am.translate(w - @@down_lower_right.width, -(h - @@down_lower_right.height))\append(
+ @@down_lower_right
+ ))
+ if font
+ @text = am.text(font, text, "left","top", color.am_color.foreground)
+ else
+ @text = am.text(text, "left", "top", color.am_color.foreground)
+ position\append(
+ am.translate(@@down_upper_left.width, -@@down_upper_right.height)\append(
+ am.scale(world.controller.text_size)\append(
+ @text
+ )))
+ @depressed = false
+ down: () =>
+ @depressed = true
+ @.node("position")\replace(@up_sprites, @down_sprites)
+ up: () =>
+ @depressed = false
+ @.node("position")\replace(@down_sprites, @up_sprites)
+ fire: (e) =>
+ if e.event == "touches_began" or e.event == "mouse_down"
+ @down!
+ if @on
+ @on(e)
+ if e.event == "touches_ended" or e.event == "mouse_up"
+ @up!
+
+Button
diff --git a/src/ui/checkbox.moon b/src/ui/checkbox.moon
new file mode 100644
index 0000000..e76a790
--- /dev/null
+++ b/src/ui/checkbox.moon
@@ -0,0 +1,13 @@
+Button = require("ui.button")
+class Checkbox extends Button
+ fire: (e) =>
+ if e.event == "mouse_down"
+ if @depressed
+ @up!
+ else
+ @down!
+ --@down!
+ if @on
+ @on(@depressed)
+
+Checkbox
diff --git a/src/ui/joystick.moon b/src/ui/joystick.moon
new file mode 100644
index 0000000..98936ed
--- /dev/null
+++ b/src/ui/joystick.moon
@@ -0,0 +1,98 @@
+
+color = require("color")
+window = require("window")
+
+circle_cache = setmetatable({},{__mode: "v"})
+hollow_circle = (x,y,radius, thickness, color) ->
+ key = string.format("%d\0%d\0%d\0%d",x,y,radius, thickness)
+ if circle_cache[key]
+ return circle_cache[key]
+ arr = {}
+ segments = 60
+ step = (2*math.pi) / segments
+ for i = 0,2*math.pi, step
+ arr[#arr+1] = vec2(i+step, 1)
+ arr[#arr+1] = vec2(i, 1)
+ arr[#arr+1] = vec2(i+step,0)
+ arr[#arr+1] = vec2(i+step,0)
+ arr[#arr+1] = vec2(i,1)
+ arr[#arr+1] = vec2(i,0)
+ circle = am.use_program(am.program([[
+ precision highp float;
+ attribute vec2 index;
+ uniform float thickness;
+ uniform float radius;
+ uniform mat4 MV;
+ uniform mat4 P;
+ void main() {
+ float distance = thickness * index[1];
+ vec2 vert = vec2(cos(index[0]) * (radius - distance), sin(index[0]) * (radius - distance));
+ gl_Position = P * MV * vec4(vert, 0.0, 1.0);
+ }
+ ]],[[
+ precision mediump float;
+ uniform vec4 color;
+ void main() {
+ gl_FragColor = color;
+ }
+ ]]))\append(am.bind({
+ MV: mat4(
+ 1, 0, 0, 0
+ 0, 1, 0, 0
+ 0, 0, 1, 0
+ x, y, 0, 1
+ )
+ thickness: thickness
+ radius: radius
+ index: am.vec2_array(arr)
+ color: color
+ })\append(am.draw("triangles")))
+ circle_cache[key] = circle
+ circle
+
+class Joystick
+ --am.sprite() only works once the window is created
+ @initialized = false
+ @initialize: =>
+ @hollow_circle = am.group!
+ step = 0.5
+ thickness = 0.02
+ lastpoint = vec2(1,0)
+ print("color.am_color.background is:", color.am_color.background)
+ for k,v in pairs(color.am_color)
+ print(k,":",v)
+ highlight_start = (5/8) * math.pi
+ highlight_end = (7/8) * math.pi
+ shadow_start = (3/2) * math.pi
+ shadow_end = 2 * math.pi
+ for i = 0,2*math.pi,step
+ nextpoint = vec2(math.cos(i), math.sin(i))
+ @hollow_circle\append(am.line(lastpoint + vec2(1,1), nextpoint + vec2(1,1), thickness, color.am_color.outline))
+ --@hollow_circle\append(am.line(lastpoint, nextpoint, thickness, color.am_color.background))
+ lastpoint = nextpoint
+ @hollow_circle\append(am.line(lastpoint, vec2(1,0), thickness * 2, color.am_color.outline))
+ @hollow_circle\append(am.line(lastpoint,vec2(1,0), thickness, color.am_color.background))
+ @initialized = true
+ new: (x,y,r)=>
+ if not @@initialized
+ @@initialize!
+ @node = am.group!
+ position = am.translate(x,y)\tag("position")
+ @node\append(position)
+ --position\append(am.circle(vec2(x,y), r, color.am_color.background))
+ @stick_pos = am.translate(0,0)\tag("stick")
+ position\append(
+ @stick_pos\append(
+ am.circle(vec2(0,0), r/9, color.am_color.outline)\append(
+ am.circle(vec2(0,0), r/10, color.am_color.background)\append(
+ am.circle(vec2(-r/60,r/60),r/15, color.am_color.foreground)\append(
+ am.circle(vec2(5,-5),r/13, color.am_color.background)
+ )))))
+ --position\append(am.scale(r,r)\append(@@hollow_circle))
+ --position\append(am.circle(vec2(x,y),r)\append(am.blend("subtract")\append(am.circle(vec2(x,y),r-10))))
+ position\append(hollow_circle(x,y,r,8,color.am_color.outline))
+ position\append(hollow_circle(x,y,r-math.sqrt(2),5,color.am_color.background))
+ fire: (tbl) =>
+ print("Fired",tbl)
+
+Joystick
diff --git a/src/ui/textbox.moon b/src/ui/textbox.moon
new file mode 100644
index 0000000..38c6d84
--- /dev/null
+++ b/src/ui/textbox.moon
@@ -0,0 +1,78 @@
+
+color = require("color")
+Button = require("ui.button")
+am.eval_js(require("textbox_bridge"))
+
+valid_chars = "abcdefghijklmnopqrstuvwxyz"
+shifted_nums = "!@#$%^&*()"
+i = 0
+class Textbox extends Button
+ new: (x,y,w,h,value,placeholder) =>
+ super(x,y,w,h,value)
+ @id = i
+ i = i + 1
+ args = am.to_json({
+ name: value or ""
+ placeholder: placeholder or ""
+ })
+ --am.eval_js("window.amulet.window_has_focus = 0;")
+ am.eval_js("window.TEXTBOX.create_textbox(" .. args .. ");")
+ if value == ""
+ @text.text = placeholder
+ --@text.color = color.am_color.shadow
+ @cursor = am.group(
+ am.translate(@em,0)\append(
+ am.rect(0,0,@em/4,-@em,color.am_color.foreground)
+ ))
+ @cursor\action(() =>
+ if not @should_hide
+ @hidden = math.floor(am.current_time! * 2) % 2 == 0
+ else
+ @hidden = true
+ )
+ @cursor.should_hide = true
+ @text\append(@cursor)
+ @cursor_pos = #@text.text
+ @update_cursor_pos!
+ @max_chars = math.huge
+ @cursor
+ down: () =>
+ super!
+ --@cursor.should_hide = false
+ --@text.color = color.am_color.foreground
+ print("textbox down")
+ am.eval_js("window.TEXTBOX.focus(" .. am.to_json({id: @id}) .. ");")
+ up: () =>
+ super!
+ print("Textbox up")
+ am.eval_js("window.TEXTBOX.blur(" .. am.to_json({id: @id}) .. ");")
+ val = am.eval_js("window.TEXTBOX.get_text(" .. am.to_json({id:@id}) .. ");")
+ print("Up, got val:", val)
+ --@cursor.should_hide = true
+ --@text.color = color.am_color.shadow
+ update_cursor_pos: () =>
+ @.cursor("translate").x = @cursor_pos * 9
+ fire: (e) =>
+ if e.event == "mouse_down"
+ @down!
+ if @on
+ @on(e)
+ add_key = e.event == "keys_pressed" and @depressed
+ if add_key
+ for key in *e.data
+ if key == "kp_enter" or key == "enter"
+ if @on
+ @on(e)
+ elseif key == "escape"
+ @up!
+
+ @update_cursor_pos!
+ text = @text.text
+ newtext = am.eval_js("window.TEXTBOX.get_text(" .. am.to_json({id:@id}) .. ");")
+ if newtext != text
+ @text.text = newtext
+ if @onchange
+ @onchange!
+ false
+
+Textbox
diff --git a/src/util.lua b/src/util.lua
new file mode 100644
index 0000000..f74953a
--- /dev/null
+++ b/src/util.lua
@@ -0,0 +1,142 @@
+--[[
+Various helpful functions
+]]
+local util = {}
+function util.cartesian(...)
+ -- cartesian(tbl1, tbl2, tbl3, ...)
+ -- for each table, returns a permutation of key, value in tbl1, tbl2, ect.
+ local args = {...}
+ return coroutine.wrap(function()
+ local cursors = {} -- cursors[1], cursors[3], ect. are the keys
+ for k,v in ipairs(args) do
+ local a,b = next(v,nil)
+ cursors[(k*2) - 1] = a
+ cursors[(k*2)] = b
+ end
+ coroutine.yield(unpack(cursors))
+ local any_left = true
+ while any_left do
+ while next(args[#args],cursors[#cursors - 1]) do
+ local a,b = next(args[#args],cursors[#cursors - 1])
+ cursors[#cursors - 1] = a
+ cursors[#cursors] = b
+ coroutine.yield(unpack(cursors))
+ end
+ any_left = false
+ for i = #args, 1, -1 do
+ if next(args[i],cursors[(i*2)-1]) then
+ cursors[(i*2)-1], cursors[i*2] = next(args[i],cursors[(i*2)-1])
+ for j = i+1, #args do
+ cursors[(j*2)-1], cursors[j*2] = next(args[j],nil)
+ end
+ coroutine.yield(unpack(cursors))
+ any_left = true
+ break
+ end
+ end
+ end
+ end)
+end
+
+-- Override tostring to display more info about the table
+local old_tostring = tostring
+local numtabs = 0
+local printed_tables = {}
+local function tostring_helper(el)
+ assert(type(el) == "table", "Tried to call helper with something that was not a table, it was a " .. type(el))
+ local mt = getmetatable(el)
+ if mt and mt.__tostring then
+ return mt.__tostring(el)
+ elseif printed_tables[el] == true then
+ return old_tostring(el)
+ else
+ printed_tables[el] = true
+ numtabs = numtabs + 1
+ local strbuilder = {"{"}
+ for k,v in pairs(el) do
+ local key,value
+ if type(k) == "table" then
+ key = tostring_helper(k)
+ else
+ key = old_tostring(k)
+ end
+ if type(v) == "table" then
+ value = tostring_helper(v)
+ else
+ value = old_tostring(v)
+ end
+ strbuilder[#strbuilder + 1] = string.format("%s%s : %s", string.rep("\t",numtabs), key, value)
+ end
+ strbuilder[#strbuilder + 1] = string.rep("\t",numtabs - 1) .. "}"
+ numtabs = numtabs - 1
+ return table.concat(strbuilder,"\n")
+ end
+
+end
+function tostring(el)
+ printed_tables = {}
+ if type(el) == "table" then
+ return tostring_helper(el)
+ end
+ return old_tostring(el)
+end
+
+function util.reverse(tbl, val)
+ val = val or true
+ local ret = {}
+ for _,v in ipairs(tbl) do
+ ret[v] = val
+ end
+ return ret
+end
+
+function util.typecheck(tbl, ...)
+ local args = {...}
+ assert(#args % 2 == 0,"Typecheck should have an odd number of arguments, found " .. tostring(#args + 1) .. ".")
+ for i = 1, #args, 2 do
+ assert(args[i] and type(args[i]) == "string", "Cannot check a field of type " .. type(args[i]) .. " at position " .. tostring(i + 1) .. ".")
+ assert(tbl[args[i]], "Failed to find a field: " .. args[i])
+ assert(args[i+1] and type(args[i + 1]) == "string", "Cannot check for a type " .. type(args[i + 1]) .. " at position " .. tostring(i + 2) .. ".")
+ assert(type(tbl[args[i]]) == args[i+1], "Expected a " .. args[i+1] .. " at position " .. tostring(i+2) .. " but found a " .. type(tbl[args[i]]))
+ end
+ return true
+end
+
+function util.peer_to_code(str)
+ -- Turn peerjs peer ids into shorter strings
+ -- Example: 0dbbe67a-5358-4ea2-910a-195754b556a7
+ -- = 16 bytes
+ local buffer = am.buffer(16)
+ local view = buffer:view("ubyte")
+ local i = 0
+ for byte in str:gmatch("%x%x") do
+ local n = tonumber(byte,16)
+ view[(i % 16) + 1] = n
+ i = i + 1
+ end
+ local encoded = am.base64_encode(buffer)
+ return encoded:gsub("[/+]",{["/"] = "-",["+"] = "_"}):sub(1,22) -- chop the last 2 glyphs, assume 16 bytes
+end
+
+function util.code_to_peer(str)
+ local padded = str:gsub("[_-]",{["-"] = "/", ["_"] = "+"}) .. "=="
+ local buffer = am.base64_decode(padded)
+ local view = buffer:view("ubyte")
+ local dashes = {4,2,2,2,6}
+ local builder = {}
+ local i = 1
+ while #dashes > 0 do
+ local nbytes = table.remove(dashes,1)
+ for _ = 1, nbytes do
+ table.insert(builder, string.format("%02x",view[i]))
+ i = i + 1
+ end
+ if #dashes > 0 then
+ table.insert(builder,"-")
+ end
+ end
+ ret = table.concat(builder)
+ return ret
+end
+
+return util
diff --git a/src/window.moon b/src/window.moon
new file mode 100644
index 0000000..7fc7953
--- /dev/null
+++ b/src/window.moon
@@ -0,0 +1,11 @@
+color = require("color")
+-- Special file to hold the window, no dependencies!
+win = am.window{
+ title: "GGJ 2026"
+ width: 360
+ height: 800
+ clear_color: color.am_color.background
+ depth_buffer: true
+}
+win.scene = am.group!
+win
diff --git a/src/world.moon b/src/world.moon
new file mode 100644
index 0000000..d3a2ca7
--- /dev/null
+++ b/src/world.moon
@@ -0,0 +1,52 @@
+-- Global state
+--win = require("window")
+hc = require("party.hc.init")
+--ecs = require("ecs")
+--settings = require("settings")
+color = require("color")
+log = require("log")
+
+MAX_LAMPS = 8
+
+--Use a collider to decide what to render
+x = {
+ -- client-side viewport offsets from the world
+ world_x: 0
+ world_y: 0
+ -- Have we selected an input type yet?
+ controller: { -- logical input abstraction, not a physical input device controller
+ text_size: 1
+ }
+ domain: "client" -- "client" or "server"
+ -- (Client) Level information
+ level: {
+ graphics:{} -- Client side graphics
+ entities:{} -- Client side entities
+ graphic_world: hc.new(5) -- Client side graphics world
+ physics_world: hc.new(1)
+ collider: nil -- Collider in the physics world to figure out what we can see
+ }
+ -- (Server Networked) level information
+ level_sync: {
+ name: "" -- The name of the level
+ data: {} -- sequence, holds arguments for level initalization
+ ents: {} -- holds all the entities in the level
+ entid: 0 -- a nonce entid, just keep increasing and allows for holes in "ents"
+ peers: {} -- holds peers in [peerid: string] = Entity
+ phys: hc.new(5) -- The physics world we need to collide with
+ ref: nil -- Holds a ref to the level, has things like name, get_spawn_location(), ect.
+ }
+ sync_time: () ->
+ am.current_time!
+ hub: nil -- server, filled out later
+ network: nil -- client, filled out later
+ -- Stuff that goes to the shader
+ geom_view: nil -- am.vec3_array
+ uv_view: nil -- am.vec4_array
+ check: () =>
+ assert(@level_sync.ents, "Ents is nil")
+ assert(type(@level_sync.ents), "ents is not a table")
+}
+
+log.info("At the end of src/world.moon, world_x is" .. tostring(x.world_x))
+x