Submission complete!
This is compiled for Windows, if you're on another platform, the .bas file is included. I was right down to the wire submitting, so I didn't have time to figure cross-platform stuff out.
See submissions and rate games at:
https://itch.io/jam/qb64-game-jam/entriesconst true = -1
const false = 0
const magnet = 1
' ===== Screen =====
const screenw = 800
const screenh = 600
const boardw = 255
const boardh = 255
const block_size = 31
dim shared stagew as integer
dim shared stageh as integer
const turn_max = 100
dim shared fullscreen as _unsigned long
fullscreen = _newimage(screenw, screenh, 32)
screen fullscreen
do: loop until _screenexists = true
_title "Round Table Flip"
_source fullscreen ' Prevent handles from ever being null
_dest fullscreen
type coordinate_dec
x as double
y as double
end type
type coordinate_int
x as long
y as long
end type
dim shared hue(6) as _unsigned long
const hue_transparent = 0
hue(hue_transparent) = _rgba32( 0, 0, 0, 0)
const hue_black = 1
hue(hue_black) = _rgba32( 0, 0, 0, 255)
const hue_white = 2
hue(hue_white) = _rgba32(255, 255, 255, 255)
const hue_red = 3
hue(hue_red) = _rgba32(255, 0, 0, 255) ' Only used for enemy health bars
const hue_green = 4
hue(hue_green) = _rgba32( 0, 255, 0, 255) ' Only used in debug menu
const hue_dkblue = 5
hue(hue_dkblue) = _rgba32( 0, 74, 149, 223) ' Windows
const hue_ltblue = 6
hue(hue_ltblue) = _rgba32( 0, 124, 249, 255)
dim shared camera as coordinate_int
dim shared move_offset as coordinate_dec
' --- Entity types ---
const e_warrior = 1
const e_archer = 2
const e_wizard = 3
const e_crate = 4
const e_crate_metal = 5
const entity_specs = 5
' --- Entity flags ---
const shield_down = 1 ' Warrior shield
const shield_up = 2
const summoned = 3 ' Wizard summoned crate
' --- Block types ---
const b_empty = 0
const b_grass = 1
const b_ground = 2
const b_ground_metal = 3
const b_spikes = 4
const b_plate = 5
const b_lever_l = 6
const b_lever_r = 7
const b_door_shut = 8
const b_door_open = 9
const b_telepad = 10
const b_goal = 11
const block_specs = 11
' --- Sprites ---
const spr_warrior_d_l = 1
const spr_warrior_d_r = 2
const spr_warrior_u_l = 3
const spr_warrior_u_r = 4
const spr_archer_d_l = 5
const spr_archer_d_r = 6
const spr_archer_u_l = 7
const spr_archer_u_r = 8
const spr_wizard_d_l = 9
const spr_wizard_d_r = 10
const spr_wizard_u_l = 11
const spr_wizard_u_r = 12
const spr_grass = 13
const spr_ground = 14
const spr_ground_metal = 15
const spr_crate = 16
const spr_crate_metal = 17
const spr_spikes = 18
const spr_plate = 19
const spr_lever_l = 20
const spr_lever_r = 21
const spr_door_shut = 22
const spr_door_open = 23
const spr_telepad = 24
const spr_goal = 25
const spr_magnetic = 26
const spr_summoned = 27
const spr_control = 28
const spr_shield = 29
const spr_psychic = 30
const sprite_total = 30
dim shared sprite_ref(sprite_total) as integer
' Use constants above as index to get sprite number for image files
' --- Sound effects ---
const sfx_menu_move = 1 ' Move menu cursor
const sfx_menu_confirm = 2 ' Confirm menu selection
const sfx_crush = 3
const sfx_lever = 4
const sfx_wind = 29
const sfx_music = 30
const sfx_total = 30
dim shared sfx(sfx_total, 100) as _unsigned long
call load_sfx
' ===== Images =====
dim shared block_image as _unsigned long
dim shared background as _unsigned long
dim shared fade_image as _unsigned long
dim shared store_screen as _unsigned long ' Anytime screen state should be stored
dim shared assembly as _unsigned long ' For assembling anything that can be done all at once
store_screen = _newimage(screenw, screenh, 32)
assembly = _newimage(screenw, screenh, 32)
call load_images
' ===== Block data =====
type block_spec_structure
solid as _byte ' true if blocks movement and gravity
metal as _byte ' false = nonmetallic, true = metallic, magnet = magnetic
sprite as integer
end type
dim shared block_spec(block_specs) as block_spec_structure
call set_block_spec_data
type block_structure
spec as _byte ' index of block type, named constants
switch as coordinate_int ' Which switch is responsible for toggling this block
flag as integer ' special status, such as wizard conjured block, named constants
metal as _byte ' false = nonmetallic, true = metallic, magnet = magnetic
end type
dim shared block(turn_max, boardw, boardh) as block_structure
' ===== Entity data =====
type entity_spec_structure
name as string
metal as _byte ' on spawn, false = nonmetallic, true = metallic, magnet = magnetic, includes warrior boots
flip as coordinate_int ' true if sprite has flipped versions on that axis
sprite as integer
end type
dim shared entity_spec(entity_specs) as entity_spec_structure
call set_entity_spec_data
type entity_structure
spec as _byte ' index of entity_spec
pos as coordinate_int
moving as _byte ' used by move_entity to mark entity as moving in the current step
flip as coordinate_int ' visual, flip.x is <-1 1>, flip.y is ^-1 1v
flag as integer ' special status, such as wizard conjured block, named constants
metal as _byte ' false = nonmetallic, true = metallic, magnet = magnetic
end type
dim shared entity(turn_max, 1000) as entity_structure
dim shared entity_count(turn_max) as integer
' ===== Movement chunking nodes =====
const node_push = 1
const node_magnet = 2
const node_support = 3
type node_structure
i as integer ' index of node entity
parent as integer ' parent node
connect as _byte ' type of node connection via constants above
end type
dim shared node(1000) as node_structure
dim shared node_count as integer
dim shared c_node as integer ' node currently being examined
' ===== Sprites =====
type sprite_structure
pos as coordinate_int ' position in sprite sheet
size as coordinate_int ' size of sprite in sheet
size_draw as coordinate_int ' size of sprite when displayed - equal to size.xy if no stretch
frames as integer ' sprite's frames of animation
fpf as _byte ' Frame counter ticks per animation frame (0 defaults to 1)
' A value of 2 will allow 2 ticks to go by before the next animation frame
offset as coordinate_int ' display position relative to hitbox position
hb_size as coordinate_int ' size of hitbox, to be copied to entity_spec().size.xy after parse
image as _unsigned long ' Handle of sprite sheet
end type
dim shared sprite_count as integer
dim shared sprite(1000) as sprite_structure
' ===== Fonts =====
type font_structure
image as _unsigned long
pos as coordinate_int
h as integer
w as integer
end type
' Alignment in font calls
const left_align = 0
const right_align = 1
const center_align = 2
const fonts = 2
dim shared font(fonts, 255) as font_structure
' Font references
const f_font = 1 ' Fixed-width, half size of blocks
const f_font_gold = 2
call initialize_font(f_font, "data\font.png")
call initialize_font(f_font_gold, "data\fontgold.png")
const cursor_offset = 40 ' Distance f_setback_blue's cursor moves left from text, when pointing at it
' ===== Menu options =====
dim shared option_restart_confirm as _byte ' true means instant restart will ask for confirmation
dim shared option_sound as _byte ' true is on
dim shared option_sensitivity as _byte ' Amount a stick needs to be tilted before input is registered
option_restart_confirm = false
option_sound = false
option_sensitivity = 7
' ===== Input handling =====
dim shared dev_keyboard as _byte ' Store device index, to be re-checked whenever inputs are involved
dim shared dev_gamepad as _byte
const keyboard = 1
const gamepad = 2
' References for press function and hold array
const armor_key = 1
const shield_key = 2
const jump_key = 3
const arrow_key = 4
const alchemy_key = 5
const block_key = 6
const action_key = 7
const gravity_key = 8
const up_key = 9
const down_key = 10
const left_key = 11
const right_key = 12
const switch_key = 13
const rewind_key = 14
const restart_key = 15
const ok_key = 16
const cancel_key = 17
const enter_key = 18
const esc_key = 19
' Input reference and binding data
const keybind_count = 19 ' Number of gameplay functions, including enter and esc
kc = keybind_count
dim shared keybind_overlap(kc, kc) as _byte ' True if slots can have the same key
dim shared keybind_name$(kc) ' Name of keybind slots - "WEAPON, UP" etc
dim shared key_name$(512, 2) ' Names of keyboard keys and gamepad buttons
dim shared keybind(kc, 2) as integer ' Contains key code assigned by player
dim shared keybind_edit(kc, 2) as integer ' Used during keybind menu, overwrites keybind() on exit
dim shared keybind_default(kc, 2) as integer ' Defaults in case player wants to reset
call set_key_data
dim shared keybind_error(kc, 2) as single ' for flashing red when attempting to bind a duplicate
' Input tracking flags
dim shared press(kc) as _byte ' What was pressed this frame
dim shared hold(kc) as _byte ' What was pressed last frame
' ===== Directions =====
const up = 1
const right = 2
const down = 3
const left = 4
dim shared delta(4) as coordinate_int
delta(up).x = 0: delta(up).y = -1
delta(right).x = 1: delta(right).y = 0
delta(down).x = 0: delta(down).y = 1
delta(left).x = -1: delta(left).y = 0
' ===== Sorting =====
type sort_structure
s_index as integer ' Reference to array being sorted
s_value as single ' Value being used for sorting
end type
dim shared sorting(1000) as sort_structure ' Before sort
dim shared sorting_count as integer
dim shared sorted(1000) as sort_structure ' After sort
dim shared sorted_count as integer
' ===== Misc data =====
dim shared current_stage as _byte
const total_stages = 4
dim shared stage_name$(total_stages)
stage_name$(1) = "QUEST"
stage_name$(2) = "LOCKS"
stage_name$(3) = "REUNION"
stage_name$(4) = "DESCENT"
dim shared turn as integer ' gameplay is turn-based, this value increments each time player makes a move
' full stage data is copied into the new turn, then altered
' rewinding simply decrements this value, which auto-reverts to old state
dim shared last_turn as integer ' turn cannot rewind into this
dim shared turn_wrap as _byte ' rewind won't wrap around to turn_max until this is set to true
dim shared gravity(turn_max) as _byte ' uses directional constants above - up, down
dim shared control as _byte ' Which character is being controlled
call parse_sprites(block_image)
call set_sprite_ref
' ===== Main =====
call load_settings
call set_hold(true)
call title
system
' ===== Routine index =====
'--- Core ---
'title
'play_stage
'option_menu
'keybind_menu
'--- Important ---
'f new_press
'update_inputs
'set_press
'set_hold
'update_gravity
'update_camera
'spawn
'despawn
'move_entity
' move_marked_entities
' add_node
' remove_node
' entity_has_node
' f magnetized
'use_lever
'detect_devices
'press_any_key
'set_default_keybinds
'f confirm
'load_stage
'--- Conversion ---
'f plus_limit
'f toggle
'f half
'f inthalf
'f sq
'f wrap
'f rounding
'f round_up
'f mod_dec
'f text_width
'f text_contains
'f trim$
'sort
'--- Shorthand ---
'f get_dir
'f on_board
'--- Loading ---
'load_settings
'save_settings
'load_images
'load_sfx
'load_stage
'initialize_font
'parse_sprites
'f scan_text
'f scan_right
'f scan_down
'scan_error
'--- Display ---
'draw_background
'draw_stage
' draw_sprite
'glass_fonts
'round_rect
'f text_tag_replace
'f text_replace
'capture_screen
'restore_screen
'clear_image
'overlay
'play_sound
' play_menu_move
' play_menu_confirm
'--- Initial data ---
'set_key_data
'set_sprite_ref
'set_block_spec_data
' --------------------------
' ========== Core ==========
' --------------------------
sub title
' 0-Start
' 1-Options
' 2-Quit
do
c = 0
call set_hold(true)
' Title screen
do
_limit 60
call clear_image(fullscreen, hue(hue_dkblue))
call draw_background
d& = fullscreen
call glass_fonts("ROUND TABLE FLIP", f_font_gold, fullscreen, inthalf(screenw), 120, center_align)
' Menu options
f = f_font
x1 = 350: y1 = 270: h = int(font(f, 0).h * 1.2)
call glass_fonts("Start", f, fullscreen, inthalf(screenw), y1, left_align)
call glass_fonts("Options", f, fullscreen, inthalf(screenw), y1 + h, left_align)
call glass_fonts("Quit", f, fullscreen, inthalf(screenw), y1 + (h * 2), left_align)
' Cursor
call glass_fonts("@", f_font, fullscreen, x1 - cursor_offset, y1 + (c * h), left_align)
call glass_fonts("By johannhowitzer, for the 2021 QB64 Game Jam", f, fullscreen, inthalf(screenw), 500, center_align)
_display
if new_press(up_key) = true and new_press(down_key) = false then
c = wrap(c - 1, 0, 2)
elseif new_press(down_key) = true and new_press(up_key) = false then
c = wrap(c + 1, 0, 2)
end if
if new_press(esc_key) = true or new_press(cancel_key) = true then
c = 2
end if
if new_press(enter_key) = true or new_press(ok_key) = true then
if c = 0 then ' Start
call play_menu_confirm
exit do
elseif c = 1 then ' Options
call play_menu_confirm
call option_menu
c = 0
elseif c = 2 then ' Quit
exit sub
end if
end if
call update_inputs
loop
do
call load_stage(current_stage)
q = play_stage
loop until q = true
loop
end sub
function play_stage
play_stage = false
turn = 1
last_turn = 1
turn_wrap = false
moved = true
d_move = false
action = false
call set_hold(true)
do
_limit 60
if _sndplaying(sfx(sfx_music, 1)) = false then play_sound(sfx_music)
if moved = true then
' Copy state
dt = wrap(turn + 1, 1, turn_max)
if dt < turn then turn_wrap = true
for y = 1 to boardh
for x = 1 to boardw
block(dt, x, y).spec = block(turn, x, y).spec
block(dt, x, y).switch.x = block(turn, x, y).switch.x
block(dt, x, y).switch.y = block(turn, x, y).switch.y
block(dt, x, y).flag = block(turn, x, y).flag
block(dt, x, y).metal = block(turn, x, y).metal
next x
next y
for n = 1 to entity_count(turn)
entity(dt, n).spec = entity(turn, n).spec
entity(dt, n).pos.x = entity(turn, n).pos.x
entity(dt, n).pos.y = entity(turn, n).pos.y
entity(dt, n).moving = entity(turn, n).moving
entity(dt, n).flip.x = entity(turn, n).flip.x
entity(dt, n).flip.y = entity(turn, n).flip.y
entity(dt, n).flag = entity(turn, n).flag
entity(dt, n).metal = entity(turn, n).metal
next n
entity_count(dt) = entity_count(turn)
gravity(dt) = gravity(turn)
' Increment turn
turn = dt
last_turn = dt
' Process what happened this turn
if d_move = left or d_move = right then
for n = 1 to entity_count(turn)
entity(turn, n).moving = false
next n
call move_entity(control, d_move, false)
call move_marked_entities(d_move)
entity(turn, control).flip.x = delta(d_move).x
end if
select case action
case armor_key
entity(turn, e_warrior).metal = toggle(entity(turn, e_warrior).metal, true, magnet)
case shield_key
entity(turn, e_warrior).flag = toggle(entity(turn, e_warrior).flag, shield_down, shield_up)
case jump_key
dx = get_dir(entity(turn, e_archer).flip.x, 0)
dy = get_dir(0, -delta(gravity(turn)).y)
for n = 1 to entity_count(turn)
entity(turn, n).moving = false
next n
call move_entity(control, dy, false)
call move_marked_entities(dy)
for n = 1 to entity_count(turn)
entity(turn, n).moving = false
next n
call move_entity(control, dy, true)
call move_marked_entities(dy)
for n = 1 to entity_count(turn)
entity(turn, n).moving = false
next n
call move_entity(control, dx, true)
call move_marked_entities(dx)
case arrow_key
d = entity(turn, e_archer).flip.x
px = entity(turn, e_archer).pos.x
lx = px + d
ly = entity(turn, e_archer).pos.y
do while on_board(lx, ly) = true
s = block(turn, lx, ly).spec
' Found a lever, use it
if s = b_lever_l or s = b_lever_r then
call use_lever(lx, ly)
exit do
end if
' Hit a solid block
if block_spec(s).solid = true then exit do
' Hit an entity
for n = 1 to entity_count(turn)
if entity(turn, n).pos.x = lx and entity(turn, n).pos.y = ly then exit do
next n
lx = lx + d
loop
for p = 1 to 5
_limit 60
call draw_stage
for x_p = px to lx step sgn(lx - px)
x1 = ((block_size + 1) * x_p) - camera.x
y1 = ((block_size + 1) * ly ) - camera.y
if x_p <> px then call draw_sprite(sprite_ref(spr_psychic), x1, y1)
next x_p
_display
next p
case alchemy_key
for n = 1 to entity_count(turn)
if entity(turn, n).flag = summoned then
entity(turn, n).spec = toggle(entity(turn, n).spec, e_crate, e_crate_metal)
entity(turn, n).metal = toggle(entity(turn, n).metal, true, false)
end if
next n
case block_key
dx = entity(turn, e_wizard).pos.x + entity(turn, e_wizard).flip.x
dy = entity(turn, e_wizard).pos.y
if on_board(dx, dy) = true then
blocked = false
if block_spec(block(turn, dx, dy).spec).solid = true then blocked = true
for n = 1 to entity_count(turn)
if entity(turn, n).pos.x = dx and entity(turn, n).pos.y = dy then blocked = true
next n
if blocked = false then
' Remove any existing summoned block
for n = entity_count(turn) to 1 step -1
if entity(turn, n).flag = summoned then call despawn(n)
next n
call spawn(e_crate, dx, dy, 1, summoned)
end if
end if
case action_key
ex = entity(turn, control).pos.x
ey = entity(turn, control).pos.y
s = block(turn, ex, ey).spec
' Lever
if s = b_lever_l or s = b_lever_r then call use_lever(ex, ey)
' *** Telepad
end select
if reverse_gravity = true then gravity(turn) = toggle(gravity(turn), up, down)
call update_gravity
end if
call update_camera
call draw_stage
_display
' Death check
d = false
for n = 1 to 3
dx = entity(turn, n).pos.x
dy = entity(turn, n).pos.y
for n1 = 4 to entity_count(turn)
if entity(turn, n1).pos.x = dx and entity(turn, n1).pos.y = dy then
d = true
exit for
end if
next n1
next n
if d = true then
_sndpause(sfx(sfx_music, 1))
call play_sound(sfx_crush)
call play_sound(sfx_wind)
do
_limit 60
if _sndplaying(sfx(sfx_wind, 1)) = false then play_sound(sfx_wind)
call draw_stage
line(0, 0)-step(screenw, screenh), _rgba(255, 0, 0, 31), bf
_display
if new_press(rewind_key) = true then
dt = wrap(turn - 1, 1, turn_max)
if dt <> last_turn then turn = dt
call set_hold(true)
_sndstop(sfx(sfx_wind, 1))
exit do
end if
if new_press(restart_key) = true then
c = true
if option_restart_confirm = true then
c = confirm("Restart?", true)
end if
if c = true then
_sndstop(sfx(sfx_wind, 1))
exit function
end if
end if
if new_press(esc_key) = true then
call play_menu_confirm
c = confirm("Quit?", false)
if c = true then
play_stage = true
_sndstop(sfx(sfx_music, 1))
_sndstop(sfx(sfx_wind, 1))
exit function
end if
end if
call update_inputs
loop
end if
' Victory check
v = false
for n = 1 to 3
if block(turn, entity(turn, n).pos.x, entity(turn, n).pos.y).spec = b_goal then v = true
next n
if v = true then
current_stage = current_stage + 1
if current_stage > total_stages then
call draw_background
call glass_fonts("You completed the game!", f_font_gold, fullscreen, inthalf(screenw), 150, center_align)
call glass_fonts("This week has been a lot of fun,", f_font, fullscreen, inthalf(screenw), 250, center_align)
call glass_fonts("and I'm very happy to finish the jam.", f_font, fullscreen, inthalf(screenw), 300, center_align)
call glass_fonts("Thanks for playing!", f_font, fullscreen, inthalf(screenw), 400, center_align)
call press_any_key
current_stage = total_stages
end if
call save_settings
exit function
end if
moved = false
for b = 1 to switch_key - 1
if new_press(b) = true then moved = true
next b
if new_press(rewind_key) = true then
dt = wrap(turn - 1, 1, turn_max)
if dt > turn and turn_wrap = false then z = false else z = true
if dt <> last_turn and z = true then turn = dt
end if
if new_press(switch_key) = true then control = wrap(control + 1, 1, 3)
if new_press(gravity_key) = true then reverse_gravity = true else reverse_gravity = false
d_move = false
if new_press(left_key) = true and new_press(right_key) = false then d_move = left
if new_press(right_key) = true and new_press(left_key) = false then d_move = right
action = false
if new_press(action_key) = true then action = action_key
if control = e_warrior and new_press(armor_key) = true then action = armor_key
if control = e_warrior and new_press(shield_key) = true then action = shield_key
if control = e_archer and new_press(jump_key) = true then action = jump_key
if control = e_archer and new_press(arrow_key) = true then action = arrow_key
if control = e_wizard and new_press(alchemy_key) = true then action = alchemy_key
if control = e_wizard and new_press(block_key) = true then action = block_key
if new_press(restart_key) = true then
c = true
if option_restart_confirm = true then
c = confirm("Restart?", true)
end if
if c = true then exit function
end if
if new_press(esc_key) = true then
call play_menu_confirm
c = confirm("Quit?", false)
if c = true then
play_stage = true
_sndstop(sfx(sfx_music, 1))
exit function
end if
end if
call update_inputs
loop
end function
sub option_menu
menu_restart_confirm = 0 ' ON-[OFF]
menu_sound = 1 ' [ON]-OFF
menu_reset = 2
menu_controls = 3
menu_exit = 4
call set_hold(true)
d& = fullscreen
c = 0
do
_limit 60
call clear_image(d&, hue(hue_black))
call draw_background
' Menu options
f = f_font: a = left_align
x1 = 300: y1 = 270: h = int(font(f, 0).h * 1.2)
call glass_fonts("Restart confirmation", f, d&, x1, y1 + (h * menu_restart_confirm), a)
call glass_fonts("Sound", f, d&, x1, y1 + (h * menu_sound), a)
call glass_fonts("Reset progress", f, d&, x1, y1 + (h * menu_reset), a)
call glass_fonts("Controls", f, d&, x1, y1 + (h * menu_controls), a)
call glass_fonts("Done", f, d&, x1, y1 + (h * menu_exit), a)
' Option states
f = f_font
x2 = 520
rc$ = "OFF": sd$ = "OFF"
if option_restart_confirm = true then rc$ = "ON"
if option_sound = true then sd$ = "ON"
a = right_align
call glass_fonts(rc$, f, d&, x2, y1 + (h * menu_restart_confirm), a)
call glass_fonts(sd$, f, d&, x2, y1 + (h * menu_sound), a)
' Cursor
call glass_fonts("@", f_font, d&, x1 - cursor_offset, y1 + (c * h), left_align)
_display
' Input
x = 0: y = 0
if new_press(left_key) = true then x = -1
if new_press(right_key) = true or new_press(enter_key) = true or new_press(ok_key) = true then x = 1
if new_press(up_key) = true then y = -1
if new_press(down_key) = true then y = 1
s = false
if y <> 0 then s = true
if x <> 0 and c <= menu_sound then s = true
if s = true then call play_menu_move
c = wrap(c + y, 0, menu_exit)
if c = menu_restart_confirm then option_restart_confirm = wrap(option_restart_confirm + x, true, false)
if c = menu_sound then option_sound = wrap(option_sound + x, true, false)
if new_press(esc_key) = true then
call play_menu_confirm
exit do
end if
if new_press(enter_key) = true or new_press(ok_key) = true then
if c = menu_reset then
call play_menu_confirm
r = confirm("Really reset progress?", false)
if r = true then current_stage = 1
elseif c = menu_controls then
call play_menu_confirm
call keybind_menu
elseif c = menu_exit then
call play_menu_confirm
exit do
end if
end if
call update_inputs
loop
call save_settings
end sub
sub keybind_menu
call set_hold(true)
d& = fullscreen
x1 = 100: x2 = 343: x3 = 543 ' Three columns
y1 = 100 ' Top of column headers
f1 = f_font: f2 = f_font
h = font(f1, 0).h
w = 120 ' Width of a keybind setting display column
kc = keybind_count
menu_stick = kc + 1
menu_default = kc + 2
menu_exit = kc + 3
cx = 1: cy = 1
' Copy keybinds to editing array
for b = 1 to kc
keybind_edit(b, keyboard) = keybind(b, keyboard)
keybind_edit(b, gamepad) = keybind(b, gamepad)
keybind_error(b, keyboard) = 0
keybind_error(b, gamepad) = 0
next b
do
_limit 60
' Red error flash decay
for b = 1 to kc
keybind_error(b, keyboard) = plus_limit(keybind_error(b, keyboard), -0.05, 0)
keybind_error(b, gamepad) = plus_limit(keybind_error(b, gamepad), -0.05, 0)
next b
call clear_image(d&, hue(hue_black))
call draw_background
' Headers
call glass_fonts("KEYBOARD", f1, d&, x2, y1, left_align)
call glass_fonts("GAMEPAD", f1, d&, x3, y1, left_align)
' Enter/Esc grey frame
call round_rect(x1 - 3, y1 + (h * enter_key) - 1, (x2 - x1) + w, (h - 1) * 2, d&, _rgba32(255, 255, 255, 127), 1)
' Keybind slots
for n = 1 to kc
y2 = y1 + (n * h)
' Red error flash for attempted duplicate keybind
if keybind_error(n, keyboard) > 0 then call round_rect(x2 - 3, y2 - 1, w, h - 1, d&, _rgba32(255, 0, 0, keybind_error(n, keyboard) * 255), 1)
if keybind_error(n, gamepad) > 0 then call round_rect(x3 - 3, y2 - 1, w, h - 1, d&, _rgba32(255, 0, 0, keybind_error(n, gamepad) * 255), 1)
call glass_fonts(keybind_name$(n), f1, d&, x1, y2, left_align)
call glass_fonts(key_name$(keybind_edit(n, keyboard), keyboard), f2, d&, x2, y2, left_align)
if n < enter_key then call glass_fonts(key_name$(keybind_edit(n, gamepad), gamepad), f2, d&, x3, y2, left_align)
next n
call glass_fonts("ANALOG SENSITIVITY", f1, d&, x2, y1 + (h * menu_stick ), left_align)
call glass_fonts("RESET TO DEFAULT", f1, d&, x2, y1 + (h * menu_default), left_align)
call glass_fonts("EXIT", f1, d&, x2, y1 + (h * menu_exit ), left_align)
t$ = ""
select case cy
case armor_key: t$ = "Lancelot: Turn your magnetic coat on or off."
case shield_key: t$ = "Lancelot: Raise or lower your shield."
case jump_key: t$ = "Percival: Jump forward."
case arrow_key: t$ = "Percival: Interact with stuff from a distance."
case alchemy_key: t$ = "Galahad: Switch your crate between wooden and metal."
case block_key: t$ = "Galahad: Generate an artificial crate."
case action_key: t$ = "Interact with stuff."
case gravity_key: t$ = "Reverse the stage's gravity."
case up_key: t$ = "For menus only."
case down_key: t$ = "For menus only."
case left_key: t$ = "Move left."
case right_key: t$ = "Move right."
case switch_key: t$ = "Select another character."
case rewind_key: t$ = "Undo last move."
case restart_key: t$ = "Restart the stage."
case ok_key: t$ = "For menus only."
case cancel_key: t$ = "For menus only."
end select
call glass_fonts(t$, f1, d&, inthalf(screenw), y1 + (h * (menu_exit + 2)), center_align)
' Bar for analog sensitivity
line(x3, y1 + (h * menu_stick) - 2)-step( w * 0.9, h - 1), hue(hue_dkblue), b
line(x3 + 2, y1 + (h * menu_stick) )-step((w - 4) * (option_sensitivity * 0.1), h - 5), hue(hue_ltblue), bf
' Cursor
if cx = 1 then call glass_fonts("@", f1, d&, x2 - cursor_offset, y1 + (h * cy), left_align)
if cx = 2 then call glass_fonts("@", f1, d&, x3 - cursor_offset, y1 + (h * cy), left_align)
_display
' Directional inputs
dx = 0: dy = 0
if new_press(left_key) = true and new_press(right_key) = false then dx = -1
if new_press(right_key) = true and new_press(left_key) = false then dx = 1
if new_press(up_key) = true and new_press(down_key) = false then dy = -1
if new_press(down_key) = true and new_press(up_key) = false then dy = 1
' Cursor movement sound
s = false
if dy <> 0 then s = true
if dx <> 0 and cy < menu_default then s = true
if s = true then call play_menu_move
' Cursor movement
cx = wrap(cx + dx, 1, 2)
do
cy = wrap(cy + dy, 1, menu_exit)
loop while cy = enter_key or cy = esc_key ' Cursor skips over Enter and Esc
if cy > kc then cx = 1
' Directional option changing
if cy = menu_stick then option_sensitivity = wrap(option_sensitivity + dx, 1, 9)
' Exit
if new_press(esc_key) = true or new_press(cancel_key) = true then
call play_menu_confirm
exit do
end if
' Handling enter/ok input
if dx = 0 and dy = 0 then
if new_press(enter_key) = true or new_press(ok_key) = true then
if cy = menu_exit then
call play_menu_confirm
exit do
elseif cy = menu_default then
call play_menu_confirm
r = confirm("Reset to default?", false)
if r = true then
call set_default_keybinds
for b = 1 to kc
keybind_edit(b, keyboard) = keybind(b, keyboard)
keybind_edit(b, gamepad) = keybind(b, gamepad)
next b
end if
elseif cy = menu_stick then
call play_menu_move
option_sensitivity = wrap(option_sensitivity + 1, 1, 9)
' Rebinding a key
elseif cy <= kc - 2 then
call detect_devices
for n = 1 to keybind_count
keybind_error(n, keyboard) = 0
keybind_error(n, gamepad) = 0
next n
v = false
' Keyboard - fixed amount of buttons, with expected codes
if cx = 1 then
call play_menu_confirm
d = keyboard
' Draw blue behind selected keybind
call round_rect(x2 - 3, y1 + (h * cy) - 1, 120, h - 1, d&, _rgba32(0, 0, 255, 191), 1)
call glass_fonts(key_name$(keybind_edit(cy, d), d), f2, d&, x2, y1 + (h * cy), left_align)
' Wait for empty keyboard inputs
do
_limit 60
_display
e = true
z = _deviceinput(dev_keyboard)
for b = 1 to _lastbutton(dev_keyboard)
if _button(b) = true then e = false
next b
loop until e = true
' Wait for valid keyboard input
do
_limit 60
_display
if new_press(esc_key) = true then exit do ' Cancel binding
z = _deviceinput(dev_keyboard)
for b = 1 to _lastbutton(dev_keyboard)
if _button(b) = true and b <> 2 and b <> 29 then
v = b
exit do
end if
next b
call update_inputs
loop until v <> false
end if
' Gamepad - variable amount of buttons and axes
if cx = 2 and dev_gamepad <> false then
call play_menu_confirm
d = gamepad
z = _deviceinput(dev_gamepad)
' Draw blue behind selected keybind
call round_rect(x3 - 3, y1 + (h * cy) - 3, 120, h - 1, d&, _rgba32(0, 0, 255, 191), 1)
call glass_fonts(key_name$(keybind_edit(cy, d), d), f2, d&, x3, y1 + (h * cy), left_align)
call glass_fonts("Press ENTER to remove", f1, d&, x3, y1 + (h * (menu_exit + 1)), left_align)
' Wait for empty gamepad inputs
do
_limit 60
_display
e = true
z = _deviceinput(dev_gamepad)
for b = 1 to _lastbutton(dev_gamepad)
if _button(b) = true then e = false
next b
for a = 1 to _lastaxis(dev_gamepad)
if abs(_axis(a)) > (option_sensitivity * 0.1) then e = false
next a
loop until e = true
' Wait for valid gamepad input
do
_limit 60
_display
if new_press(esc_key) = true then exit do ' Cancel binding
if new_press(enter_key) = true then ' Remove existing button
call play_menu_confirm
keybind_edit(cy, d) = false
v = false
exit do
end if
z = _deviceinput(dev_gamepad)
for b = 1 to _lastbutton(dev_gamepad)
if _button(b) = true then
v = b
exit do
end if
next b
for a = 1 to _lastaxis(dev_gamepad)
ax = _axis(a)
if abs(ax) > (option_sensitivity * 0.1) then
v = a
if ax < 0 then v = v + 100 else v = v + 200
exit do
end if
next a
call update_inputs
loop until v <> false
end if
if v <> false then
' Check for duplicates
dupe = false
for b = 1 to keybind_count
if b <> cy and keybind_edit(b, d) = v and keybind_overlap(cy, b) = false then
dupe = true
keybind_error(b, d) = 1
end if
next b
' No duplicate, set new keybind
if dupe = false then
call play_menu_confirm
keybind_edit(cy, d) = v
else
call play_sound(sfx_explosion)
end if
end if
call set_press(true)
end if
end if
end if
call update_inputs
loop
' Copy new keybinds to keybind array
for b = 1 to kc
keybind(b, keyboard) = keybind_edit(b, keyboard)
keybind(b, gamepad) = keybind_edit(b, gamepad)
next b
call save_settings
end sub
' -------------------------------
' ========== Important ==========
' -------------------------------
function new_press(b)
new_press = false
if press(b) = true and hold(b) = false then new_press = true
end function
sub update_inputs
call detect_devices
for b = 1 to keybind_count
hold(b) = press(b)
press(b) = false
d = keyboard
if dev_keyboard <> false then
z = _deviceinput(dev_keyboard)
if _button(keybind(b, d)) = true then press(b) = true
end if
d = gamepad
if dev_gamepad <> false and keybind(b, d) <> false then
z = _deviceinput(dev_gamepad)
if keybind(b, d) < 100 then ' Button
if _button(keybind(b, d)) = true then press(b) = true
' Stick handling:
' keybind() set to 101, 102 etc. is an assignment of stick 1, 2 etc. in the negative direction
' keybind() set to 201, 202 etc. is an assignment of stick 1, 2 etc. in the positive direction
elseif keybind(b, d) > 200 then ' Stick positive
if _axis(keybind(b, d) - 200) > option_sensitivity * 0.1 then press(b) = true
else ' Stick negative
if _axis(keybind(b, d) - 100) < -option_sensitivity * 0.1 then press(b) = true
end if
end if
next b
end sub
sub set_press(p)
for b = 1 to keybind_count
press(b) = p
next b
end sub
sub set_hold(p)
for b = 1 to keybind_count
hold(b) = p
next b
end sub
sub update_gravity
do
for n = 1 to entity_count(turn)
entity(turn, n).moving = false
next n
' Mark all entities that can be moved by gravity
for n = 1 to entity_count(turn)
if entity(turn, n).moving = false then call move_entity(n, gravity(turn), false)
next n
call move_marked_entities(gravity(turn))
moved_any = false
for n = 1 to entity_count(turn)
if entity(turn, n).moving = true then moved_any = true
next n
loop until moved_any = false
end sub
sub update_camera
' Get camera destination
cx = ((block_size + 1) * entity(turn, control).pos.x) - inthalf(screenw)
cy = ((block_size + 1) * entity(turn, control).pos.y) - inthalf(screenh)
' Camera moves in the direction of that destination
camera.x = plus_limit(camera.x, round_up((cx - camera.x) * 0.3), cx)
camera.y = plus_limit(camera.y, round_up((cy - camera.y) * 0.3), cy)
end sub
sub spawn(i, x, y, f, flag)
entity_count(turn) = entity_count(turn) + 1
n = entity_count(turn)
entity(turn, n).spec = i
entity(turn, n).pos.x = x
entity(turn, n).pos.y = y
entity(turn, n).flip.x = f
entity(turn, n).flip.y = delta(gravity(turn)).y
entity(turn, n).flag = flag
entity(turn, n).metal = entity_spec(i).metal
end sub
sub despawn(d)
entity_count(turn) = entity_count(turn) - 1
for n = d to entity_count(turn)
entity(turn, n).spec = entity(turn, n + 1).spec
entity(turn, n).pos.x = entity(turn, n + 1).pos.x
entity(turn, n).pos.y = entity(turn, n + 1).pos.y
entity(turn, n).flip.x = entity(turn, n + 1).flip.x
entity(turn, n).flip.y = entity(turn, n + 1).flip.y
entity(turn, n).flag = entity(turn, n + 1).flag
entity(turn, n).metal = entity(turn, n + 1).metal
next n
end sub
sub move_entity(i, m, jump)
node_count = 1
node(1).i = i
node(1).parent = true ' can never be removed via false parent index
c_node = 1
g = gravity(turn)
' Assemble chunk
do
e = node(c_node).i
ex = entity(turn, e).pos.x
ey = entity(turn, e).pos.y
for n = 1 to entity_count(turn)
dx = entity(turn, n).pos.x - ex
dy = entity(turn, n).pos.y - ey
if abs(dx) + abs(dy) = 1 then
' Entity is next to node
d = get_dir(dx, dy)
' Logic for adding nodes to chunk
add = false
if d = m and n > 3 then add = node_push ' Push
' Moving character can shear off magnetic block beneath
if c_node = 1 and e <= 3 and d = g then z = false else z = true
if magnetized(entity(turn, e).metal, entity(turn, n).metal) = true and z = true then add = node_magnet ' Magnetism
' Characters can't support anything unless warrior with shield up
if e <= 3 and entity(turn, e).flag <> shield_up then z = false else z = true
if abs(d - g) = 2 and z = true then add = node_support ' Support
if add <> false then call add_node(n, c_node, add)
end if
next n
c_node = c_node + 1
loop until c_node > node_count
' Remove invalid nodes until none are removed
do
removed_node = false
c_node = 1
do
e = node(c_node).i
mx = entity(turn, e).pos.x + delta(m).x
my = entity(turn, e).pos.y + delta(m).y
' Logic for removing nodes from chunk
remove = false
if node(c_node).parent = false then remove = true ' Parent missing
' Moving into a solid block
if on_board(mx, my) = true then
if block_spec(block(turn, mx, my).spec).solid = true then remove = true
end if
' Moving into a non-chunk entity
for n = 1 to entity_count(turn)
if entity(turn, n).pos.x = mx and entity(turn, n).pos.y = my then
z = false
if e <= 3 and n <= 3 then z = true ' Character can move into character
if m = g and e > 3 and n <= 3 then z = true ' Non-character can fall into character
if entity_has_node(n) = false and z = false then remove = true
end if
next n
if remove = false and m = g then
' Falling onto warrior shield
if entity(turn, e_warrior).pos.x = mx and entity(turn, e_warrior).pos.y = my and entity(turn, e_warrior).flag = shield_up then remove = true
' Magnetism while falling
for d = 1 to 4
dx = entity(turn, e).pos.x + delta(d).x
dy = entity(turn, e).pos.y + delta(d).y
' Magnetized to a block
if on_board(dx, dy) = true then
if magnetized(entity(turn, e).metal, block(turn, dx, dy).metal) = true then remove = true
end if
' Magnetized to a non-chunk entity
for n = 1 to entity_count(turn)
if entity(turn, n).pos.x = dx and entity(turn, n).pos.y = dy then
if entity_has_node(n) = false and magnetized(entity(turn, e).metal, entity(turn, n).metal) = true then remove = true
end if
next n
next d
elseif remove = false and m <> g then
' First node is character trying to move with nothing underneath
if c_node = 1 and e <= 3 and jump = false then
gx = entity(turn, e).pos.x + delta(g).x
gy = entity(turn, e).pos.y + delta(g).y
s = true
if on_board(gx, gy) = true then
s = false
' Block underneath
if block_spec(block(turn, gx, gy).spec).solid = true then s = true
' Non-character entity underneath
for n = 4 to entity_count(turn)
if entity(turn, n).pos.x = gx and entity(turn, n).pos.y = gy then
s = true
exit for
end if
next n
' Warrior with shield underneath
if entity(turn, e_warrior).pos.x = gx and entity(turn, e_warrior).pos.y = gy and entity(turn, e_warrior).flag = shield_up then s = true
end if
if s = false then remove = true
end if
' *** Magnetism while moving
end if
if remove = true then
if c_node = 1 then exit sub ' Movement failed completely
removed_node = true
call remove_node(c_node)
end if
c_node = c_node + 1
loop until c_node > node_count
loop until removed_node = false
' Mark all entities involved in this move
for n = 1 to node_count
entity(turn, node(n).i).moving = true
next n
cls
end sub
sub move_marked_entities(d)
' Move entities
for n = 1 to entity_count(turn)
if entity(turn, n).moving = true then
entity(turn, n).pos.x = entity(turn, n).pos.x + delta(d).x
entity(turn, n).pos.y = entity(turn, n).pos.y + delta(d).y
if d = gravity(turn) then entity(turn, n).flip.y = delta(gravity(turn)).y
end if
next n
' Animate the move
mpf = 0.34
dx = delta(d).x
dy = delta(d).y
move_offset.x = -dx
move_offset.y = -dy
do
_limit 60
call update_camera
call draw_stage
_display
move_offset.x = plus_limit(move_offset.x, dx * mpf, 0)
move_offset.y = plus_limit(move_offset.y, dy * mpf, 0)
loop until move_offset.x = 0 and move_offset.y = 0
move_offset.x = 0
move_offset.y = 0
end sub
sub add_node(i, p, c)
' Abort if node already exists
for n = 2 to node_count ' Skip first node since its parent is true
if node(n).i = i and node(node(n).parent).i = node(p).i and node(n).connect = c then exit sub
next n
node_count = node_count + 1
node(node_count).i = i
node(node_count).parent = p
node(node_count).connect = c
end sub
sub remove_node(n)
node_count = node_count - 1
node(n).i = node(n + 1).i
node(n).parent = node(n + 1).parent
node(n).connect = node(n + 1).connect
' Adjust references
for p = 1 to node_count
if node(p).parent > n then node(p).parent = node(p).parent - 1
if node(p).parent = n then node(p).parent = false
if c_node => n then c_node = c_node - 1
next p
end sub
function entity_has_node(e)
entity_has_node = false
for n = 1 to node_count
if node(n).i = e then
entity_has_node = true
exit function
end if
next n
end function
function magnetized(m1, m2)
magnetized = false
if m1 = magnet or m2 = magnet then z = true else z = false
if m1 <> false and m2 <> false and z = true then magnetized = true
end function
sub use_lever(lx, ly)
call play_sound(sfx_lever)
block(turn, lx, ly).spec = toggle(block(turn, lx, ly).spec, b_lever_l, b_lever_r)
for y = 1 to stageh
for x = 1 to stagew
if block(turn, x, y).switch.x = lx and block(turn, x, y).switch.y = ly then
s = block(turn, x, y).spec
' Switch ground magnetism
if s = b_ground_metal then block(turn, x, y).metal = toggle(block(turn, x, y).metal, true, magnet)
' Switch door
if s = b_door_shut or s = b_door_open then block(turn, x, y).spec = toggle(block(turn, x, y).spec, b_door_shut, b_door_open)
end if
next x
next y
end sub
sub detect_devices
dev_keyboard = false
dev_gamepad = false
devices = _devices
for n = devices to 1 step -1
if left$(_device$(n), 10) = "[KEYBOARD]" then dev_keyboard = n
if left$(_device$(n), 12) = "[CONTROLLER]" then dev_gamepad = n
next n
end sub
sub press_any_key
do
_limit 60
_display
for b = 1 to keybind_count
if new_press(b) = true then exit sub
next b
call update_inputs
loop
end sub
sub set_default_keybinds
call detect_devices
for b = 1 to keybind_count
keybind(b, keyboard) = keybind_default(b, keyboard)
keybind(b, gamepad) = keybind_default(b, gamepad)
next b
' Eliminate any defaults that go beyond a gamepad's features
if dev_gamepad <> false then
d = gamepad
l = _lastbutton(dev_gamepad)
for b = 1 to keybind_count
if keybind(b, d) < 100 and keybind(b, d) > l then keybind(b, d) = false
next b
if _lastaxis(dev_gamepad) < 2 then
keybind(up_key, d) = false
keybind(down_key, d) = false
keybind(left_key, d) = false
keybind(right_key, d) = false
end if
end if
end sub
function confirm(t$, c)
call capture_screen
' pass c in with starting cursor position, true starts on YES
f = f_font
t2$ = "YES NO": t2b$ = "NO"
t3$ = "@"
h = font(f, 0).h
w = text_width(t$, f)
w2 = text_width(t2$, f): w2b = text_width(t2b$, f)
if w < w2 + (cursor_offset * 2) then w = w2 + (cursor_offset * 2)
x = inthalf(screenw)
y = inthalf(screenh) - h
w3 = inthalf(w) + h
call set_hold(true)
do
_limit 60
call restore_screen
call round_rect(x - w3, y - h, w3 * 2, (h * 4) - 4, fullscreen, hue(hue_dkblue), 2)
cx = x - inthalf(w2) - cursor_offset + ((c + 1) * (w2 - w2b))
call glass_fonts(t$, f, fullscreen, x, y, center_align)
call glass_fonts(t2$, f, fullscreen, x, y + h, center_align)
call glass_fonts(t3$, f, fullscreen, cx, y + h, left_align)
_display
if new_press(left_key) = true or new_press(right_key) = true then
call play_menu_move
c = wrap(c + 1, true, false)
else
if new_press(enter_key) = true or new_press(ok_key) = true then
call play_menu_confirm
exit do
end if
end if
call update_inputs
loop
confirm = c
end function
sub load_stage(stage)
dim b$(boardh)
for l = 1 to boardh
b$(l) = ""
next l
turn = 1
last_turn = turn_max
gravity(turn) = down ' Start with normal gravity by default
f_x = 1 ' Start with characters facing right by default
stagew = 0
stageh = 0
control = 1
entity_count(turn) = 0
for y = 1 to boardh
for x = 1 to boardw
block(turn, x, y).spec = b_empty
next x
next y
if stage = 2 then
l = 1
' 1 2 3 4 5 6 7 8 9 10
' 12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890
for n = 1 to 15
b$(l) = "##############################################################################################################": l = l + 1
next n
b$(l) = "##############################################################################################################": l = l + 1
b$(l) = "############################################ : # | : #####################################################": l = l + 1
b$(l) = "############################################ ~ # ~~~ #####################################################": l = l + 1
b$(l) = "############################################ ~~# # # #####################################################": l = l + 1
b$(l) = "############################################ # | # \ # #####################################################": l = l + 1
b$(l) = "############################################ # ~~#~~~# #####################################################": l = l + 1
b$(l) = "############################################ # ### # #####################################################": l = l + 1
b$(l) = "############################################ # ### # #####################################################": l = l + 1
b$(l) = "############################################ # : | ! #\ #####################################################": l = l + 1
b$(l) = "############################################ #~~~~~~~#~~ #####################################################": l = l + 1
b$(l) = "############################################ # # #####################################################": l = l + 1
b$(l) = "############################################ # 321 # #####################################################": l = l + 1
b$(l) = "############################################ # ~~~~~ # #####################################################": l = l + 1
b$(l) = "############################################ ##### #####################################################": l = l + 1
b$(l) = "############################################ ##### #####################################################": l = l + 1
b$(l) = "############################################~~~~#####~~~~#####################################################": l = l + 1
for n = 1 to 15
b$(l) = "##############################################################################################################": l = l + 1
next n
block(turn, 54, 17).switch.x = 55
block(turn, 54, 17).switch.y = 24
block(turn, 52, 17).switch.x = 55
block(turn, 52, 17).switch.y = 24
block(turn, 48, 17).switch.x = 55
block(turn, 48, 17).switch.y = 24
block(turn, 48, 20).switch.x = 55
block(turn, 48, 20).switch.y = 24
block(turn, 48, 24).switch.x = 52
block(turn, 48, 24).switch.y = 20
block(turn, 50, 24).switch.x = 52
block(turn, 50, 24).switch.y = 20
elseif stage = 4 then
l = 1
' 1 2 3 4 5 6 7 8 9 10
' 12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890
for n = 1 to 15
b$(l) = "##############################################################################################################": l = l + 1
next n
b$(l) = "#################################################%%###%%######################################################": l = l + 1
b$(l) = "################################################ #####################################################": l = l + 1
b$(l) = "################################################ #####################################################": l = l + 1
b$(l) = "################################################ #####################################################": l = l + 1
b$(l) = "################################################ #####################################################": l = l + 1
b$(l) = "################################################ ~~~ #####################################################": l = l + 1
b$(l) = "################################################ #####################################################": l = l + 1
b$(l) = "################################################ #####################################################": l = l + 1
b$(l) = "################################################ ! #####################################################": l = l + 1
b$(l) = "################################################ ~~~ #####################################################": l = l + 1
b$(l) = "################################################ #####################################################": l = l + 1
b$(l) = "################################################ #####################################################": l = l + 1
b$(l) = "################################################ #####################################################": l = l + 1
b$(l) = "################################################ ###################################################": l = l + 1
b$(l) = "################################################ \###################################################": l = l + 1
b$(l) = "################################################ ~~###################################################": l = l + 1
b$(l) = "################################################ #####################################################": l = l + 1
b$(l) = "################################################ #####################################################": l = l + 1
b$(l) = "################################################ 321 | +##################################################": l = l + 1
b$(l) = "################################################~~~~~~~~~~~~##################################################": l = l + 1
for n = 1 to 15
b$(l) = "##############################################################################################################": l = l + 1
next n
block(turn, 58, 34).switch.x = 59
block(turn, 58, 34).switch.y = 30
elseif stage = 1 then
l = 1
' 1 2 3 4 5 6 7 8 9 10
' 12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890
for n = 1 to 15
b$(l) = "##############################################################################################################": l = l + 1
next n
b$(l) = "##############################################################################################################": l = l + 1
b$(l) = "############################################# ##############################################################": l = l + 1
b$(l) = "############################################# ##############################################################": l = l + 1
b$(l) = "############################################ #############################################################": l = l + 1
b$(l) = "################################### ### ! #############################################################": l = l + 1
b$(l) = "################################### ~~~ ############################################################": l = l + 1
b$(l) = "################################ ############################################################": l = l + 1
b$(l) = "################################ +~~~~~~~~~## ####################################################": l = l + 1
b$(l) = "################################ ~~%%&&########### ####################################################": l = l + 1
b$(l) = "################################ ################# ####################################################": l = l + 1
b$(l) = "################################ ################# ####################################################": l = l + 1
b$(l) = "################################ ### ############# ##################################################": l = l + 1
b$(l) = "################################ ### ### | \##################################################": l = l + 1
b$(l) = "################################~~ # ~ ~~~~~ ~~##################################################": l = l + 1
b$(l) = "################################## = # ### ####################################################": l = l + 1
b$(l) = "##################################~~~~~~~ + ### ####################################################": l = l + 1
b$(l) = "#########################################~~~~~~~~###~~~~ ####################################################": l = l + 1
b$(l) = "######################################################## ####################################################": l = l + 1
b$(l) = "######################################################## ####################################################": l = l + 1
b$(l) = "################################################# ### ####################################################": l = l + 1
b$(l) = "################################################# ### ####################################################": l = l + 1
b$(l) = "############################################### ### ####################################################": l = l + 1
b$(l) = "############################## ###\ ### ####################################################": l = l + 1
b$(l) = "############################## ###~~ ### ####################################################": l = l + 1
b$(l) = "############################## = | ####################################################": l = l + 1
b$(l) = "############################## 321 ~~~~~~~ ~~~~~~~~~~~~~~####################################################": l = l + 1
b$(l) = "##############################~~~~~~#######~##################################################################": l = l + 1
for n = 1 to 15
b$(l) = "##############################################################################################################": l = l + 1
next n
block(turn, 55, 40).switch.x = 48
block(turn, 55, 40).switch.y = 38
block(turn, 51, 28).switch.x = 60
block(turn, 51, 28).switch.y = 28
elseif stage = 3 then
l = 1
' 1 2 3 4 5 6 7 8 9 10
' 12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890
for n = 1 to 15
b$(l) = "##############################################################################################################": l = l + 1
next n
b$(l) = "#######################################################################&&#####################################": l = l + 1
b$(l) = "###################################################################### ####################################": l = l + 1
b$(l) = "###################################################################### ####################################": l = l + 1
b$(l) = "###################################################################### ####################################": l = l + 1
b$(l) = "#######################################################%############## ####################################": l = l + 1
b$(l) = "############################################ #### ############ #################################": l = l + 1
b$(l) = "############################################ #################################": l = l + 1
b$(l) = "############################################ ! #################################": l = l + 1
b$(l) = "############################################ ~~~ ~~~~~~~ ~~~#################################": l = l + 1
b$(l) = "############################################ 1 + ~~~~ \+ ### ####### ####################################": l = l + 1
b$(l) = "############################################~~~~%####~~~~~### ####### ####################################": l = l + 1
b$(l) = "############################################################# ####### ####################################": l = l + 1
b$(l) = "############################################################# #######~~~~####################################": l = l + 1
b$(l) = "############################################################# ###############################################": l = l + 1
b$(l) = "############################################################# ###############################################": l = l + 1
b$(l) = "############################################################# ##### ###################################": l = l + 1
b$(l) = "############################################################# ##### ###################################": l = l + 1
b$(l) = "############################################### ##### ####### #### ~ ~ ~ ###################################": l = l + 1
b$(l) = "############################################ ###### #### # # # ###################################": l = l + 1
b$(l) = "############################################ # # # # #################################": l = l + 1
b$(l) = "############################################ 3 = ~~ | \#################################": l = l + 1
b$(l) = "############################################~~~%~##~~ ~~~~~~~~~~~~~~ # # # ~~#################################": l = l + 1
b$(l) = "##################################################### ############## # # # ###################################": l = l + 1
b$(l) = "#####################################################~############## ###################################": l = l + 1
b$(l) = "#################################################################### 2 ###################################": l = l + 1
b$(l) = "####################################################################~~~~~~~###################################": l = l + 1
for n = 1 to 15
b$(l) = "##############################################################################################################": l = l + 1
next n
block(turn, 66, 36).switch.x = 77
block(turn, 66, 36).switch.y = 36
block(turn, 56, 20).switch.x = 56
block(turn, 56, 20).switch.y = 25
end if
' Set blocks
for y = 1 to boardh
if b$(y) <> "" then stageh = y
l = len(b$(y))
for x = 1 to l
if l > stagew then stagew = l
t$ = mid$(b$(y), x, 1)
if t$ = " " then block(turn, x, y).spec = b_empty
if t$ = "~" then block(turn, x, y).spec = b_grass
if t$ = "#" then block(turn, x, y).spec = b_ground
if t$ = "&" or t$ = "%" then block(turn, x, y).spec = b_ground_metal
if t$ = "^" then block(turn, x, y).spec = b_spikes
if t$ = "_" then block(turn, x, y).spec = b_plate
if t$ = "\" then block(turn, x, y).spec = b_lever_l
if t$ = "/" then block(turn, x, y).spec = b_lever_r
if t$ = "|" then block(turn, x, y).spec = b_door_shut
if t$ = ":" then block(turn, x, y).spec = b_door_open
if t$ = "@" then block(turn, x, y).spec = b_telepad
if t$ = "!" then block(turn, x, y).spec = b_goal
block(turn, x, y).metal = block_spec(block(turn, x, y).spec).metal
if t$ = "%" then block(turn, x, y).metal = magnet
next x
next y
' Spawn characters
for c = 1 to 3
for y = 1 to boardh
for x = 1 to len(b$(y))
t$ = mid$(b$(y), x, 1)
if t$ = "1" or t$ = "2" or t$ = "3" then
if val(t$) = c then call spawn(c, x, y, f_x, false)
end if
next x
next y
next c
entity(turn, e_warrior).flag = shield_down
' Spawn other entities
for y = 1 to boardh
for x = 1 to len(b$(y))
select case mid$(b$(y), x, 1)
case "=": call spawn(e_crate, x, y, 1, false)
case "+": call spawn(e_crate_metal, x, y, 1, false)
end select
next x
next y
' Initial camera
camera.x = ((block_size + 1) * entity(turn, control).pos.x) - inthalf(screenw)
camera.y = ((block_size + 1) * entity(turn, control).pos.y) - inthalf(screenh)
end sub
' --------------------------------
' ========== Conversion ==========
' --------------------------------
function plus_limit(n, p, l)
q = n + p
if sgn(q - l) = sgn(p) then q = l
plus_limit = q
end function
function toggle(v, p, q)
if v = p then toggle = q
if v = q then toggle = p
end function
function half(n)
half = n * 0.5
end function
function inthalf(n)
inthalf = int(n * 0.5)
end function
function sq(n)
' For code clarity
sq = n * n
end function
function wrap(n, l1, h1)
' n is adjusted back within lower(l) and upper(h) bounds similar to mod operator
l = l1: h = h1 ' make sure h is never less than l, this also prevents division by zero
if h1 < l1 then
l = h1: h = l1
end if
x = (l - n) / ((h - l) + 1)
if x <> int(x) then x = x + 1
wrap = n + (int(x) * ((h - l) + 1))
end function
function rounding(n)
p = int(n)
if mod_dec(n, 1) > 0.5 then p = p + 1
rounding = p
end function
function round_up(n)
p = int(n)
if mod_dec(n, 1) <> 0 then p = p + 1
round_up = p
end function
function mod_dec(n, d)
mod_dec = n
if d = 0 then exit function ' Division by zero protection
mod_dec = ((n / d) - int(n / d)) * d
end function
function text_width(t$, f)
w = 0
for n = 1 to len(t$)
w = w + font(f, asc(mid$(t$, n, 1))).w + 1
next n
text_width = w - 1
end function
function text_contains(t$, c$)
text_contains = false
for n = 1 to len(t$) - len(c$) + 1
if mid$(t$, n, len(c$)) = c$ then
text_contains = n
exit function
end if
next n
end function
function trim$(t$)
trim$ = ""
for n = 1 to len(t$)
if mid$(t$, n, 1) <> " " then
trim$ = right$(t$, n - 1)
exit function
end if
next n
end function
sub sort(d)
' Before calling, put key values in sorting().s_index, .s_value, and sorting_count
' Takes s_index and s_value in sorting(), sorts them into sorted() by s_value, in direction of sgn(d)
c = 1
sorted(1).s_index = sorting(1).s_index
sorted(1).s_value = sorting(1).s_value
for n1 = 2 to sorting_count ' sorting() index being inserted
for n2 = 1 to c + 1 ' position in sorted() being checked
if n2 > c or sgn(sorted(n2).s_value - sorting(n1).s_value) = sgn(d) then
for n3 = c to n2 step -1 ' make space for insertion
sorted(n3 + 1).s_index = sorted(n3).s_index
sorted(n3 + 1).s_value = sorted(n3).s_value
next n3
sorted(n2).s_index = sorting(n1).s_index
sorted(n2).s_value = sorting(n1).s_value
c = c + 1
exit for
end if
next n2
next n1
sorted_count = c
end sub
' -------------------------------
' ========== Shorthand ==========
' -------------------------------
function get_dir(x, y)
get_dir = false
for d = 1 to 4
if delta(d).x = x and delta(d).y = y then get_dir = d
next d
end function
function on_board(x, y)
on_board = true
if x < 1 or x > boardw or y < 1 or y > boardh then on_board = false
end function
' -----------------------------
' ========== Loading ==========
' -----------------------------
sub load_settings
if _fileexists("settings.ini") = false then
call save_settings
exit sub
end if
open "settings.ini" for binary as #1
get #1, 1, keybind()
get #1, , current_stage
get #1, , option_restart_confirm
get #1, , option_sound
get #1, , option_sensitivity
close #1
call detect_devices
for b = 1 to keybind_count
' Reset invalid keyboard binds to default
if keybind(b, keyboard) < 1 or keybind(b, keyboard) > 512 then keybind(b, keyboard) = keybind_default(b, keyboard)
' Reset invalid gamepad binds to unset
if dev_gamepad <> false then
lb = _lastbutton(dev_gamepad)
la = _lastaxis(dev_gamepad)
if keybind(b, gamepad) < 100 then
if keybind(b, gamepad) < 1 or keybind(b, gamepad) > lb then keybind(b, gamepad) = false
else
if keybind(b, gamepad) > 200 then k = keybind(b, gamepad) - 200 else k = keybind(b, gamepad) - 100
if k < 1 or k > la then keybind(b, gamepad) = false
end if
end if
next b
' Reset invalid option states to default
if current_stage < 1 or current_stage > total_stages then current_stage = 1
if option_restart_confirm <> true then option_restart_confirm = false
if option_sound <> false then option_sound = true
if option_sensitivity < 1 or option_sensitivity > 9 then option_sensitivity = 7
end sub
sub save_settings
open "settings.ini" for binary as #1
put #1, 1, keybind()
put #1, , current_stage
put #1, , option_restart_confirm
put #1, , option_sound
put #1, , option_sensitivity
close #1
end sub
sub load_images
preserve& = _source
fade_image = _loadimage("data\fade.png")
block_image = _loadimage("data\block.png")
background = _loadimage("data\background.png")
_source preserve&
end sub
sub load_sfx
sfx(sfx_crush, 1) = _sndopen("data\crush.ogg")
sfx(sfx_lever, 1) = _sndopen("data\lever.ogg")
sfx(sfx_wind, 1) = _sndopen("data\wind.ogg")
sfx(sfx_music, 1) = _sndopen("data\music.ogg")
end sub
sub initialize_font(f, font$)
preserve& = _source
font(f, 0).image = _loadimage(font$)
_source font(f, 0).image
_clearcolor point(0, 0), font(f, 0).image
i& = font(f, 0).image
d~& = point(1, 0) ' Detection color
' Height
font(f, 0).h = scan_down(1, 2, i&, d~&) - 3
y = 0
for cy = 0 to 15
y = scan_down(1, y, i&, d~&) + 1
x = 1
for cx = 0 to 15
n = (cy * 16) + cx
font(f, n).pos.x = x ' Source position
font(f, n).pos.y = y
x = scan_right(x, y, i&, d~&) + 1
font(f, n).w = x - font(f, n).pos.x - 2 ' Variable width
next cx
next cy
_source preserve&
end sub
sub parse_sprites(i&)
preserve& = _source
_source i&
d~& = point(0, 0) ' Detection color
s = sprite_count + 1
x1 = 1 ' Top left of first sprite
y1 = 2
do
sprite(s).image = i&
' Source position
sprite(s).pos.x = x1
sprite(s).pos.y = y1
' Sprite size
x2 = scan_right(x1, y1, i&, d~&)
y2 = scan_down(x1, y1, i&, d~&)
sprite(s).size.x = x2 - x1 - 1
sprite(s).size.y = y2 - y1 - 1
' Animation frame count
x2 = scan_right(x2, y1, i&, d~&)
sprite(s).frames = int( ((x2 + 1) - x1) / (sprite(s).size.x + 2) )
if sprite(s).frames < 1 then sprite(s).frames = 1
' Frame counter ticks per animation frame
sprite(s).fpf = scan_right(x2, y1 - 1, i&, d~&) - x2
if sprite(s).fpf < 1 then sprite(s).fpf = 1
x2 = x2 + 1
' Sprite display position - relative to entity hitbox position
x_hb = scan_right(x2 - 1, y1, i&, d~&)
y_hb = scan_down(x2, y1 - 1, i&, d~&)
sprite(s).offset.x = x2 - x_hb
sprite(s).offset.y = y1 - y_hb
' #OPT If either offset is zero, this forces the other one to be zero as well
' Easy fix is to move the detection pixels outside the sprite area
' Hitbox size
sprite(s).hb_size.x = scan_right(x_hb, y1, i&, d~&) - x_hb
sprite(s).hb_size.y = scan_down(x2, y_hb, i&, d~&) - y_hb
y1 = y2 + 1
if point(x1 - 1, y1) = d~& then ' End of column
if point(x1, 0) = d~& then exit do ' No more columns
y1 = 2
x1 = scan_right(x1, 0, i&, d~&) + 1 ' Find new column
end if
s = s + 1
loop
sprite_count = s
_source preserve&
end sub
function scan_text(p1, t$, d$)
p = p1
do
p = p + 1
if p > len(t$) - (len(d$) - 1) then
scan_text = 0
exit function
end if
loop until mid$(t$, p, len(d$)) = d$
scan_text = p
end function
function scan_right(x1, y, i&, d~&) ' Starting position (noninclusive), image, detection color
x = x1
preserve& = _source
_source i&
w = _width(i&)
do
x = x + 1
if x > w then call scan_error(x, y, "right")
loop until point(x, y) = d~& or x > w
scan_right = x
_source preserve&
end function
function scan_down(x, y1, i&, d~&)
y = y1
preserve& = _source
_source i&
h = _height(i&)
do
y = y + 1
if y > h then call scan_error(x, y, "down")
loop until point(x, y) = d~& or y > h
scan_down = y
_source preserve&
end function
sub scan_error(x, y, t$)
t1$ = "Moved " + t$ + " beyond image at" + str$(x) + "," + str$(y)
call glass_fonts(t1$, f_font, fullscreen, 0, 0, left_align)
call press_any_key
end sub
' -----------------------------
' ========== Display ==========
' -----------------------------
sub draw_background
_putimage(0, 0)-(screenw, screenh), background, fullscreen, (0, 0)-(screenw, screenh)
end sub
sub draw_stage
call clear_image(fullscreen, hue(hue_black))
call draw_background
w = block_size
' Draw blocks
for y = 1 to boardh
for x = 1 to boardw
b = block(turn, x, y).spec
if b <> b_empty then
x1 = ((w + 1) * x) - camera.x
y1 = ((w + 1) * y) - camera.y
s = block_spec(b).sprite
call draw_sprite(s, x1, y1)
if block(turn, x, y).metal = magnet then call draw_sprite(sprite_ref(spr_magnetic), x1, y1) ' Magnetic overlay
end if
next x
next y
' Draw entities, ending with characters
for n = entity_count(turn) to 1 step -1
e = entity(turn, n).spec
x1 = ((w + 1) * entity(turn, n).pos.x) - camera.x
y1 = ((w + 1) * entity(turn, n).pos.y) - camera.y
s = entity_spec(e).sprite
if entity_spec(e).flip.x = true and entity(turn, n).flip.x = 1 then m = 1 else m = 0
if entity_spec(e).flip.y = true and entity(turn, n).flip.y = -1 then
if entity_spec(e).flip.x = true then m = m + 2 else m = m + 1
end if
s = s + m
if entity(turn, n).moving = true then
x1 = x1 + ((w + 1) * move_offset.x)
y1 = y1 + ((w + 1) * move_offset.y)
end if
call draw_sprite(s, x1, y1)
if entity(turn, n).metal = magnet then call draw_sprite(sprite_ref(spr_magnetic), x1, y1) ' Magnetic overlay
if entity(turn, n).flag = summoned then call draw_sprite(sprite_ref(spr_summoned), x1, y1) ' Summoned crate overlay
if entity(turn, n).flag = shield_up then call draw_sprite(sprite_ref(spr_shield), x1, y1) ' Shield overlay
if n = control then call draw_sprite(sprite_ref(spr_control), x1, y1) ' Yellow player control arrow
next n
_putimage(0, 0)-(800, 65), fade_image, fullscreen, (0, 0)-(800, 65)
s& = fullscreen
a = left_align
f = f_font
fg = f_font_gold
h = font(f, 0).h
y1 = 0: x1 = 0
y2 = h: x2 = 100
y3 = h * 2
d = keyboard
locate 1, 1
if control = e_warrior then
tk1$ = key_name$(keybind(armor_key, d), d): ta1$ = "Magnetic Coat"
tk2$ = key_name$(keybind(shield_key, d), d): ta2$ = "Shield"
elseif control = e_archer then
tk1$ = key_name$(keybind(jump_key, d), d): ta1$ = "Jump"
tk2$ = key_name$(keybind(arrow_key, d), d): ta2$ = "Telekinesis"
elseif control = e_wizard then
tk1$ = key_name$(keybind(alchemy_key, d), d): ta1$ = "Transform Block"
tk2$ = key_name$(keybind(block_key, d), d): ta2$ = "Summon Block"
end if
call glass_fonts("--- " + entity_spec(control).name + " ---", fg, s&, x1, y1, a)
call glass_fonts("[" + tk1$ + "]", fg, s&, x1, y2, a): call glass_fonts(ta1$, f, s&, x2, y2, a)
call glass_fonts("[" + tk2$ + "]", fg, s&, x1, y3, a): call glass_fonts(ta2$, f, s&, x2, y3, a)
x1 = 250: x2 = 350
call glass_fonts("[" + key_name$(keybind(action_key, d), d) + "]", fg, s&, x1, y1, a): call glass_fonts("Action", f, s&, x2, y1, a)
call glass_fonts("[" + key_name$(keybind(gravity_key, d), d) + "]", fg, s&, x1, y2, a): call glass_fonts("Reverse Gravity", f, s&, x2, y2, a)
call glass_fonts("[" + key_name$(keybind(switch_key, d), d) + "]", fg, s&, x1, y3, a): call glass_fonts("Switch Character", f, s&, x2, y3, a)
x1 = 500: x2 = 600
call glass_fonts("--- STAGE" + str$(current_stage) + ": " + stage_name$(current_stage) + " ---", fg, s&, x1, y1, a)
call glass_fonts("[" + key_name$(keybind(rewind_key, d), d) + "]", fg, s&, x1, y2, a): call glass_fonts("Rewind", f, s&, x2, y2, a)
call glass_fonts("[" + key_name$(keybind(restart_key, d), d) + "]", fg, s&, x1, y3, a): call glass_fonts("Restart", f, s&, x2, y3, a)
locate 5
'print entity(turn, control).pos.x; entity(turn, control).pos.y
end sub
sub draw_sprite(s, x, y)
w = block_size
_putimage(x, y)-step(w, w), block_image, fullscreen, (sprite(s).pos.x, sprite(s).pos.y)-step(w, w)
end sub
sub glass_fonts(t$, f, p&, x1, y1, d)
' Text, font, destination image surface, position, alignment
x = x1: y = y1
if d <> left_align then
' Adjust starting point based on line width, for center or right align
w = text_width(t$, f)
if d = center_align then w = inthalf(w)
x = plus_limit(x, -w, 0)
end if
h = font(f, 0).h
for n = 1 to len(t$)
c = asc(mid$(t$, n, 1))
w = font(f, c).w
_putimage(x, y)-step(w, h), font(f, 0).image, p&, (font(f, c).pos.x, font(f, c).pos.y)-step(w, h)
x = x + w + 1
next n
end sub
sub round_rect(px, py, sx, sy, d&, h&, bevel)
preserve& = _dest
_dest d&
for n1 = 0 to bevel
n2 = bevel - n1
if n1 <> bevel then
line(px + n2, py + n1 )-(px + sx - n2, py + n1 ), h&
line(px + n2, py + sy - n1)-(px + sx - n2, py + sy - n1), h&
else
line(px + n2, py + n1)-(px + sx - n2, py + sy - n1), h&, bf
end if
next n1
_dest preserve&
end sub
function text_tag_replace$(t1$, f)
' Flag parameter invokes specific tag set, false to use all
t$ = t1$
if f = false or f = text_tag_keybind then
' Look for every "#kb01" etc in string and replace with key_name$
n = scan_text(0, lcase$(t$), "#kb")
do while n <> 0
i = val(mid$(t$, n + 3, 2))
t$ = text_replace$(t$, key_name$(keybind(i, keyboard), keyboard), n, 5)
n = scan_text(n, lcase$(t$), "#kb")
loop
end if
text_tag_replace$ = t$
end function
function text_replace$(t$, r$, p, l)
' p = position of section to replace, l = its length
text_replace$ = left$(t$, p - 1) + r$ + right$(t$, len(t$) - p - (l - 1))
end function
sub capture_screen
call clear_image(store_screen, hue(hue_black))
_putimage(0, 0)-(screenw, screenh), fullscreen, store_screen, (0, 0)-(screenw, screenh)
end sub
sub restore_screen
call clear_image(fullscreen, hue(hue_black))
_putimage(0, 0)-(screenw, screenh), store_screen, fullscreen, (0, 0)-(screenw, screenh)
end sub
sub clear_image(d&, h~&)
preserve& = _dest
_dest d&
cls , h~&
_dest preserve&
end sub
sub overlay(d&, h~&)
preserve& = _dest: preserve2& = _source
_dest d&: _source d&
line(0, 0)-(_width, _height), h~&, bf
_clearcolor point(_width - 1, _height - 1), d&
_dest preserve&: _source preserve2&
end sub
sub play_sound(s)
if option_sound = false then exit sub
' Count valid sounds at this index and select one randomly
c = 1
do until sfx(s, c + 1) = false
c = c + 1
loop
r = int(rnd * c) + 1
if sfx(s, r) <> false then _sndplay sfx(s, r)
end sub
sub play_menu_move
call play_sound(sfx_menu_move)
end sub
sub play_menu_confirm
call play_sound(sfx_menu_confirm)
end sub
' ----------------------------------
' ========== Initial data ==========
' ----------------------------------
sub set_key_data
key_name$(false, keyboard) = "NOT SET"
key_name$(false, gamepad) = "NOT SET"
d = keyboard
for n = 1 to 512
key_name$(n, d) = "UNKNOWN"
next n
key_name$(2, d) = "ESC"
key_name$(60, d) = "F1"
key_name$(61, d) = "F2"
key_name$(62, d) = "F3"
key_name$(63, d) = "F4"
key_name$(64, d) = "F5"
key_name$(65, d) = "F6"
key_name$(66, d) = "F7"
key_name$(67, d) = "F8"
key_name$(68, d) = "F9"
key_name$(88, d) = "F11"
key_name$(89, d) = "F12"
key_name$(42, d) = "~"
key_name$(3, d) = "1"
key_name$(4, d) = "2"
key_name$(5, d) = "3"
key_name$(6, d) = "4"
key_name$(7, d) = "5"
key_name$(8, d) = "6"
key_name$(9, d) = "7"
key_name$(10, d) = "8"
key_name$(11, d) = "9"
key_name$(12, d) = "0"
key_name$(13, d) = "-"
key_name$(14, d) = "="
key_name$(15, d) = "BKSP"
key_name$(16, d) = "TAB"
key_name$(17, d) = "Q"
key_name$(18, d) = "W"
key_name$(19, d) = "E"
key_name$(20, d) = "R"
key_name$(21, d) = "T"
key_name$(22, d) = "Y"
key_name$(23, d) = "U"
key_name$(24, d) = "I"
key_name$(25, d) = "O"
key_name$(26, d) = "P"
key_name$(27, d) = "["
key_name$(28, d) = "]"
key_name$(44, d) = "\"
key_name$(31, d) = "A"
key_name$(32, d) = "S"
key_name$(33, d) = "D"
key_name$(34, d) = "F"
key_name$(35, d) = "G"
key_name$(36, d) = "H"
key_name$(37, d) = "J"
key_name$(38, d) = "K"
key_name$(39, d) = "L"
key_name$(40, d) = ";"
key_name$(41, d) = "'"
key_name$(29, d) = "ENTER"
key_name$(43, d) = "L SHIFT"
key_name$(45, d) = "Z"
key_name$(46, d) = "X"
key_name$(47, d) = "C"
key_name$(48, d) = "V"
key_name$(49, d) = "B"
key_name$(50, d) = "N"
key_name$(51, d) = "M"
key_name$(52, d) = ","
key_name$(53, d) = "."
key_name$(54, d) = "/"
key_name$(55, d) = "R SHIFT"
key_name$(30, d) = "L CTRL"
key_name$(58, d) = "SPACE"
key_name$(286, d) = "R CTRL"
key_name$(339, d) = "INS"
key_name$(340, d) = "DEL"
key_name$(328, d) = "HOME"
key_name$(336, d) = "END"
key_name$(330, d) = "PG UP"
key_name$(338, d) = "PG DN"
key_name$(329, d) = "UP"
key_name$(337, d) = "DOWN"
key_name$(332, d) = "LEFT"
key_name$(334, d) = "RIGHT"
key_name$(310, d) = "NUM /"
key_name$(56, d) = "NUM *"
key_name$(75, d) = "NUM -"
key_name$(79, d) = "NUM +"
key_name$(285, d) = "NUM ENTER"
key_name$(72, d) = "NUM 7"
key_name$(73, d) = "NUM 8"
key_name$(74, d) = "NUM 9"
key_name$(76, d) = "NUM 4"
key_name$(77, d) = "NUM 5"
key_name$(78, d) = "NUM 6"
key_name$(80, d) = "NUM 1"
key_name$(81, d) = "NUM 2"
key_name$(82, d) = "NUM 3"
key_name$(83, d) = "NUM 0"
key_name$(84, d) = "NUM ."
' Troublesome keyboard codes:
' 71 - Scroll Lock
' 70 - Pause
' 59 - Caps Lock
' 348 - Windows Left
' 349 - Windows Right?
' 350 - Menu
' 326 - Num Lock
d = gamepad
for n = 1 to 20
key_name$(n, d) = "BUTTON" + str$(n)
next n
for n = 1 to 8
key_name$(n + 100, d) = "AXIS" + str$(n) + "-"
key_name$(n + 200, d) = "AXIS" + str$(n) + "+"
next n
'const armor_key = 1
'const shield_key = 2
'const jump_key = 3
'const arrow_key = 4
'const alchemy_key = 5
'const block_key = 6
'const action_key = 7
'const gravity_key = 8
'const up_key = 9
'const down_key = 10
'const left_key = 11
'const right_key = 12
'const switch_key = 13
'const rewind_key = 14
'const restart_key = 15
'const ok_key = 16
'const cancel_key = 17
'const enter_key = 18
'const esc_key = 19
keybind_name$(armor_key) = "MAGNETIC COAT"
keybind_name$(shield_key) = "SHIELD"
keybind_name$(jump_key) = "JUMP"
keybind_name$(arrow_key) = "TELEKINESIS"
keybind_name$(alchemy_key) = "ALCHEMY"
keybind_name$(block_key) = "SUMMON BLOCK"
keybind_name$(action_key) = "ACTION"
keybind_name$(gravity_key) = "REVERSE GRAVITY"
keybind_name$(up_key) = "UP"
keybind_name$(down_key) = "DOWN"
keybind_name$(left_key) = "LEFT"
keybind_name$(right_key) = "RIGHT"
keybind_name$(switch_key) = "SWITCH CHARACTER"
keybind_name$(rewind_key) = "REWIND"
keybind_name$(restart_key) = "RESTART STAGE"
keybind_name$(ok_key) = "MENU OK"
keybind_name$(cancel_key) = "MENU CANCEL"
keybind_name$(enter_key) = "PAUSE/OK"
keybind_name$(esc_key) = "PAUSE/CANCEL"
d = keyboard
keybind_default(armor_key, d) = 47 ' c
keybind_default(shield_key, d) = 45 ' z
keybind_default(jump_key, d) = 47 ' c
keybind_default(arrow_key, d) = 45 ' z
keybind_default(alchemy_key, d) = 47 ' c
keybind_default(block_key, d) = 45 ' z
keybind_default(action_key, d) = 46 ' x
keybind_default(gravity_key, d) = 58 ' SPACE
keybind_default(up_key, d) = 329
keybind_default(down_key, d) = 337
keybind_default(left_key, d) = 332
keybind_default(right_key, d) = 334
keybind_default(switch_key, d) = 30 ' LCTRL
keybind_default(rewind_key, d) = 15 ' BKSP
keybind_default(restart_key, d) = 20 ' r
keybind_default(ok_key, d) = 46 ' x
keybind_default(cancel_key, d) = 47 ' c
keybind_default(enter_key, d) = 29
keybind_default(esc_key, d) = 2
d = gamepad
keybind_default(armor_key, d) = 2 ' B
keybind_default(shield_key, d) = 4 ' Y
keybind_default(jump_key, d) = 2 ' B
keybind_default(arrow_key, d) = 4 ' Y
keybind_default(alchemy_key, d) = 2 ' B
keybind_default(block_key, d) = 4 ' Y
keybind_default(action_key, d) = 1 ' A
keybind_default(gravity_key, d) = 3 ' X
keybind_default(up_key, d) = 102 ' stick up
keybind_default(down_key, d) = 202 ' stick down
keybind_default(left_key, d) = 101 ' stick left
keybind_default(right_key, d) = 201 ' stick right
keybind_default(switch_key, d) = 6 ' R
keybind_default(rewind_key, d) = 5 ' L
keybind_default(restart_key, d) = 7 ' Select
keybind_default(ok_key, d) = 1 ' A
keybind_default(cancel_key, d) = 2 ' B
keybind_default(enter_key, d) = false ' Enter and Esc are not found on gamepad
keybind_default(esc_key, d) = false
' OK and Cancel can overlap with any gameplay functions
b = ok_key
keybind_overlap(b, armor_key) = true: keybind_overlap(armor_key, b) = true
keybind_overlap(b, shield_key) = true: keybind_overlap(shield_key, b) = true
keybind_overlap(b, jump_key) = true: keybind_overlap(jump_key, b) = true
keybind_overlap(b, arrow_key) = true: keybind_overlap(arrow_key, b) = true
keybind_overlap(b, alchemy_key) = true: keybind_overlap(alchemy_key, b) = true
keybind_overlap(b, block_key) = true: keybind_overlap(block_key, b) = true
keybind_overlap(b, action_key) = true: keybind_overlap(action_key, b) = true
keybind_overlap(b, gravity_key) = true: keybind_overlap(gravity_key, b) = true
keybind_overlap(b, switch_key) = true: keybind_overlap(switch_key, b) = true
keybind_overlap(b, rewind_key) = true: keybind_overlap(rewind_key, b) = true
keybind_overlap(b, restart_key) = true: keybind_overlap(restart_key, b) = true
b = cancel_key
keybind_overlap(b, armor_key) = true: keybind_overlap(armor_key, b) = true
keybind_overlap(b, shield_key) = true: keybind_overlap(shield_key, b) = true
keybind_overlap(b, jump_key) = true: keybind_overlap(jump_key, b) = true
keybind_overlap(b, arrow_key) = true: keybind_overlap(arrow_key, b) = true
keybind_overlap(b, alchemy_key) = true: keybind_overlap(alchemy_key, b) = true
keybind_overlap(b, block_key) = true: keybind_overlap(block_key, b) = true
keybind_overlap(b, action_key) = true: keybind_overlap(action_key, b) = true
keybind_overlap(b, gravity_key) = true: keybind_overlap(gravity_key, b) = true
keybind_overlap(b, switch_key) = true: keybind_overlap(switch_key, b) = true
keybind_overlap(b, rewind_key) = true: keybind_overlap(rewind_key, b) = true
keybind_overlap(b, restart_key) = true: keybind_overlap(restart_key, b) = true
' Characters can overlap with each other in any way
keybind_overlap(armor_key, jump_key) = true: keybind_overlap(jump_key, armor_key) = true
keybind_overlap(armor_key, arrow_key) = true: keybind_overlap(arrow_key, armor_key) = true
keybind_overlap(shield_key, jump_key) = true: keybind_overlap(jump_key, shield_key) = true
keybind_overlap(shield_key, arrow_key) = true: keybind_overlap(arrow_key, shield_key) = true
keybind_overlap(jump_key, alchemy_key) = true: keybind_overlap(alchemy_key, jump_key) = true
keybind_overlap(jump_key, block_key) = true: keybind_overlap(block_key, jump_key) = true
keybind_overlap(arrow_key, alchemy_key) = true: keybind_overlap(alchemy_key, arrow_key) = true
keybind_overlap(arrow_key, block_key) = true: keybind_overlap(block_key, arrow_key) = true
keybind_overlap(alchemy_key, armor_key) = true: keybind_overlap(armor_key, alchemy_key) = true
keybind_overlap(alchemy_key, shield_key) = true: keybind_overlap(shield_key, alchemy_key) = true
keybind_overlap(block_key, armor_key) = true: keybind_overlap(armor_key, block_key) = true
keybind_overlap(block_key, shield_key) = true: keybind_overlap(shield_key, block_key) = true
call set_default_keybinds
end sub
sub set_sprite_ref
' ----- Set sprite references - must be in order found in image files -----
s = 1
sprite_ref(spr_warrior_d_l) = s: s = s + 1
sprite_ref(spr_warrior_d_r) = s: s = s + 1
sprite_ref(spr_warrior_u_l) = s: s = s + 1
sprite_ref(spr_warrior_u_r) = s: s = s + 1
sprite_ref(spr_archer_d_l) = s: s = s + 1
sprite_ref(spr_archer_d_r) = s: s = s + 1
sprite_ref(spr_archer_u_l) = s: s = s + 1
sprite_ref(spr_archer_u_r) = s: s = s + 1
sprite_ref(spr_wizard_d_l) = s: s = s + 1
sprite_ref(spr_wizard_d_r) = s: s = s + 1
sprite_ref(spr_wizard_u_l) = s: s = s + 1
sprite_ref(spr_wizard_u_r) = s: s = s + 1
sprite_ref(spr_grass) = s: s = s + 1
sprite_ref(spr_ground) = s: s = s + 1
sprite_ref(spr_ground_metal) = s: s = s + 1
sprite_ref(spr_crate) = s: s = s + 1
sprite_ref(spr_crate_metal) = s: s = s + 1
sprite_ref(spr_spikes) = s: s = s + 1
sprite_ref(spr_plate) = s: s = s + 1
sprite_ref(spr_lever_l) = s: s = s + 1
sprite_ref(spr_lever_r) = s: s = s + 1
sprite_ref(spr_door_shut) = s: s = s + 1
sprite_ref(spr_door_open) = s: s = s + 1
sprite_ref(spr_telepad) = s: s = s + 1
sprite_ref(spr_goal) = s: s = s + 1
sprite_ref(spr_magnetic) = s: s = s + 1
sprite_ref(spr_control) = s: s = s + 1
sprite_ref(spr_summoned) = s: s = s + 1
sprite_ref(spr_shield) = s: s = s + 1
sprite_ref(spr_psychic) = s: s = s + 1
' ----- Set sprites -----
entity_spec(e_warrior).sprite = sprite_ref(spr_warrior_d_l)
entity_spec(e_archer).sprite = sprite_ref(spr_archer_d_l)
entity_spec(e_wizard).sprite = sprite_ref(spr_wizard_d_l)
entity_spec(e_crate).sprite = sprite_ref(spr_crate)
entity_spec(e_crate_metal).sprite = sprite_ref(spr_crate_metal)
block_spec(b_grass).sprite = sprite_ref(spr_grass)
block_spec(b_ground).sprite = sprite_ref(spr_ground)
block_spec(b_ground_metal).sprite = sprite_ref(spr_ground_metal)
block_spec(b_spikes).sprite = sprite_ref(spr_spikes)
block_spec(b_plate).sprite = sprite_ref(spr_plate)
block_spec(b_lever_l).sprite = sprite_ref(spr_lever_l)
block_spec(b_lever_r).sprite = sprite_ref(spr_lever_r)
block_spec(b_door_shut).sprite = sprite_ref(spr_door_shut)
block_spec(b_door_open).sprite = sprite_ref(spr_door_open)
block_spec(b_telepad).sprite = sprite_ref(spr_telepad)
block_spec(b_goal).sprite = sprite_ref(spr_goal)
end sub
sub set_entity_spec_data
entity_spec(e_warrior).name = "Lancelot"
entity_spec(e_warrior).metal = true
entity_spec(e_warrior).flip.x = true
entity_spec(e_warrior).flip.y = true
entity_spec(e_archer).name = "Percival"
entity_spec(e_archer).metal = false
entity_spec(e_archer).flip.x = true
entity_spec(e_archer).flip.y = true
entity_spec(e_wizard).name = "Galahad"
entity_spec(e_wizard).metal = false
entity_spec(e_wizard).flip.x = true
entity_spec(e_wizard).flip.y = true
entity_spec(e_crate).metal = false
entity_spec(e_crate_metal).metal = true
end sub
sub set_block_spec_data
block_spec(b_empty).solid = false
block_spec(b_empty).metal = false
block_spec(b_grass).solid = true
block_spec(b_grass).metal = false
block_spec(b_ground).solid = true
block_spec(b_ground).metal = false
block_spec(b_ground_metal).solid = true
block_spec(b_ground_metal).metal = true
block_spec(b_spikes).solid = true
block_spec(b_spikes).metal = false
block_spec(b_plate).solid = false
block_spec(b_plate).metal = false
block_spec(b_lever_l).solid = false
block_spec(b_lever_l).metal = false
block_spec(b_lever_r).solid = false
block_spec(b_lever_r).metal = false
block_spec(b_door_shut).solid = true
block_spec(b_door_shut).metal = false
block_spec(b_door_open).solid = false
block_spec(b_door_open).metal = false
block_spec(b_telepad).solid = false
block_spec(b_telepad).metal = false
block_spec(b_goal).solid = false
block_spec(b_goal).metal = false
end sub
Now, notes about the code!
The core mechanics of gravity and magnetism created some really confusing interplay that took me several days to untangle to this point, and there are still a few minor things that don't work quite correctly, but you can solve the puzzles just fine. Due to all the wrangling of the core mechanics, I was only able to create four levels, and the levels aren't super inspired. They just show off the mechanics and leave it at that.
It's just shy of 3000 lines of code, although I imported some key systems from my main game project for this, namely the input handling and keybinding system (woo, gamepad support, if that's your bag!), the sprite sheet slicer, and my own custom font system. My code is extremely well-organized, so if you want to repurpose those systems, feel free, it shouldn't be hard. If you wave a cookie in my face, I might yank one out for you myself.
You can also create your own levels if you want, go to sub load_stage and you'll see walls of text strings where I did my level building. You can replace these levels with your own, or expand the level count if you like by changing total_stages in the header. Due to my hurry to complete the jam, some stuff like level names, character names, etc are in random places in the code - some in the header, some in the last couple subs. Down to the wire, I had to stop being so organized and start doing stuff quickly.
The logic of the core gravity and magnetism mechanics can be seen in sub move_entity. I decided to create a reflexive tree of nodes, where each node records its entity index and parent node. First I swept each node's neighbors to add more nodes, until I ran out of nodes to look at; then I iterated through removing disqualified nodes until no more got removed. For moving a character, this just starts with the character and immediately moves whatever was found to be connected; for gravity, since it has to check every entity in the stage, the move_entity routine simply marks the entities as moving, then when I've checked everything, every marked entity moves.
The game will only accept inputs when nothing is moving, and I put in a 3-frame slide so you can see things move and fall; this means it's easy to move too fast and have the game not take your inputs. But since it's turn-based, you can't be screwed over by this, and since the game has a rewind mechanic (up to 100 moves can be undone), I didn't have to exhaustively look for potential softlocks. Which are everywhere in this, the good old push-block-into-corner will get you stuck in lots of places.
I made a couple of the assets, but nearly everything is from opengameart and freesound. You are completely free to reuse whatever you like.
Let me know what you think!