▄▄▄▄   ▓█████  ███▄    █   ██████  ▄▄▄       ██▓    ▓█████  ███▄ ▄███▓
▓█████▄ ▓█    █ ▀█   █ ▒██   ▒████▄    ▓██▒    ▓█   ▀ ▓██▒▀█▀ ██▒
▒██▒ ▄██▒███   ▓██  ▀█ ██▒░ ▓██▄   ▒██  ▀█▄  ▒██░    ▒███   ▓██    ▓██░
▒██░█▀  ▒▓█  ▄ ▓██▒  ▐▌██▒  ▒   ██▒░██▄▄▄▄██ ▒██░    ▒▓█  ▄ ▒██    ▒██ 
░▓█  ▀█▓░▒████▒▒██░   ▓██░▒██████▒▒ ▓█   ▓██▒░██████▒░▒████▒▒██▒   ░██▒
░▒▓███▀▒░░ ▒░ ░░ ▒░   ▒ ▒ ▒ ▒▓▒ ▒ ░ ▒▒   ▓▒█░░ ▒░▓  ░░░ ▒░ ░░ ▒░   ░  ░
▒░▒   ░  ░ ░  ░░ ░░   ░ ▒░░ ░▒  ░ ░  ▒   ▒▒ ░░ ░ ▒  ░ ░ ░  ░░  ░      ░
 ░    ░    ░      ░   ░ ░ ░  ░  ░    ░   ▒     ░ ░      ░   ░      ░   
 ░         ░  ░         ░       ░        ░  ░    ░  ░   ░  ░       ░   
      ░

0 player in this world


ROCK ASCII castle


ROCK is a runtime for worlds. This interactive ASCII castle room is a small intro.

Try opening this world in a second browser tab to see another player enter the castle.

If you don't see a little ASCII room -> try disabling/enabling a VPN. It could be a server connection issue.


github docs

WASD to move

@ = you, P = another player, + = door, T = lectern

docs.lua (210 LoC)

-- ============================================================================ -- ROCK ASCII castle gamemode -- ============================================================================ -- Not all features are presented in the source of this gamemode. I'm in the process of writing a comprehensive documentation. local keyboard = input.bindings.keyboard local controller = input.bindings.controller local stick = input.bindings.stick input.new():vector() :defaults({ keyboard = { up = { keyboard.KeyW, keyboard.ArrowUp }, down = { keyboard.KeyS, keyboard.ArrowDown }, left = { keyboard.KeyA, keyboard.ArrowLeft }, right = { keyboard.KeyD, keyboard.ArrowRight }, }, controller = { up = { controller.DPadUp }, down = { controller.DPadDown }, left = { controller.DPadLeft }, right = { controller.DPadRight }, }, stick = stick.LeftStick, }) :register("Move") input.new():button() :defaults({ keyboard = { keyboard.KeyE }, controller = { controller.ButtonA }, }) :register("Use") local SYMBOL = { Floor = 0, Wall = 1, Door = 2, Lectern = 3, } local MAP_WIDTH = 30 local MAP_HEIGHT = 14 local MAX_INTERACT_DISTANCE = 1 local OBJECTS = { { id = "farcaster_1978_door", symbol_id = SYMBOL.Door, type = "door", hint = "<span class="text-purple-400">Farcaster 1978</span> is under construction", position = { x = 12, y = 13 }, }, { id = "philosophy_lectern", symbol_id = SYMBOL.Lectern, type = "lectern", hint = "<span class="text-purple-400">Philosophy lectern</span>", position = { x = 16, y = 4 }, }, { id = "getting_started_lectern", symbol_id = SYMBOL.Lectern, type = "lectern", hint = "<span class="text-purple-400">Getting Started lectern</span>", position = { x = 16, y = 9 }, }, } local OBJECT_COLLIDERS = {} local objects_near_player = {} local function to_index(x, y) return y * MAP_WIDTH + x + 1 end local function is_border(x, y) return x == 0 or x == MAP_WIDTH - 1 or y == 0 or y == MAP_HEIGHT - 1 end local function ensure_player_near_objects(pid) local near_objects = objects_near_player[pid] if not near_objects then near_objects = {} objects_near_player[pid] = near_objects end return near_objects end local function can_interact_with(object, pos) return math.abs(object.position.x - pos.x) <= MAX_INTERACT_DISTANCE and math.abs(object.position.y - pos.y) <= MAX_INTERACT_DISTANCE end local function build_map() local map = {} for y = 0, MAP_HEIGHT - 1 do for x = 0, MAP_WIDTH - 1 do local symbol = is_border(x, y) and SYMBOL.Wall or SYMBOL.Floor map[#map + 1] = symbol end end for _, object in ipairs(OBJECTS) do local index = to_index(object.position.x, object.position.y) map[index] = object.symbol_id OBJECT_COLLIDERS[index] = true end return map end local map = build_map() local player_bp = entity.blueprint() :position({ x = 2, y = 2 }) :custom({ kind = "player" }) player_bp:sync():commit() on.world.awake() :take(1) :each(function() print("Runtime started") end) on.player.online() :each(function(p) local pid = p:id() p:signal("Map"):data({ width = MAP_WIDTH, height = MAP_HEIGHT, tiles = map, }):send() p:signal("Identity"):data({ id = pid }):send() local player_char = player_bp:spawn() :owned_by(pid) :room("lobby") p:vision():attach(player_char) end) on.player.input() :bind_action("Move") :each(function(p, data) local pid = p:id() local dx = data.x or 0 local dy = data.y or 0 if dx == 0 and dy == 0 then return end entity.query() :owned_by(pid) :blueprint(player_bp) :each(function(ent) local pos = ent:position() local new_pos = { x = pos.x + dx, y = pos.y + dy, } local index = to_index(new_pos.x, new_pos.y) if is_border(new_pos.x, new_pos.y) or OBJECT_COLLIDERS[index] then return end ent:position(new_pos) if ent:custom().kind ~= "player" then return end local near_objects = ensure_player_near_objects(pid) for _, object in ipairs(OBJECTS) do local is_near = can_interact_with(object, new_pos) local was_near = near_objects[object.id] if is_near and not was_near then near_objects[object.id] = true p:signal("ObjectApproach"):data(object):send() elseif not is_near and was_near then near_objects[object.id] = nil p:signal("ObjectDepart"):data({ id = object.id }):send() end end end) end) on.player.input() :bind_action("Use") :each(function(p, _) local near_objects = objects_near_player[p:id()] if not near_objects then return end for id in pairs(near_objects) do p:signal("ObjectUse"):data({ id = id }):send() end end) on.player.offline() :each(function(p) objects_near_player[p:id()] = nil entity.query():owned_by(p:id()):each(function(e) e:despawn() end) end)