diff options
Diffstat (limited to 'src')
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 |
