Author Topic: Round Table Flip - QB64 Game Jam project!  (Read 4545 times)

0 Members and 1 Guest are viewing this topic.

Offline johannhowitzer

  • Forum Regular
  • Posts: 118
    • View Profile
Round Table Flip - QB64 Game Jam project!
« on: February 13, 2021, 05:36:16 am »
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/entries


cover.png



Code: [Select]
const 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!
« Last Edit: February 13, 2021, 07:00:22 am by johannhowitzer »

Offline bplus

  • Global Moderator
  • Forum Resident
  • Posts: 8053
  • b = b + ...
    • View Profile
Re: Round Table Flip - QB64 Game Jam project!
« Reply #1 on: February 13, 2021, 10:42:40 am »
Well I'm glad someone is posting here wish I had time to check out looks marvelous!

Yeah this may have been thrown together in days but there is years of experienced coding in here, I think.

Offline bplus

  • Global Moderator
  • Forum Resident
  • Posts: 8053
  • b = b + ...
    • View Profile
Re: Round Table Flip - QB64 Game Jam project!
« Reply #2 on: February 14, 2021, 03:25:43 pm »
Hey I was checking out files and saw my mortal enemy ogg but to my surprise the oggs didn't kill my Windows Explorer and crash my system. What? there weren't enough? maybe but, I think something got fixed with the Explorer.

This is very professionally put together but not obvious to me how to play. I need to be slowly immersed into new worlds, I think they call it "getting sucked in." I am wandering around flipping gravity or magnetic shoes back and forth figure something to move the crate wander around back and forth up and the other up... I jump on the crate smack! into the ceiling LOL gameover.

Offline johannhowitzer

  • Forum Regular
  • Posts: 118
    • View Profile
Re: Round Table Flip - QB64 Game Jam project!
« Reply #3 on: February 15, 2021, 03:22:59 am »
It probably wasn't clear enough, but you can rewind your moves at any time, including when you die, like in Baba is You, or Braid.

I had to recode the core systems four times before they finally worked, and by then I only had four hours left to make levels and clean stuff up for submission.  So the levels were REALLY rushed.

In fact, here's a short list of stuff I wish I'd had time to do add or improve:

- More levels, with a smoother introduction to the mechanics and abilities
- More sound effects, apart from the switch and crush sounds, it feels really empty
- Basic animations for the characters to make them stand out better, they're tiny and difficult to see
- I had a teleport pad and a pressure plate, but didn't have time to code them in
- A stage select; currently the game just loads the furthest level you've seen when you hit Start
- A better indicator of magnetism, those little lightning bolts aren't very clear

Plenty of very deserved criticism all around, and every bit of it with me nodding my head in agreement.  I think I reached a little far for my first game jam, in retrospect.  Maybe I'll go back and fix some of this stuff and make a lot more levels, when I have time and energy.
« Last Edit: February 15, 2021, 03:25:13 am by johannhowitzer »

Offline Dav

  • Forum Resident
  • Posts: 792
    • View Profile
Re: Round Table Flip - QB64 Game Jam project!
« Reply #4 on: February 15, 2021, 09:55:18 am »
This is wonderful.  Really nice work!

- Dav

FellippeHeitor

  • Guest
Re: Round Table Flip - QB64 Game Jam project!
« Reply #5 on: February 18, 2021, 01:58:30 pm »
This is soooo impressive! You started this exclusively for the jam???

Offline Pete

  • Forum Resident
  • Posts: 2361
  • Cuz I sez so, varmint!
    • View Profile
Re: Round Table Flip - QB64 Game Jam project!
« Reply #6 on: February 18, 2021, 07:26:08 pm »
Now I know why I stay in SCREEN 0. Alright, I'm just grumpy because I had no idea when you stand upside down, on a block, and disengage the gravity effect, you die at the bottom. I mean it would have cheered me up a bit if it had actually squished the character, and a little sign on a stick popped up from underneath the block, that read, "Ouch!" but no, nothing , the music just stopped. Where's the fun in that? :D

Nicely organized and clever idea about using resources and different characters to navigate the maze. Initially I spent three days looking for the gym, on level one, so I could build up my jumping ability, before I switched characters, and discovered I could summon boxes. Damn that second lever.

