diff options
| author | Alex Pickering <alex@cogarr.net> | 2026-02-01 13:14:32 -0600 |
|---|---|---|
| committer | Alexander M Pickering <alex@cogarr.net> | 2026-02-01 13:14:32 -0600 |
| commit | 3a975db66a3711f34e8b64bb27a8eaca79fdeca9 (patch) | |
| tree | fcc12f8f9d638ff575c1963796de76b7628854b4 /src/menu | |
| download | ggj26-3a975db66a3711f34e8b64bb27a8eaca79fdeca9.tar.gz ggj26-3a975db66a3711f34e8b64bb27a8eaca79fdeca9.tar.bz2 ggj26-3a975db66a3711f34e8b64bb27a8eaca79fdeca9.zip | |
Diffstat (limited to 'src/menu')
| -rw-r--r-- | src/menu/game.moon | 208 | ||||
| -rw-r--r-- | src/menu/input.moon | 30 | ||||
| -rw-r--r-- | src/menu/lobby.moon | 103 | ||||
| -rw-r--r-- | src/menu/main.moon | 137 | ||||
| -rw-r--r-- | src/menu/playername.moon | 23 | ||||
| -rw-r--r-- | src/menu/settings.moon | 58 | ||||
| -rw-r--r-- | src/menu/tutorial.moon | 106 |
7 files changed, 665 insertions, 0 deletions
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 |
