the code guh
38
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
dragonruby
|
||||
dragonruby-httpd
|
||||
dragonruby-publish
|
||||
console-logo.png
|
||||
dragonruby.png
|
||||
dragonruby-controller.png
|
||||
font.ttf
|
||||
ctags-*
|
||||
eula.txt
|
||||
open-source-licenses.txt
|
||||
README.txt
|
||||
VERSION.txt
|
||||
docs/
|
||||
samples/
|
||||
.dragonruby/
|
||||
|
||||
# Same files inside mygame/
|
||||
mygame/dragonruby
|
||||
mygame/dragonruby-httpd
|
||||
mygame/dragonruby-publish
|
||||
mygame/console-logo.png
|
||||
mygame/dragonruby.png
|
||||
mygame/dragonruby-controller.png
|
||||
mygame/font.ttf
|
||||
mygame/eula.txt
|
||||
mygame/open-source-licenses.txt
|
||||
mygame/README.txt
|
||||
mygame/VERSION.txt
|
||||
mygame/.dragonruby/
|
||||
|
||||
# Runtime-generated files
|
||||
logs/
|
||||
tmp/
|
||||
builds/
|
||||
mygame/logs/
|
||||
mygame/errors/
|
||||
mygame/tmp/
|
||||
mygame/builds/
|
||||
21
LICENSE
|
|
@ -1,21 +0,0 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2026 End
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
33
README.md
|
|
@ -1 +1,32 @@
|
|||
# Rooms-We-Share
|
||||
# Rooms We Share
|
||||
|
||||
A top-down exploration game about plurality, made for the Hack Club Ember Game Jam 2026 (theme: "Beneath the Surface!").
|
||||
|
||||
## About
|
||||
|
||||
Explore a house and discover different perspectives. Start as the Host and unlock other system members (Guardian, Little One, Analyst) who each see the world differently through unique color palettes and commentary on objects.
|
||||
|
||||
## Controls
|
||||
|
||||
- **Arrow Keys / WASD** - Move
|
||||
- **E** - Examine nearby objects
|
||||
- **1/2/3/4** - Switch between unlocked members
|
||||
- **R** - Reset game
|
||||
|
||||
## Running the Game
|
||||
|
||||
1. Download [DragonRuby GTK](https://dragonruby.org/)
|
||||
2. Clone this repository into the DragonRuby folder or copy `mygame/` into your DragonRuby installation
|
||||
3. Run `./dragonruby` (or double-click the DragonRuby executable)
|
||||
|
||||
## Play Online
|
||||
|
||||
[Play in your browser](https://endoftimee.itch.io/rooms-we-share)
|
||||
|
||||
## Development
|
||||
|
||||
Built with DragonRuby GTK using a geometric/minimalist art style with color-based mood system.
|
||||
|
||||
## License
|
||||
|
||||
See LICENSE file for details.
|
||||
|
|
|
|||
85
app/commentary.rb
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
def init_commentary(args)
|
||||
args.state.commentary_active ||= false
|
||||
args.state.commentary_text ||= ''
|
||||
args.state.commentary_tick ||= 0
|
||||
args.state.commentary_last_index ||= -1
|
||||
args.state.commentary_cooldown ||= 0
|
||||
end
|
||||
|
||||
def tick_commentary(args)
|
||||
return if args.state.transitioning
|
||||
return if args.state.examining
|
||||
|
||||
# Cooldown between commentary
|
||||
if args.state.commentary_cooldown > 0
|
||||
args.state.commentary_cooldown -= 1
|
||||
return
|
||||
end
|
||||
|
||||
# If commentary is active, check if it should end
|
||||
if args.state.commentary_active
|
||||
elapsed = Kernel.tick_count - args.state.commentary_tick
|
||||
total = COMMENTARY_FADE_IN + COMMENTARY_HOLD + COMMENTARY_FADE_OUT
|
||||
if elapsed >= total
|
||||
args.state.commentary_active = false
|
||||
args.state.commentary_cooldown = 120 # 2s cooldown
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
# Random chance to trigger new commentary (~1 in 300 frames)
|
||||
return unless rand(300) == 0
|
||||
|
||||
room = ROOMS[args.state.current_room]
|
||||
return unless room
|
||||
|
||||
member = args.state.current_member || MEMBER_HOST
|
||||
lines = room[:commentary][member]
|
||||
return unless lines && !lines.empty?
|
||||
|
||||
# Pick a line, avoid repeating the last one
|
||||
index = rand(lines.length)
|
||||
index = (index + 1) % lines.length if index == args.state.commentary_last_index && lines.length > 1
|
||||
|
||||
args.state.commentary_active = true
|
||||
args.state.commentary_text = lines[index]
|
||||
args.state.commentary_tick = Kernel.tick_count
|
||||
args.state.commentary_last_index = index
|
||||
end
|
||||
|
||||
def render_commentary(args)
|
||||
return unless args.state.commentary_active
|
||||
|
||||
elapsed = Kernel.tick_count - args.state.commentary_tick
|
||||
|
||||
# Calculate alpha based on fade phase
|
||||
if elapsed < COMMENTARY_FADE_IN
|
||||
alpha = (elapsed.to_f / COMMENTARY_FADE_IN * 255).to_i
|
||||
elsif elapsed < COMMENTARY_FADE_IN + COMMENTARY_HOLD
|
||||
alpha = 255
|
||||
else
|
||||
fade_elapsed = elapsed - COMMENTARY_FADE_IN - COMMENTARY_HOLD
|
||||
alpha = ((1 - fade_elapsed.to_f / COMMENTARY_FADE_OUT) * 255).to_i
|
||||
end
|
||||
|
||||
alpha = alpha.clamp(0, 255)
|
||||
return if alpha <= 0
|
||||
|
||||
palette = current_palette(args)
|
||||
|
||||
# Float text above the player
|
||||
text_x = args.state.player.x + args.state.player.w / 2
|
||||
text_y = args.state.player.y + args.state.player.h + 30
|
||||
|
||||
# Keep on screen
|
||||
text_x = text_x.clamp(100, SCREEN_W - 100)
|
||||
text_y = text_y.clamp(100, SCREEN_H - 20)
|
||||
|
||||
args.outputs.labels << {
|
||||
x: text_x, y: text_y,
|
||||
text: args.state.commentary_text,
|
||||
size_enum: -1,
|
||||
alignment_enum: 1,
|
||||
r: palette[:text][:r], g: palette[:text][:g], b: palette[:text][:b], a: alpha
|
||||
}
|
||||
end
|
||||
83
app/constants.rb
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
# Game constants
|
||||
|
||||
SCREEN_W = 1280
|
||||
SCREEN_H = 720
|
||||
|
||||
TILE_SIZE = 40
|
||||
GRID_W = SCREEN_W / TILE_SIZE # 32
|
||||
GRID_H = SCREEN_H / TILE_SIZE # 18
|
||||
|
||||
PLAYER_SIZE = 30
|
||||
PLAYER_SPEED = 3
|
||||
|
||||
# Member identifiers
|
||||
MEMBER_HOST = :host
|
||||
MEMBER_GUARDIAN = :guardian
|
||||
MEMBER_LITTLE = :little
|
||||
MEMBER_ANALYST = :analyst
|
||||
|
||||
MEMBER_ORDER = [MEMBER_HOST, MEMBER_GUARDIAN, MEMBER_LITTLE, MEMBER_ANALYST].freeze
|
||||
|
||||
# Color palettes per member
|
||||
PALETTES = {
|
||||
host: {
|
||||
bg: { r: 180, g: 175, b: 170 },
|
||||
wall: { r: 100, g: 95, b: 90 },
|
||||
object: { r: 160, g: 155, b: 150 },
|
||||
text: { r: 220, g: 155, b: 150 },
|
||||
accent: { r: 200, g: 180, b: 140 },
|
||||
player: { r: 150, g: 145, b: 140 },
|
||||
overlay: { r: 0, g: 0, b: 0, a: 0 }
|
||||
},
|
||||
guardian: {
|
||||
bg: { r: 40, g: 45, b: 70 },
|
||||
wall: { r: 25, g: 25, b: 45 },
|
||||
object: { r: 80, g: 60, b: 60 },
|
||||
text: { r: 200, g: 150, b: 150 },
|
||||
accent: { r: 220, g: 60, b: 60 },
|
||||
player: { r: 80, g: 80, b: 160 },
|
||||
overlay: { r: 10, g: 10, b: 40, a: 60 }
|
||||
},
|
||||
little: {
|
||||
bg: { r: 255, g: 230, b: 200 },
|
||||
wall: { r: 200, g: 170, b: 180 },
|
||||
object: { r: 255, g: 200, b: 160 },
|
||||
text: { r: 255, g: 220, b: 200 },
|
||||
accent: { r: 255, g: 200, b: 100 },
|
||||
player: { r: 255, g: 200, b: 180 },
|
||||
overlay: { r: 255, g: 230, b: 180, a: 30 }
|
||||
},
|
||||
analyst: {
|
||||
bg: { r: 210, g: 210, b: 210 },
|
||||
wall: { r: 50, g: 50, b: 50 },
|
||||
object: { r: 180, g: 180, b: 180 },
|
||||
text: { r: 100, g: 220, b: 100 },
|
||||
accent: { r: 100, g: 255, b: 100 },
|
||||
player: { r: 170, g: 170, b: 170 },
|
||||
overlay: { r: 0, g: 0, b: 0, a: 20 }
|
||||
}
|
||||
}
|
||||
|
||||
# Commentary timing (frames, 60fps)
|
||||
COMMENTARY_FADE_IN = 30 # 0.5s appear
|
||||
COMMENTARY_HOLD = 180 # 3s visible
|
||||
COMMENTARY_FADE_OUT = 60 # 1s disappear
|
||||
|
||||
# Dialogue
|
||||
DIALOGUE_TEXT_SPEED = 0.5
|
||||
DIALOGUE_PADDING = 20
|
||||
|
||||
# Room transitions
|
||||
ROOM_TRANSITION_FRAMES = 30 # 0.5s fade to black
|
||||
|
||||
# Memory
|
||||
MEMORY_PULSE_SPEED = 0.05
|
||||
|
||||
# Tile types
|
||||
TILE_EMPTY = 0 # floor
|
||||
TILE_WALL = 1
|
||||
TILE_DOOR = 2 # triggers room transition
|
||||
TILE_OBJECT = 3
|
||||
TILE_MEMORY = 4 # collectible memory fragment
|
||||
TILE_NPC = 5
|
||||
TILE_HIDDEN = 6 # only visible to certain members
|
||||
41
app/main.rb
Executable file
|
|
@ -0,0 +1,41 @@
|
|||
require 'app/constants'
|
||||
require 'app/rooms'
|
||||
require 'app/members'
|
||||
require 'app/player'
|
||||
require 'app/objects'
|
||||
require 'app/commentary'
|
||||
require 'app/memory'
|
||||
require 'app/render'
|
||||
require 'app/render_ui'
|
||||
|
||||
def tick(args)
|
||||
unless args.state.initialized
|
||||
init(args)
|
||||
args.state.initialized = true
|
||||
end
|
||||
|
||||
tick_members(args)
|
||||
tick_player(args)
|
||||
tick_objects(args)
|
||||
tick_commentary(args)
|
||||
tick_memory(args)
|
||||
tick_room_transition(args)
|
||||
|
||||
render_room(args)
|
||||
render_objects(args)
|
||||
render_player(args)
|
||||
render_commentary(args)
|
||||
render_memory(args)
|
||||
render_ui(args)
|
||||
render_overlay(args)
|
||||
render_transition(args)
|
||||
end
|
||||
|
||||
def init(args)
|
||||
init_rooms(args)
|
||||
init_members(args)
|
||||
init_player(args)
|
||||
init_objects(args)
|
||||
init_commentary(args)
|
||||
init_memory(args)
|
||||
end
|
||||
64
app/members.rb
Executable file
|
|
@ -0,0 +1,64 @@
|
|||
def init_members(args)
|
||||
args.state.current_member ||= MEMBER_HOST
|
||||
|
||||
args.state.unlocked_members ||= {
|
||||
host: true,
|
||||
guardian: false,
|
||||
little: false,
|
||||
analyst: false
|
||||
}
|
||||
|
||||
args.state.member_data ||= {
|
||||
host: {
|
||||
name: 'You',
|
||||
description: 'Everything seems... fine.',
|
||||
palette_key: :host
|
||||
},
|
||||
guardian: {
|
||||
name: 'The Guardian',
|
||||
description: 'Watch the exits. Check the locks. Stay alert.',
|
||||
palette_key: :guardian
|
||||
},
|
||||
little: {
|
||||
name: 'Bailey',
|
||||
description: "Ooh! What's that? Can we explore pleaseeee? BOBAAAAA!!",
|
||||
palette_key: :little
|
||||
},
|
||||
analyst: {
|
||||
name: 'The Analyst',
|
||||
description: 'Cataloguing. Observing. Processing.',
|
||||
palette_key: :analyst
|
||||
}
|
||||
}
|
||||
|
||||
args.state.member_just_switched ||= false
|
||||
args.state.member_switch_tick ||= 0
|
||||
end
|
||||
|
||||
def tick_members(args)
|
||||
# Reset switch flag each frame
|
||||
args.state.member_just_switched = false
|
||||
|
||||
if args.inputs.keyboard.key_down.one
|
||||
try_switch_member(args, MEMBER_HOST)
|
||||
elsif args.inputs.keyboard.key_down.two
|
||||
try_switch_member(args, MEMBER_GUARDIAN)
|
||||
elsif args.inputs.keyboard.key_down.three
|
||||
try_switch_member(args, MEMBER_LITTLE)
|
||||
elsif args.inputs.keyboard.key_down.four
|
||||
try_switch_member(args, MEMBER_ANALYST)
|
||||
end
|
||||
end
|
||||
|
||||
def try_switch_member(args, member_id)
|
||||
# do nothing if not unlocked
|
||||
return unless args.state.unlocked_members[member_id]
|
||||
# don't switch if already active
|
||||
return if args.state.current_member == member_id
|
||||
|
||||
args.state.current_member = member_id
|
||||
|
||||
# set flag so commentary and render know a switch happened
|
||||
args.state.member_just_switched = true
|
||||
args.state.member_switch_tick = Kernel.tick_count
|
||||
end
|
||||
35
app/memory.rb
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
def init_memory(args)
|
||||
args.state.memories_collected ||= []
|
||||
end
|
||||
|
||||
def tick_memory(args)
|
||||
return if args.state.transitioning
|
||||
|
||||
room = args.state.current_room_data
|
||||
return unless room
|
||||
|
||||
tiles = room[:parsed_tiles]
|
||||
return unless tiles
|
||||
|
||||
px = args.state.player.x + args.state.player.w / 2
|
||||
py = args.state.player.y + args.state.player.h / 2
|
||||
player_col = (px / TILE_SIZE).floor
|
||||
player_row = (py / TILE_SIZE).floor
|
||||
|
||||
return if player_row < 0 || player_row >= GRID_H
|
||||
return if player_col < 0 || player_col >= GRID_W
|
||||
|
||||
return unless tiles[player_row][player_col] == TILE_MEMORY
|
||||
|
||||
memory_key = "#{args.state.current_room}_#{player_col}_#{player_row}"
|
||||
return if args.state.memories_collected.include?(memory_key)
|
||||
|
||||
args.state.memories_collected << memory_key
|
||||
# Replace tile with empty floor
|
||||
tiles[player_row][player_col] = TILE_EMPTY
|
||||
end
|
||||
|
||||
def render_memory(args)
|
||||
# Memory pulsing effect is rendered as part of render_room.
|
||||
# This is a placeholder for future memory collection UI (flash, text, etc.)
|
||||
end
|
||||
91
app/objects.rb
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
def init_objects(args)
|
||||
args.state.examining ||= false
|
||||
args.state.examine_text ||= ''
|
||||
args.state.examine_object_id ||= nil
|
||||
args.state.nearby_object ||= nil
|
||||
end
|
||||
|
||||
def tick_objects(args)
|
||||
return if args.state.transitioning
|
||||
|
||||
# Find nearby interactable object
|
||||
args.state.nearby_object = find_nearby_object(args)
|
||||
|
||||
return unless args.inputs.keyboard.key_down.e
|
||||
|
||||
if args.state.examining
|
||||
# Close examination
|
||||
args.state.examining = false
|
||||
args.state.examine_text = ''
|
||||
args.state.examine_object_id = nil
|
||||
elsif args.state.nearby_object
|
||||
# Open examination
|
||||
obj = args.state.nearby_object
|
||||
member = args.state.current_member || MEMBER_HOST
|
||||
text = obj[:examine][member] || obj[:examine][:host] || ''
|
||||
args.state.examining = true
|
||||
args.state.examine_text = text
|
||||
args.state.examine_object_id = obj[:id]
|
||||
end
|
||||
end
|
||||
|
||||
def find_nearby_object(args)
|
||||
room = ROOMS[args.state.current_room]
|
||||
return nil unless room
|
||||
|
||||
px = args.state.player.x + args.state.player.w / 2
|
||||
py = args.state.player.y + args.state.player.h / 2
|
||||
player_col = (px / TILE_SIZE).floor
|
||||
player_row = (py / TILE_SIZE).floor
|
||||
|
||||
room[:objects].each do |obj|
|
||||
obj[:cols].each do |col|
|
||||
obj[:rows].each do |row|
|
||||
tile_row = GRID_H - 1 - row
|
||||
dist_col = (player_col - col).abs
|
||||
dist_row = (player_row - tile_row).abs
|
||||
return obj if dist_col <= 1 && dist_row <= 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
nil
|
||||
end
|
||||
|
||||
def render_objects(args)
|
||||
return unless args.state.examining
|
||||
|
||||
palette = current_palette(args)
|
||||
|
||||
# Examination dialogue box
|
||||
box_w = 600
|
||||
box_h = 120
|
||||
box_x = (SCREEN_W - box_w) / 2
|
||||
box_y = 40
|
||||
|
||||
args.outputs.solids << {
|
||||
x: box_x, y: box_y, w: box_w, h: box_h,
|
||||
r: 20, g: 20, b: 20, a: 220
|
||||
}
|
||||
|
||||
args.outputs.borders << {
|
||||
x: box_x, y: box_y, w: box_w, h: box_h,
|
||||
r: palette[:accent][:r], g: palette[:accent][:g], b: palette[:accent][:b]
|
||||
}
|
||||
|
||||
args.outputs.labels << {
|
||||
x: box_x + DIALOGUE_PADDING,
|
||||
y: box_y + box_h - DIALOGUE_PADDING,
|
||||
text: args.state.examine_text,
|
||||
size_enum: 0,
|
||||
r: palette[:text][:r], g: palette[:text][:g], b: palette[:text][:b]
|
||||
}
|
||||
|
||||
# Close prompt
|
||||
args.outputs.labels << {
|
||||
x: box_x + box_w - 100, y: box_y + 25,
|
||||
text: '[E] Close',
|
||||
size_enum: -2,
|
||||
r: 180, g: 180, b: 180
|
||||
}
|
||||
end
|
||||
94
app/player.rb
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
def init_player(args)
|
||||
args.state.player.x ||= SCREEN_W / 2 - PLAYER_SIZE / 2
|
||||
args.state.player.y ||= SCREEN_H / 2 - PLAYER_SIZE / 2
|
||||
args.state.player.w ||= PLAYER_SIZE
|
||||
args.state.player.h ||= PLAYER_SIZE
|
||||
args.state.player.speed ||= PLAYER_SPEED
|
||||
end
|
||||
|
||||
def tick_player(args)
|
||||
# no movement during room transitions
|
||||
return if args.state.transitioning
|
||||
|
||||
dx = 0
|
||||
dy = 0
|
||||
|
||||
# WASD + arrow keys + HJKL (declan wanted so here it is)
|
||||
dx -= 1 if args.inputs.keyboard.left || args.inputs.keyboard.a || args.inputs.keyboard.h
|
||||
dx += 1 if args.inputs.keyboard.right || args.inputs.keyboard.d || args.inputs.keyboard.l
|
||||
dy += 1 if args.inputs.keyboard.up || args.inputs.keyboard.w || args.inputs.keyboard.k
|
||||
dy -= 1 if args.inputs.keyboard.down || args.inputs.keyboard.s || args.inputs.keyboard.j
|
||||
|
||||
return if dx == 0 && dy == 0
|
||||
|
||||
# normalize diagonal movement
|
||||
if dx != 0 && dy != 0
|
||||
dx *= 0.707
|
||||
dy *= 0.707
|
||||
end
|
||||
|
||||
speed = args.state.player.speed
|
||||
new_x = args.state.player.x + (dx * speed)
|
||||
new_y = args.state.player.y + (dy * speed)
|
||||
|
||||
# separate axis collision detection
|
||||
unless collides_with_wall?(args, new_x, args.state.player.y, args.state.player.w, args.state.player.h)
|
||||
args.state.player.x = new_x
|
||||
end
|
||||
|
||||
unless collides_with_wall?(args, args.state.player.x, new_y, args.state.player.w, args.state.player.h)
|
||||
args.state.player.y = new_y
|
||||
end
|
||||
|
||||
# check for door collision
|
||||
check_door_collision(args)
|
||||
end
|
||||
|
||||
def collides_with_wall?(args, x, y, w, h)
|
||||
room = args.state.current_room_data
|
||||
return false unless room
|
||||
|
||||
tiles = room[:parsed_tiles]
|
||||
return false unless tiles
|
||||
|
||||
# check all tiles the player bounding box overlaps
|
||||
left_col = (x / TILE_SIZE).floor
|
||||
right_col = ((x + w - 1) / TILE_SIZE).floor
|
||||
bottom_row = (y / TILE_SIZE).floor
|
||||
top_row = ((y + h - 1) / TILE_SIZE).floor
|
||||
|
||||
(bottom_row..top_row).each do |row|
|
||||
(left_col..right_col).each do |col|
|
||||
next if row < 0 || row >= GRID_H || col < 0 || col >= GRID_W
|
||||
return true if tiles[row][col] == TILE_WALL
|
||||
end
|
||||
end
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def check_door_collision(args)
|
||||
room = args.state.current_room_data
|
||||
return unless room
|
||||
|
||||
tiles = room[:parsed_tiles]
|
||||
return unless tiles
|
||||
|
||||
px = args.state.player.x
|
||||
py = args.state.player.y
|
||||
pw = args.state.player.w
|
||||
ph = args.state.player.h
|
||||
|
||||
center_col = ((px + pw / 2) / TILE_SIZE).floor
|
||||
center_row = ((py + ph / 2) / TILE_SIZE).floor
|
||||
|
||||
return if center_row < 0 || center_row >= GRID_H
|
||||
return if center_col < 0 || center_col >= GRID_W
|
||||
|
||||
return unless tiles[center_row][center_col] == TILE_DOOR
|
||||
|
||||
door = find_door_at(args, center_col, center_row)
|
||||
return unless door
|
||||
|
||||
start_room_transition(args, door[:target_room], door[:spawn_x], door[:spawn_y])
|
||||
end
|
||||
99
app/render.rb
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
def current_palette(args)
|
||||
member = args.state.current_member || MEMBER_HOST
|
||||
PALETTES[member] || PALETTES[MEMBER_HOST]
|
||||
end
|
||||
|
||||
def render_room(args)
|
||||
room = args.state.current_room_data
|
||||
return unless room
|
||||
|
||||
palette = current_palette(args)
|
||||
tiles = room[:parsed_tiles]
|
||||
return unless tiles
|
||||
|
||||
args.outputs.solids << {
|
||||
x: 0, y: 0, w: SCREEN_W, h: SCREEN_H,
|
||||
r: palette[:bg][:r], g: palette[:bg][:g], b: palette[:bg][:b]
|
||||
}
|
||||
|
||||
tiles.each_with_index do |row, row_idx|
|
||||
row.each_with_index do |tile, col_idx|
|
||||
x = col_idx * TILE_SIZE
|
||||
y = row_idx * TILE_SIZE
|
||||
|
||||
case tile
|
||||
when TILE_WALL
|
||||
args.outputs.solids << {
|
||||
x: x, y: y, w: TILE_SIZE, h: TILE_SIZE,
|
||||
r: palette[:wall][:r], g: palette[:wall][:g], b: palette[:wall][:b]
|
||||
}
|
||||
when TILE_OBJECT
|
||||
args.outputs.solids << {
|
||||
x: x + 4, y: y + 4, w: TILE_SIZE - 8, h: TILE_SIZE - 8,
|
||||
r: palette[:object][:r], g: palette[:object][:g], b: palette[:object][:b]
|
||||
}
|
||||
when TILE_MEMORY
|
||||
pulse = (Math.sin(Kernel.tick_count * MEMORY_PULSE_SPEED) + 1) / 2
|
||||
alpha = (100 + pulse * 155).to_i
|
||||
args.outputs.solids << {
|
||||
x: x + 8, y: y + 8, w: TILE_SIZE - 16, h: TILE_SIZE - 16,
|
||||
r: palette[:accent][:r], g: palette[:accent][:g], b: palette[:accent][:b], a: alpha
|
||||
}
|
||||
when TILE_DOOR
|
||||
args.outputs.solids << {
|
||||
x: x + 2, y: y + 2, w: TILE_SIZE - 4, h: TILE_SIZE - 4,
|
||||
r: palette[:accent][:r], g: palette[:accent][:g], b: palette[:accent][:b], a: 120
|
||||
}
|
||||
when TILE_HIDDEN
|
||||
# Only visible to certain members
|
||||
if [MEMBER_GUARDIAN, MEMBER_ANALYST].include?(args.state.current_member)
|
||||
args.outputs.solids << {
|
||||
x: x + 4, y: y + 4, w: TILE_SIZE - 8, h: TILE_SIZE - 8,
|
||||
r: palette[:accent][:r], g: palette[:accent][:g], b: palette[:accent][:b], a: 80
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def render_player(args)
|
||||
palette = current_palette(args)
|
||||
p = args.state.player
|
||||
|
||||
args.outputs.solids << {
|
||||
x: p.x, y: p.y, w: p.w, h: p.h,
|
||||
r: palette[:player][:r], g: palette[:player][:g], b: palette[:player][:b]
|
||||
}
|
||||
end
|
||||
|
||||
def render_overlay(args)
|
||||
palette = current_palette(args)
|
||||
ov = palette[:overlay]
|
||||
return if !ov[:a] || ov[:a] == 0
|
||||
|
||||
args.outputs.solids << {
|
||||
x: 0, y: 0, w: SCREEN_W, h: SCREEN_H,
|
||||
r: ov[:r], g: ov[:g], b: ov[:b], a: ov[:a]
|
||||
}
|
||||
end
|
||||
|
||||
def render_transition(args)
|
||||
return unless args.state.transitioning
|
||||
|
||||
elapsed = Kernel.tick_count - args.state.transition_tick
|
||||
half = ROOM_TRANSITION_FRAMES / 2
|
||||
|
||||
alpha = if elapsed < half
|
||||
(elapsed.to_f / half * 255).to_i
|
||||
else
|
||||
((ROOM_TRANSITION_FRAMES - elapsed).to_f / half * 255).to_i
|
||||
end
|
||||
|
||||
alpha = alpha.clamp(0, 255)
|
||||
|
||||
args.outputs.solids << {
|
||||
x: 0, y: 0, w: SCREEN_W, h: SCREEN_H,
|
||||
r: 0, g: 0, b: 0, a: alpha
|
||||
}
|
||||
end
|
||||
82
app/render_ui.rb
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
def render_ui(args)
|
||||
palette = current_palette(args)
|
||||
member = args.state.current_member || MEMBER_HOST
|
||||
member_info = args.state.member_data ? args.state.member_data[member] : nil
|
||||
|
||||
# Current member name (top-left)
|
||||
if member_info
|
||||
args.outputs.labels << {
|
||||
x: 20, y: SCREEN_H - 20,
|
||||
text: member_info[:name],
|
||||
size_enum: 0,
|
||||
r: palette[:accent][:r], g: palette[:accent][:g], b: palette[:accent][:b]
|
||||
}
|
||||
end
|
||||
|
||||
# Room name (top-center)
|
||||
room = ROOMS[args.state.current_room]
|
||||
if room
|
||||
args.outputs.labels << {
|
||||
x: SCREEN_W / 2, y: SCREEN_H - 20,
|
||||
text: room[:name],
|
||||
size_enum: -1,
|
||||
alignment_enum: 1,
|
||||
r: palette[:text][:r], g: palette[:text][:g], b: palette[:text][:b], a: 180
|
||||
}
|
||||
end
|
||||
|
||||
# Memory count (top-right)
|
||||
collected = args.state.memories_collected ? args.state.memories_collected.length : 0
|
||||
args.outputs.labels << {
|
||||
x: SCREEN_W - 20, y: SCREEN_H - 20,
|
||||
text: "Memories: #{collected}",
|
||||
size_enum: -2,
|
||||
alignment_enum: 2,
|
||||
r: palette[:text][:r], g: palette[:text][:g], b: palette[:text][:b], a: 150
|
||||
}
|
||||
|
||||
# Interaction prompt
|
||||
if args.state.nearby_object && !args.state.examining
|
||||
args.outputs.labels << {
|
||||
x: SCREEN_W / 2, y: 30,
|
||||
text: "[E] Examine #{args.state.nearby_object[:name]}",
|
||||
size_enum: -1,
|
||||
alignment_enum: 1,
|
||||
r: palette[:accent][:r], g: palette[:accent][:g], b: palette[:accent][:b]
|
||||
}
|
||||
end
|
||||
|
||||
# Member switch hints (bottom-left, only show if multiple members unlocked)
|
||||
unlocked = args.state.unlocked_members
|
||||
if unlocked
|
||||
hints = []
|
||||
hints << '[1] Host' if unlocked[:host]
|
||||
hints << '[2] Guardian' if unlocked[:guardian]
|
||||
hints << '[3] Little' if unlocked[:little]
|
||||
hints << '[4] Analyst' if unlocked[:analyst]
|
||||
|
||||
if hints.length > 1
|
||||
args.outputs.labels << {
|
||||
x: 20, y: 30,
|
||||
text: hints.join(' '),
|
||||
size_enum: -3,
|
||||
r: 150, g: 150, b: 150, a: 100
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
# Member switch flash effect
|
||||
return unless args.state.member_switch_tick
|
||||
|
||||
flash_age = Kernel.tick_count - args.state.member_switch_tick
|
||||
return unless flash_age < 15
|
||||
|
||||
flash_alpha = ((15 - flash_age).to_f / 15 * 100).to_i
|
||||
flash_alpha = flash_alpha.clamp(0, 100)
|
||||
return unless flash_alpha > 0
|
||||
|
||||
args.outputs.solids << {
|
||||
x: 0, y: 0, w: SCREEN_W, h: SCREEN_H,
|
||||
r: palette[:accent][:r], g: palette[:accent][:g], b: palette[:accent][:b], a: flash_alpha
|
||||
}
|
||||
end
|
||||
277
app/repl.rb
Executable file
|
|
@ -0,0 +1,277 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# ===============================================================
|
||||
# Welcome to repl.rb
|
||||
# ===============================================================
|
||||
# You can experiement with code within this file. Code in this
|
||||
# file is only executed when you save (and only excecuted ONCE).
|
||||
# ===============================================================
|
||||
|
||||
# ===============================================================
|
||||
# REMOVE the "x" from the word "xrepl" and save the file to RUN
|
||||
# the code in between the do/end block delimiters.
|
||||
# ===============================================================
|
||||
|
||||
# ===============================================================
|
||||
# ADD the "x" to the word "repl" (make it xrepl) and save the
|
||||
# file to IGNORE the code in between the do/end block delimiters.
|
||||
# ===============================================================
|
||||
|
||||
# Remove the x from xrepl to run the code. Add the x back to ignore to code.
|
||||
xrepl do
|
||||
puts "The result of 1 + 2 is: #{1 + 2}"
|
||||
end
|
||||
|
||||
# ====================================================================================
|
||||
# Ruby Crash Course:
|
||||
# Strings, Numeric, Booleans, Conditionals, Looping, Enumerables, Arrays
|
||||
# ====================================================================================
|
||||
|
||||
# ====================================================================================
|
||||
# Strings
|
||||
# ====================================================================================
|
||||
# Remove the x from xrepl to run the code. Add the x back to ignore to code.
|
||||
xrepl do
|
||||
message = 'Hello World'
|
||||
puts "The value of message is: #{message}"
|
||||
puts "Any value can be interpolated within a string using \#{}."
|
||||
puts "Interpolated message: #{message}."
|
||||
puts "This #{message} is not interpolated because the string uses single quotes."
|
||||
end
|
||||
|
||||
# ====================================================================================
|
||||
# Numerics
|
||||
# ====================================================================================
|
||||
# Remove the x from xrepl to run the code. Add the x back to ignore to code.
|
||||
xrepl do
|
||||
a = 10
|
||||
puts "The value of a is: #{a}"
|
||||
puts "a + 1 is: #{a + 1}"
|
||||
puts "a / 3 is: #{a / 3}"
|
||||
end
|
||||
|
||||
# Remove the x from xrepl to run the code. Add the x back to ignore to code.
|
||||
xrepl do
|
||||
b = 10.12
|
||||
puts "The value of b is: #{b}"
|
||||
puts "b + 1 is: #{b + 1}"
|
||||
puts "b as an integer is: #{b.to_i}"
|
||||
puts ''
|
||||
end
|
||||
|
||||
# ====================================================================================
|
||||
# Booleans
|
||||
# ====================================================================================
|
||||
# Remove the x from xrepl to run the code. Add the x back to ignore to code.
|
||||
xrepl do
|
||||
c = 30
|
||||
puts "The value of c is #{c}."
|
||||
|
||||
puts 'This if statement ran because c is truthy.' if c
|
||||
end
|
||||
|
||||
# Remove the x from xrepl to run the code. Add the x back to ignore to code.
|
||||
xrepl do
|
||||
d = false
|
||||
puts "The value of d is #{d}."
|
||||
|
||||
puts 'This if statement ran because d is falsey, using the not operator (!) makes d evaluate to true.' unless d
|
||||
|
||||
e = nil
|
||||
puts "Nil is also considered falsey. The value of e is: #{e}."
|
||||
|
||||
puts 'This if statement ran because e is nil (a falsey value).' unless e
|
||||
end
|
||||
|
||||
# ====================================================================================
|
||||
# Conditionals
|
||||
# ====================================================================================
|
||||
# Remove the x from xrepl to run the code. Add the x back to ignore to code.
|
||||
xrepl do
|
||||
i_am_true = true
|
||||
i_am_false = false
|
||||
|
||||
puts '======== if statement'
|
||||
i_am_one = 1
|
||||
puts 'This was printed because i_am_one is truthy.' if i_am_one
|
||||
|
||||
puts '======== if/else statement'
|
||||
if i_am_false
|
||||
puts 'This will NOT get printed because i_am_false is false.'
|
||||
else
|
||||
puts 'This was printed because i_am_false is false.'
|
||||
end
|
||||
|
||||
puts '======== if/elsif/else statement'
|
||||
if i_am_false
|
||||
puts 'This will NOT get printed because i_am_false is false.'
|
||||
elsif i_am_true
|
||||
puts 'This was printed because i_am_true is true.'
|
||||
else
|
||||
puts 'This will NOT get printed i_am_true was true.'
|
||||
end
|
||||
|
||||
puts '======== case statement '
|
||||
i_am_one = 1
|
||||
case i_am_one
|
||||
when 10
|
||||
puts 'case equaled: 10'
|
||||
when 9
|
||||
puts 'case equaled: 9'
|
||||
when 5
|
||||
puts 'case equaled: 5'
|
||||
when 1
|
||||
puts 'case equaled: 1'
|
||||
else
|
||||
puts "Value wasn't cased."
|
||||
end
|
||||
|
||||
puts '======== different types of comparisons'
|
||||
puts 'equal (4 == 4)' if 4 == 4
|
||||
|
||||
puts 'not equal (4 != 3)' if 4 != 3
|
||||
|
||||
puts 'less than (3 < 4)' if 3 < 4
|
||||
|
||||
puts 'greater than (4 > 3)' if 4 > 3
|
||||
|
||||
puts 'or statement ((4 > 3) || (3 < 4) || false)' if (4 > 3) || (3 < 4) || false
|
||||
|
||||
puts 'and statement ((4 > 3) && (3 < 4))' if (4 > 3) && (3 < 4)
|
||||
end
|
||||
|
||||
# ====================================================================================
|
||||
# Looping
|
||||
# ====================================================================================
|
||||
# Remove the x from xrepl to run the code. Add the x back to ignore to code.
|
||||
xrepl do
|
||||
puts '======== times block'
|
||||
3.times do |i|
|
||||
puts i
|
||||
end
|
||||
puts '======== range block exclusive'
|
||||
(0...3).each do |i|
|
||||
puts i
|
||||
end
|
||||
puts '======== range block inclusive'
|
||||
(0..3).each do |i|
|
||||
puts i
|
||||
end
|
||||
end
|
||||
|
||||
# ====================================================================================
|
||||
# Enumerables
|
||||
# ====================================================================================
|
||||
# Remove the x from xrepl to run the code. Add the x back to ignore to code.
|
||||
xrepl do
|
||||
puts '======== array each'
|
||||
colors = %w[red blue yellow]
|
||||
colors.each do |color|
|
||||
puts color
|
||||
end
|
||||
|
||||
puts '======== array each_with_index'
|
||||
colors = %w[red blue yellow]
|
||||
colors.each_with_index do |color, i|
|
||||
puts "#{color} at index #{i}"
|
||||
end
|
||||
end
|
||||
|
||||
# Remove the x from xrepl to run the code. Add the x back to ignore to code.
|
||||
xrepl do
|
||||
puts '======== single parameter function'
|
||||
def add_one_to(n)
|
||||
n + 5
|
||||
end
|
||||
|
||||
puts add_one_to(3)
|
||||
|
||||
puts '======== function with default value'
|
||||
def function_with_default_value(v = 10)
|
||||
v * 10
|
||||
end
|
||||
|
||||
puts "passing three: #{function_with_default_value(3)}"
|
||||
puts "passing nil: #{function_with_default_value}"
|
||||
|
||||
puts '======== Or Equal (||=) operator for nil values'
|
||||
def function_with_nil_default_with_local(a = nil)
|
||||
result = a
|
||||
result ||= 'or equal operator was exected and set a default value'
|
||||
end
|
||||
|
||||
puts "passing 'hi': #{function_with_nil_default_with_local 'hi'}"
|
||||
puts "passing nil: #{function_with_nil_default_with_local}"
|
||||
end
|
||||
|
||||
# ====================================================================================
|
||||
# Arrays
|
||||
# ====================================================================================
|
||||
# Remove the x from xrepl to run the code. Add the x back to ignore to code.
|
||||
xrepl do
|
||||
puts '======== Create an array with the numbers 1 to 10.'
|
||||
one_to_ten = (1..10).to_a
|
||||
puts one_to_ten
|
||||
|
||||
puts '======== Create a new array that only contains even numbers from the previous array.'
|
||||
one_to_ten = (1..10).to_a
|
||||
evens = one_to_ten.find_all(&:even?)
|
||||
puts evens
|
||||
|
||||
puts '======== Create a new array that rejects odd numbers.'
|
||||
one_to_ten = (1..10).to_a
|
||||
also_even = one_to_ten.reject(&:odd?)
|
||||
puts also_even
|
||||
|
||||
puts '======== Create an array that doubles every number.'
|
||||
one_to_ten = (1..10).to_a
|
||||
doubled = one_to_ten.map do |number|
|
||||
number * 2
|
||||
end
|
||||
puts doubled
|
||||
|
||||
puts '======== Create an array that selects only odd numbers and then multiply those by 10.'
|
||||
one_to_ten = (1..10).to_a
|
||||
odd_doubled = one_to_ten.find_all(&:odd?).map do |odd_number|
|
||||
odd_number * 10
|
||||
end
|
||||
puts odd_doubled
|
||||
|
||||
puts '======== All combination of numbers 1 to 10.'
|
||||
one_to_ten = (1..10).to_a
|
||||
all_combinations = one_to_ten.product(one_to_ten)
|
||||
puts all_combinations
|
||||
|
||||
puts '======== All uniq combinations of numbers. For example: [1, 2] is the same as [2, 1].'
|
||||
one_to_ten = (1..10).to_a
|
||||
uniq_combinations =
|
||||
one_to_ten.product(one_to_ten)
|
||||
.map(&:sort).uniq
|
||||
puts uniq_combinations
|
||||
end
|
||||
|
||||
# ====================================================================================
|
||||
# Advanced Arrays
|
||||
# ====================================================================================
|
||||
# Remove the x from xrepl to run the code. Add the x back to ignore to code.
|
||||
xrepl do
|
||||
puts '======== All unique Pythagorean Triples between 1 and 40 sorted by area of the triangle.'
|
||||
|
||||
one_to_hundred = (1..40).to_a
|
||||
triples =
|
||||
one_to_hundred.product(one_to_hundred).map do |width, height|
|
||||
[width, height, Math.sqrt(width**2 + height**2)]
|
||||
end.find_all do |_, _, hypotenuse|
|
||||
hypotenuse.to_i == hypotenuse
|
||||
end.map do |triangle|
|
||||
triangle.map(&:to_i)
|
||||
end.uniq(&:sort).map do |width, height, hypotenuse|
|
||||
[width, height, hypotenuse, (width * height) / 2]
|
||||
end.sort_by do |_, _, _, area|
|
||||
area
|
||||
end
|
||||
|
||||
triples.each do |width, height, hypotenuse, area|
|
||||
puts "(#{width}, #{height}, #{hypotenuse}) = #{area}"
|
||||
end
|
||||
end
|
||||
190
app/rooms.rb
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
# Map characters to tile types
|
||||
CHAR_TO_TILE = {
|
||||
'.' => TILE_EMPTY,
|
||||
'W' => TILE_WALL,
|
||||
'D' => TILE_DOOR,
|
||||
'O' => TILE_OBJECT,
|
||||
'M' => TILE_MEMORY,
|
||||
'N' => TILE_NPC,
|
||||
'H' => TILE_HIDDEN
|
||||
}
|
||||
|
||||
|
||||
ROOMS = {
|
||||
bedroom: {
|
||||
name: 'Bedroom',
|
||||
layout: [
|
||||
'WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW',
|
||||
'W..............................W',
|
||||
'W..OO..........................W',
|
||||
'W..OO..........................W',
|
||||
'W..............................W',
|
||||
'W..............................W',
|
||||
'W..............O...............W',
|
||||
'W..............................W',
|
||||
'W..............................W',
|
||||
'W..............................W',
|
||||
'W..............................W',
|
||||
'W..............................W',
|
||||
'W..............................W',
|
||||
'W..............................W',
|
||||
'W.........................H....W',
|
||||
'W..............................W',
|
||||
'W..............................W',
|
||||
'WWWWWWWWWWWWWWWDWWWWWWWWWWWWWWWW'
|
||||
],
|
||||
doors: [
|
||||
{ col: 15, row: 17, target_room: :hallway_upstairs,
|
||||
spawn_x: 15 * TILE_SIZE, spawn_y: 2 * TILE_SIZE }
|
||||
],
|
||||
objects: [
|
||||
{
|
||||
id: :bed,
|
||||
cols: [2, 3], rows: [2, 3],
|
||||
name: 'Bed',
|
||||
examine: {
|
||||
host: "Your bed. It's unmade, as usual.",
|
||||
guardian: 'Defensible position. Back to the wall. Good.',
|
||||
little: "Ooh, it's so soft! Can we jump on it?",
|
||||
analyst: 'Standard twin mattress. Signs of restless sleep patterns.'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: :desk,
|
||||
cols: [14], rows: [6],
|
||||
name: 'Desk',
|
||||
examine: {
|
||||
host: 'A cluttered desk. Papers, pens, half-finished thoughts.',
|
||||
guardian: "...there's a letter here. Don't read it.",
|
||||
little: 'Can we draw something? Pleeease?',
|
||||
analyst: 'Multiple unfinished documents. Evidence of task-switching behavior.'
|
||||
}
|
||||
}
|
||||
],
|
||||
commentary: {
|
||||
host: ['This room feels smaller than it used to.',
|
||||
'...when did I last open the curtains?'],
|
||||
guardian: ['The window is locked. Good.',
|
||||
'Check under the bed.'],
|
||||
little: ['I wanna go explore!',
|
||||
'Is someone else here?'],
|
||||
analyst: ['Room dimensions: approximately 4x5 meters.',
|
||||
'Dust accumulation suggests infrequent cleaning.']
|
||||
}
|
||||
},
|
||||
|
||||
hallway_upstairs: {
|
||||
name: 'Upstairs Hallway',
|
||||
layout: [
|
||||
'WWWWWWWWWWWWWWWDWWWWWWWWWWWWWWWW',
|
||||
'W..............................W',
|
||||
'W..............................W',
|
||||
'W..............................W',
|
||||
'W..............................W',
|
||||
'W..............................W',
|
||||
'W..............................W',
|
||||
'W..............................W',
|
||||
'W..............................W',
|
||||
'W..............................W',
|
||||
'W..............................W',
|
||||
'W..............................W',
|
||||
'W..............................W',
|
||||
'W..............................W',
|
||||
'W..............................W',
|
||||
'W..............................W',
|
||||
'W..............................W',
|
||||
'WWWWWWWWWWWWWWWDWWWWWWWWWWWWWWWW'
|
||||
],
|
||||
doors: [
|
||||
{ col: 15, row: 17, target_room: :bedroom,
|
||||
spawn_x: 15 * TILE_SIZE, spawn_y: 2 * TILE_SIZE },
|
||||
{ col: 15, row: 0, target_room: :hallway_main,
|
||||
spawn_x: 15 * TILE_SIZE, spawn_y: (GRID_H - 3) * TILE_SIZE }
|
||||
],
|
||||
objects: [],
|
||||
commentary: {
|
||||
host: ['The hallway stretches on.',
|
||||
'Family photos line the walls... but the faces seem wrong.'],
|
||||
guardian: ['Too many doors. Too many entry points.',
|
||||
'Stay in the middle of the hall.'],
|
||||
little: ['This hall is super long! Race you to the end!',
|
||||
'The pictures are funny-looking.'],
|
||||
analyst: ['Standard residential hallway. Four doors observed.',
|
||||
'Photo frames: inconsistent timeline arrangement.']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def init_rooms(args)
|
||||
args.state.current_room ||= :bedroom
|
||||
args.state.transitioning ||= false
|
||||
args.state.transition_tick ||= 0
|
||||
args.state.transition_target ||= nil
|
||||
args.state.transition_spawn_x ||= nil
|
||||
args.state.transition_spawn_y ||= nil
|
||||
|
||||
# Parse room layouts into tile arrays
|
||||
unless args.state.rooms_parsed
|
||||
ROOMS.each do |_room_id, room|
|
||||
room[:parsed_tiles] = parse_room_layout(room[:layout])
|
||||
end
|
||||
args.state.rooms_parsed = true
|
||||
end
|
||||
|
||||
args.state.current_room_data = ROOMS[args.state.current_room]
|
||||
end
|
||||
|
||||
def parse_room_layout(layout)
|
||||
tiles = []
|
||||
layout.reverse.each_with_index do |row_str, row_idx|
|
||||
tiles[row_idx] = []
|
||||
row_str.chars.each_with_index do |ch, col_idx|
|
||||
tiles[row_idx][col_idx] = CHAR_TO_TILE[ch] || TILE_EMPTY
|
||||
end
|
||||
end
|
||||
tiles
|
||||
end
|
||||
|
||||
def find_door_at(args, col, row)
|
||||
room = ROOMS[args.state.current_room]
|
||||
return nil unless room
|
||||
|
||||
room[:doors].each do |door|
|
||||
door_row = GRID_H - 1 - door[:row]
|
||||
return door if door[:col] == col && door_row == row
|
||||
end
|
||||
|
||||
nil
|
||||
end
|
||||
|
||||
def start_room_transition(args, target_room, spawn_x, spawn_y)
|
||||
return if args.state.transitioning
|
||||
return unless ROOMS[target_room]
|
||||
|
||||
args.state.transitioning = true
|
||||
args.state.transition_tick = Kernel.tick_count
|
||||
args.state.transition_target = target_room
|
||||
args.state.transition_spawn_x = spawn_x
|
||||
args.state.transition_spawn_y = spawn_y
|
||||
end
|
||||
|
||||
def tick_room_transition(args)
|
||||
return unless args.state.transitioning
|
||||
|
||||
elapsed = Kernel.tick_count - args.state.transition_tick
|
||||
half = ROOM_TRANSITION_FRAMES / 2
|
||||
|
||||
# At the midpoint, switch rooms
|
||||
if elapsed == half
|
||||
args.state.current_room = args.state.transition_target
|
||||
args.state.current_room_data = ROOMS[args.state.current_room]
|
||||
args.state.player.x = args.state.transition_spawn_x
|
||||
args.state.player.y = args.state.transition_spawn_y
|
||||
end
|
||||
|
||||
# End transition
|
||||
return unless elapsed >= ROOM_TRANSITION_FRAMES
|
||||
|
||||
args.state.transitioning = false
|
||||
args.state.transition_target = nil
|
||||
end
|
||||
63
metadata/cvars.txt
Executable file
|
|
@ -0,0 +1,63 @@
|
|||
# ==========================
|
||||
# In-Game web server.
|
||||
# ==========================
|
||||
# The in-game web server is disabled by default. To enable it, uncomment the lines
|
||||
# below. For remote hotload to work, the webserver needs to be enabled, must have
|
||||
# the port set to 9001, and remote_clients must be set to true.
|
||||
# webserver.enabled=true
|
||||
# webserver.port=9001
|
||||
# webserver.remote_clients=true
|
||||
|
||||
# ==========================
|
||||
# Logging
|
||||
# ==========================
|
||||
# Log levels:
|
||||
# - spam
|
||||
# - debug
|
||||
# - info
|
||||
# - warn
|
||||
# - error
|
||||
# - unfiltered
|
||||
# - nothing
|
||||
# Default value is "debug" during development and "info" in production.
|
||||
# log.level=debug
|
||||
|
||||
# Log Level Char Preamble
|
||||
# Removes the log level char from log output (defaulted to true/enabled)
|
||||
# log.level_char=true
|
||||
|
||||
# Log Timing Preamble
|
||||
# Removes the log timing prefix from log output (defaulted to fals/disabled)
|
||||
# log.timing=false
|
||||
|
||||
# Log Subsystem Preamble
|
||||
# Removes the log subsystem prefix from log output (defaulted to true/enabled)
|
||||
# log.subsystem=true
|
||||
|
||||
# Log entries that should be excluded as a comma delimited list.
|
||||
# Available subsystems:
|
||||
# - Engine
|
||||
# - Game
|
||||
# - Render
|
||||
# - HTTP
|
||||
# - HTTPServer
|
||||
log.filter_subsystems=HTTPServer,HTTP
|
||||
|
||||
# Should use the whole display
|
||||
# equivalent to calling $gtk.set_window_fullscreen(true)
|
||||
# Note: If you use this flag, you must provide a way to
|
||||
# exit full sceen mode from your game and wire it up
|
||||
# to $gtk.set_window_fullscreen(false)
|
||||
# renderer.fullscreen=true
|
||||
|
||||
# Milliseconds to sleep per frame when the game is in
|
||||
# the background (zero to disable)
|
||||
# renderer.background_sleep=0
|
||||
|
||||
# Set the window as borderless.
|
||||
# Note: the ablity to quit the application via OS shortcuts will not
|
||||
# work if this value is true and you must provide a means to exit the
|
||||
# game and wire it up to $gtk.request_quit
|
||||
# OS Shortcuts for quitting such as ALT+F4 and CMD+Q will still work.
|
||||
# renderer.borderless=true
|
||||
|
||||
126
metadata/game_metadata.txt
Executable file
|
|
@ -0,0 +1,126 @@
|
|||
# Remove the pound sign and leading space for the properties below before publishing your game.
|
||||
devid=EndofTimee
|
||||
devtitle=End N
|
||||
gameid=rooms-we-share
|
||||
gametitle=Rooms We Share
|
||||
version=0.01
|
||||
icon=metadata/icon.png
|
||||
|
||||
# === Flags available at all licensing tiers ===
|
||||
|
||||
# === Orientation Portrait/Landscape ===
|
||||
# The orientation can be set to either ~landscape~ (1280x720), ~portrait~ (720x1280), ~landscape,portrait~, or ~portrait,landscape~. The default is ~landscape~.
|
||||
# If ~landscape,portrait~ or ~portrait,landscape~, then the first value is used as the starting orientation for your game.
|
||||
# orientation=landscape
|
||||
|
||||
# === Sprite Rendering scale quality ===
|
||||
# Defines the render scale quality for sprites. scale_quality=0 (default) is nearest neighbor, scale_quality=1 is linear, scale_quality=2 is antialiased.
|
||||
# - 0: nearest neighbor, pixel perfect scaling (use this if your game uses pixel-art)
|
||||
# - 1: linear, less blocky visuals
|
||||
# - 2: anisotropic/best, higher computational overhead than linear, but provides the best visual quality
|
||||
# scale_quality=0
|
||||
|
||||
# === Directories to Ignore during packaging ===
|
||||
# A comma delimited list of directories that should be ignored during the
|
||||
# publishing process (ONLY TOP LEVEL DIRECTORIES ARE TESTED UNLESS ignore_directories_recursively=true).
|
||||
# For example, if your game supports saves, you'd want to ignore
|
||||
# that directory (example format: ignore_directories=tmp,saves,dev,assets).
|
||||
# ignore_directories=saves
|
||||
|
||||
# If you want the ignore_directories list to be applied to child directories, set ignore_directories_recursively=true.
|
||||
# IMPORTANT: Any directory that matches the ignore list will be ignored (REGARDLESS OF HIERARCHY).
|
||||
# Be very careful in enabling this behavior as it can cause a child directory
|
||||
# to be ignored that you didn't intend to (especially if your ignore_directories list has
|
||||
# common/generic directory names).
|
||||
# For example:
|
||||
# ignore_directories value: saves
|
||||
# Directory structure:
|
||||
# - mygame
|
||||
# - saves <---- This directory will be ignored
|
||||
# - libs
|
||||
# - saves <---- This directory will be ignored
|
||||
# ignore_directories_recursively=false
|
||||
|
||||
# === Flags available in DragonRuby Game Toolkit Pro ====
|
||||
# Uncomment the entry below to bytecode compile your Ruby code
|
||||
# compile_ruby=false
|
||||
|
||||
# Uncomment the entry below to specify the package name for your APK
|
||||
# packageid=org.dev.gamename
|
||||
|
||||
# === Orientation Override for Mobile ==
|
||||
# You can override the orientation of your game for mobile devices. If this value isn't provided, it which will use
|
||||
# the value from ~orientation=~. Valid values for the override are portrait, or landscape
|
||||
# orientation_android=inherit
|
||||
# orientation_ios=inherit
|
||||
|
||||
# === HD Mode ===
|
||||
# HD Mode: when enabled, will give you 720p, 1080p, 1440p, 4k, and 5k rendering options
|
||||
# Check out the following YouTube Video for a demo of DragonRuby's HD Capabilities
|
||||
# https://youtu.be/Rnc6z84zaa4
|
||||
# hd=false
|
||||
|
||||
# === High DPI ===
|
||||
# Setting this property to true will enable High DPI rendering (try in combination with scale_quality to see what looks best)
|
||||
# highdpi=false
|
||||
|
||||
# === Texture Atlases ===
|
||||
# See sample app for texture atlas usage: =./samples/07_advanced_rendering_hd/02_texture_atlases=
|
||||
# DragonRuby will recursively search the following directory for texture atlases.
|
||||
# sprites_directory=sprites
|
||||
|
||||
# === All Screen Mode ===
|
||||
# To render edge to edge on the device, set hd_letterbox=false
|
||||
# NOTE: remove your game's letter box will mean more work for you since you have to
|
||||
# think about what you want to render outside of the games main 1280x720 logical
|
||||
# area.
|
||||
# hd_letterbox=true
|
||||
|
||||
# === Scaling options for All Screen Mode ===
|
||||
# You can specify the maximum scale for your game. hd_max_scale's default value is 0 which
|
||||
# means "stretch to fit" (while retaining a 16:9 aspect ratio). This scaling method is not
|
||||
# pixel perfect, but is a reasonable default for most games.
|
||||
# hd_max_scale=0
|
||||
|
||||
# hd_max_scale values other than 0 *will* be pixel perfect. Resolutions higher than your max scale
|
||||
# will give more area outside of your safe area that can be rendered to (if hd_letterbox=false)
|
||||
# or give you a bigger letterbox (if hd_letterbox=true).
|
||||
# Example:
|
||||
# - Assuming that the hd_max_scale=1 (which means a game max scale of 1280x720) and hd_letterbox=false...
|
||||
# - If the screen size is 2560x1440...
|
||||
# - The safe area of the game will be rendered at 1280x720 centered vertically and horizontally within 2560x1440.
|
||||
# - The horizontal centering of the game will mean that you have 640 pixels to the left and 640 pixels to the right of the game's safe area to render to.
|
||||
# - The vertical centering of the game will mean that you have 360 pixels above and 360 below the game's safe area to render to.
|
||||
#
|
||||
# Take a look at the following sample apps to see how updating hd_max_scale and hd_letterbox affects your game:
|
||||
# - ./samples/07_advanced_rendering_hd/03_allscreen_properties
|
||||
# - ./samples/99_genre_platformer/clepto_frog
|
||||
# - ./samples/99_genre_platformer/the_little_probe
|
||||
|
||||
# Available hd_max_scale values (other than hd_max_scale=0 which is described above)
|
||||
# 720p: Scales up to 1280x720
|
||||
# hd_max_scale=100
|
||||
|
||||
# HD+: scales up to 1600x900
|
||||
# hd_max_scale=125
|
||||
|
||||
# 1080p/Full HD: scales up to 1920x1080
|
||||
# hd_max_scale=150
|
||||
|
||||
# Full HD+: scales up to 2240x1260
|
||||
# hd_max_scale=175
|
||||
|
||||
# 1440p: scales up to 2560x1440
|
||||
# hd_max_scale=200
|
||||
|
||||
# 1800p: scales up to 3200x1800
|
||||
# hd_max_scale=250
|
||||
|
||||
# 4k: scales up to 3840x2160
|
||||
# hd_max_scale=300
|
||||
|
||||
# 5k: scales up to 6400x2880
|
||||
# NOTE: If you want a pixel perfect game for all resolutions. You'll want to use hd_max_scale=400.
|
||||
# If you don't want to worry about rendering game artifacts outside of the pixel perfect area,
|
||||
# keep hd_letterbox=true.
|
||||
# hd_max_scale=400
|
||||
BIN
metadata/icon.png
Executable file
|
After Width: | Height: | Size: 153 KiB |
13
metadata/ios_metadata.txt
Executable file
|
|
@ -0,0 +1,13 @@
|
|||
# ios_metadata.txt is used by the Pro version of DragonRuby Game Toolkit to create iOS apps.
|
||||
# Information about the Pro version can be found at: http://dragonruby.org/toolkit/game#purchase
|
||||
|
||||
# teamid needs to be set to your assigned Team Id which can be found at https://developer.apple.com/account/#/membership/
|
||||
teamid=
|
||||
# appid needs to be set to your application identifier which can be found at https://developer.apple.com/account/resources/identifiers/list
|
||||
appid=
|
||||
# appname is the name you want to show up underneath the app icon on the device. Keep it under 10 characters.
|
||||
appname=
|
||||
# devcert is the certificate to use for development/deploying to your local device. This is the NAME of the certificate as it's displayed in Keychain Access.
|
||||
devcert=
|
||||
# prodcert is the certificate to use for distribution to the app store. This is the NAME of the certificate as it's displayed in Keychain Access.
|
||||
prodcert=
|
||||
BIN
sprites/circle/black.png
Executable file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
sprites/circle/blue.png
Executable file
|
After Width: | Height: | Size: 4 KiB |
BIN
sprites/circle/gray.png
Executable file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
sprites/circle/green.png
Executable file
|
After Width: | Height: | Size: 4 KiB |
BIN
sprites/circle/indigo.png
Executable file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
sprites/circle/orange.png
Executable file
|
After Width: | Height: | Size: 4 KiB |
BIN
sprites/circle/red.png
Executable file
|
After Width: | Height: | Size: 4 KiB |
BIN
sprites/circle/violet.png
Executable file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
sprites/circle/white.png
Executable file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
sprites/circle/yellow.png
Executable file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
sprites/hexagon/black.png
Executable file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
sprites/hexagon/blue.png
Executable file
|
After Width: | Height: | Size: 2 KiB |
BIN
sprites/hexagon/gray.png
Executable file
|
After Width: | Height: | Size: 2 KiB |
BIN
sprites/hexagon/green.png
Executable file
|
After Width: | Height: | Size: 2 KiB |
BIN
sprites/hexagon/indigo.png
Executable file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
sprites/hexagon/orange.png
Executable file
|
After Width: | Height: | Size: 2 KiB |
BIN
sprites/hexagon/red.png
Executable file
|
After Width: | Height: | Size: 2 KiB |
BIN
sprites/hexagon/violet.png
Executable file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
sprites/hexagon/white.png
Executable file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
sprites/hexagon/yellow.png
Executable file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
sprites/isometric/black.png
Executable file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
sprites/isometric/blue.png
Executable file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
sprites/isometric/gray.png
Executable file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
sprites/isometric/green.png
Executable file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
sprites/isometric/indigo.png
Executable file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
sprites/isometric/orange.png
Executable file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
sprites/isometric/red.png
Executable file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
sprites/isometric/violet.png
Executable file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
sprites/isometric/white.png
Executable file
|
After Width: | Height: | Size: 2 KiB |
BIN
sprites/isometric/yellow.png
Executable file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
sprites/misc/dragon-0.png
Executable file
|
After Width: | Height: | Size: 13 KiB |
BIN
sprites/misc/dragon-1.png
Executable file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
sprites/misc/dragon-2.png
Executable file
|
After Width: | Height: | Size: 3 KiB |
BIN
sprites/misc/dragon-3.png
Executable file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
sprites/misc/dragon-4.png
Executable file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
sprites/misc/dragon-5.png
Executable file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
sprites/misc/explosion-0.png
Executable file
|
After Width: | Height: | Size: 267 B |
BIN
sprites/misc/explosion-1.png
Executable file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
sprites/misc/explosion-2.png
Executable file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
sprites/misc/explosion-3.png
Executable file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
sprites/misc/explosion-4.png
Executable file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
sprites/misc/explosion-5.png
Executable file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
sprites/misc/explosion-6.png
Executable file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
sprites/misc/explosion-sheet.png
Executable file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
sprites/misc/lowrez-ship-blue.png
Executable file
|
After Width: | Height: | Size: 109 B |
BIN
sprites/misc/lowrez-ship-red.png
Executable file
|
After Width: | Height: | Size: 104 B |
BIN
sprites/misc/simple-mood-16x16.png
Executable file
|
After Width: | Height: | Size: 14 KiB |
BIN
sprites/misc/star.png
Executable file
|
After Width: | Height: | Size: 711 B |
BIN
sprites/misc/tiny-star.png
Executable file
|
After Width: | Height: | Size: 112 B |
BIN
sprites/square/black.png
Executable file
|
After Width: | Height: | Size: 326 B |
BIN
sprites/square/blue.png
Executable file
|
After Width: | Height: | Size: 329 B |
BIN
sprites/square/empty.png
Executable file
|
After Width: | Height: | Size: 655 B |
BIN
sprites/square/gray.png
Executable file
|
After Width: | Height: | Size: 325 B |
BIN
sprites/square/green.png
Executable file
|
After Width: | Height: | Size: 329 B |
BIN
sprites/square/indigo.png
Executable file
|
After Width: | Height: | Size: 335 B |
BIN
sprites/square/orange.png
Executable file
|
After Width: | Height: | Size: 335 B |
BIN
sprites/square/red.png
Executable file
|
After Width: | Height: | Size: 335 B |
BIN
sprites/square/violet.png
Executable file
|
After Width: | Height: | Size: 334 B |
BIN
sprites/square/white.png
Executable file
|
After Width: | Height: | Size: 325 B |
BIN
sprites/square/yellow.png
Executable file
|
After Width: | Height: | Size: 319 B |
BIN
sprites/t-pose/black.png
Executable file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
sprites/t-pose/blue.png
Executable file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
sprites/t-pose/gray.png
Executable file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
sprites/t-pose/green.png
Executable file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
sprites/t-pose/indigo.png
Executable file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
sprites/t-pose/orange.png
Executable file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
sprites/t-pose/red.png
Executable file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
sprites/t-pose/violet.png
Executable file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
sprites/t-pose/white.png
Executable file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
sprites/t-pose/yellow.png
Executable file
|
After Width: | Height: | Size: 446 B |
BIN
sprites/tile/wall-0000.png
Executable file
|
After Width: | Height: | Size: 93 B |
BIN
sprites/tile/wall-0001.png
Executable file
|
After Width: | Height: | Size: 99 B |
BIN
sprites/tile/wall-0010.png
Executable file
|
After Width: | Height: | Size: 98 B |
BIN
sprites/tile/wall-0011.png
Executable file
|
After Width: | Height: | Size: 101 B |
BIN
sprites/tile/wall-0100.png
Executable file
|
After Width: | Height: | Size: 98 B |
BIN
sprites/tile/wall-0101.png
Executable file
|
After Width: | Height: | Size: 100 B |
BIN
sprites/tile/wall-0110.png
Executable file
|
After Width: | Height: | Size: 100 B |
BIN
sprites/tile/wall-0111.png
Executable file
|
After Width: | Height: | Size: 102 B |
BIN
sprites/tile/wall-1000.png
Executable file
|
After Width: | Height: | Size: 99 B |
BIN
sprites/tile/wall-1001.png
Executable file
|
After Width: | Height: | Size: 101 B |
BIN
sprites/tile/wall-1010.png
Executable file
|
After Width: | Height: | Size: 100 B |
BIN
sprites/tile/wall-1011.png
Executable file
|
After Width: | Height: | Size: 101 B |