So while I don't like games, this one was certainly worth downloading. The lateral scrolling reminded me of Mario Bros. I had that on an original Nintendo. I let my Son play it when he just turned two. He wins lot of game contests now. Honestly, I doubt he would have ever been interested in video games, except I noticed how hard he would laugh when people fell or got smacked in some way on America's Funniest Home videos. The same thing happened with old reruns of The Three Stooges. So, squishing mushrooms was a blast for him. He actually figured the rest of the game for himself, over time. If only our incredibly stupid educational systems could figure out what motivates minds to learn, we'd all be raising geniuses of various sorts. Oh well.

Pete
Want to learn how to write code on cave walls? https://www.tapatalk.com/groups/qbasic/qbasic-f1/

Offline johannhowitzer

  • Forum Regular
  • Posts: 118
    • View Profile
Re: Round Table Flip - QB64 Game Jam project!
« Reply #7 on: February 19, 2021, 03:17:45 am »
Quote
This is wonderful.  Really nice work!

- Dav

Thanks!

Quote
This is soooo impressive! You started this exclusively for the jam???

Yes.  First day I did brainstorming, decided on a concept, did basic gameplay design, and threw together a skeleton of the code.  I filled in some of the easier structures and imported the systems I mentioned from my earlier work.  I started with very simple placeholders for the terrain and objects.  Then it took me until the last day to get the mechanics right - I had to draw things out on paper and analyze the logic, then code move_entity... FOUR TIMES.  Nearly ended up missing the deadline due to that monster of a snag.  But that's where I learned the most.

On the last day, I had some hours left to fill in missing sounds, get better sprites, find a good music track, and finish off all the leftover pieces of code.

Quote
Now I know why I stay in SCREEN 0. Alright, I'm just grumpy because I had no idea when you stand upside down, on a block, and disengage the gravity effect, you die at the bottom. I mean it would have cheered me up a bit if it had actually squished the character, and a little sign on a stick popped up from underneath the block, that read, "Ouch!" but no, nothing , the music just stopped. Where's the fun in that? :D

Yeah, I really wanted a death sprite, but I ran out of time.

Quote
Nicely organized and clever idea about using resources and different characters to navigate the maze. Initially I spent three days looking for the gym, on level one, so I could build up my jumping ability, before I switched characters, and discovered I could summon boxes. Damn that second lever.

Hm, that's an oversight.  You're supposed to use Percival's telekinesis to use the lever while standing next to the door.  Not the only cheese I'm aware of, either - the first stage's last puzzle is entirely unnecessary since Percival can just jump past it.  In hindsight, the third level would have been a better first level, as it lets you mess around with each character in isolation at first.

And while trying to build more level ideas while bored at work, it dawned on me that Lancelot has no incentive to ever disable his shield.  So he either needs something unique to do with it turned off, or I need to make the shield always-on and give him some other ability.

Offline Pete

  • Forum Resident
  • Posts: 2361
  • Cuz I sez so, varmint!
    • View Profile
Re: Round Table Flip - QB64 Game Jam project!
« Reply #8 on: February 19, 2021, 04:18:28 pm »
And while trying to build more level ideas while bored at work, it dawned on me that Lancelot has no incentive to ever disable his shield.  So he either needs something unique to do with it turned off, or I need to make the shield always-on and give him some other ability.

Introducing Guinevere, are we?
Want to learn how to write code on cave walls? https://www.tapatalk.com/groups/qbasic/qbasic-f1/

Offline 191Brian

  • Newbie
  • Posts: 91
    • View Profile
    • My Itch page
Re: Round Table Flip - QB64 Game Jam project!
« Reply #9 on: February 19, 2021, 04:33:28 pm »
Very impressive work!
Brian ...

Offline johannhowitzer

  • Forum Regular
  • Posts: 118
    • View Profile
Re: Round Table Flip - QB64 Game Jam project!
« Reply #10 on: February 19, 2021, 08:14:05 pm »
Quote
Introducing Guinevere, are we?

At first it was Arthur (shield/magnets), Guinevere (jump/telekinesis) and Merlin (conjuring/alchemy).
« Last Edit: February 20, 2021, 02:31:10 am by johannhowitzer »

Offline bplus

  • Global Moderator
  • Forum Resident
  • Posts: 8053
  • b = b + ...
    • View Profile
Re: Round Table Flip - QB64 Game Jam project!
« Reply #11 on: February 20, 2021, 10:14:38 am »
Quote
Introducing Guinevere, are we?

Guinevere has an alter-ego, Evil Guin, who persuades Arthur to take off his suit so she can steal it, now we have a Game Opera! ;-))