Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Round Table Flip by johannhowitzer
#1
Game made for the 2021 QB64 Game Jam Contest: https://itch.io/jam/qb64-game-jam/rate/918922

Quote: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!

   


.zip   Round Table Flip.zip (Size: 2.3 MB / Downloads: 31)

Code: (Select All)
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
Reply




Users browsing this thread: 2 Guest(s)