|
|
@@ -0,0 +1,529 @@ |
|
|
# Copyright 2021 Scratchwork Development LLC. All rights reserved. |
|
|
|
|
|
PI = 3.1415926 |
|
|
|
|
|
class Game |
|
|
attr_gtk |
|
|
|
|
|
def tick |
|
|
defaults |
|
|
render |
|
|
input |
|
|
calc |
|
|
end |
|
|
|
|
|
def defaults |
|
|
defaults_fiddle |
|
|
defaults_game |
|
|
end |
|
|
|
|
|
def defaults_fiddle |
|
|
state.sprite.width = 19 * 1.5 |
|
|
state.sprite.height = 10 * 1.5 |
|
|
state.momentum_toward_normal = 1.04 |
|
|
state.momentum_away_from_normal = 0.96 |
|
|
state.momentum_decay_out_of_drift = 0.98 |
|
|
state.momentum_decay_in_drift = 0.99 |
|
|
state.drift_minimum = 0.1 |
|
|
state.drift_maximum = 0.5 |
|
|
state.steering_wheel_range = (PI.fdiv 4).round 4 |
|
|
state.steering_wheel_delta_in_drift = 3300.0 |
|
|
state.steering_wheel_delta_out_of_drift = 4800.0 |
|
|
state.steering_wheel_delta_drifting = (PI.fdiv state.steering_wheel_delta_in_drift).round 4 |
|
|
state.steering_wheel_delta_not_drifting = (PI.fdiv state.steering_wheel_delta_out_of_drift).round 4 |
|
|
state.steering_wheel_reset_perc = 0.5 |
|
|
state.camera.scale = 1.5 |
|
|
end |
|
|
|
|
|
def defaults_game |
|
|
return if state.tick_count != 0 |
|
|
load_map! |
|
|
reset_game! |
|
|
end |
|
|
|
|
|
def render |
|
|
outputs.background_color = [0, 0, 0] |
|
|
render_won |
|
|
render_game |
|
|
render_instructions |
|
|
end |
|
|
|
|
|
def render_won |
|
|
return if !state.won |
|
|
|
|
|
outputs.labels << { x: 640, y: 380, text: "you win!", size_enum: 5, alignment_enum: 1, r: 255, g: 255, b: 255 } |
|
|
outputs.labels << { x: 640, y: 340, text: "#{'%.2f' % (state.clock.fdiv 60)}s", size_enum: 5, r: 255, g: 255, b: 255, alignment_enum: 1 } |
|
|
outputs.labels << { x: 640, y: 300, text: "(press 'r' to go again)", size_enum: 5, r: 255, g: 255, b: 255, alignment_enum: 1 } |
|
|
end |
|
|
|
|
|
def render_god_mode |
|
|
return if state.won |
|
|
return if state.god_mode != :enabled |
|
|
|
|
|
outputs[:scene].borders << { x: inputs.mouse.x - 30, y: inputs.mouse.y - 30, w: 60, h: 60, g: 255 } |
|
|
outputs[:scene].borders << state.track_rects.map { |t| relative_to_car t.merge(g: 255) } |
|
|
outputs[:scene].borders << (relative_to_car car_collision_rect) |
|
|
end |
|
|
|
|
|
def render_game |
|
|
return if state.won |
|
|
|
|
|
outputs[:scene].w = 2560 |
|
|
outputs[:scene].h = 1440 |
|
|
outputs[:scene].background_color = [0, 0, 0, 0] |
|
|
outputs[:scene].primitives << sprites_map_viewport |
|
|
outputs[:scene].primitives << state.smoke.map { |smoke| relative_to_car smoke } |
|
|
outputs[:scene].primitives << sprites_car.merge(x: sprites_car.x + 640, y: sprites_car.y + 360) |
|
|
outputs[:scene].primitives << state.deaths.flat_map do |stones| |
|
|
stones.map { |stone| relative_to_car stone } |
|
|
end |
|
|
|
|
|
if state.god_mode == :enabled |
|
|
outputs.sprites << { x: 0, |
|
|
y: 0, |
|
|
w: 2560, |
|
|
h: 1440, |
|
|
path: :scene, blendmode_enum: 0 } |
|
|
else |
|
|
outputs.sprites << { x: 0 + sprites_scene_offset_x, |
|
|
y: 0 + sprites_scene_offset_y, |
|
|
w: 2560 * state.camera.scale, |
|
|
h: 1440 * state.camera.scale, |
|
|
path: :scene, blendmode_enum: 0 } |
|
|
end |
|
|
end |
|
|
|
|
|
def render_instructions |
|
|
outputs.labels << { x: 10, |
|
|
y: 30, |
|
|
text: "controls - turn: left/right arrow keys, drift: spacebar", |
|
|
size_enum: 1, |
|
|
alignment_enum: 0, |
|
|
r: 255, |
|
|
g: 255, |
|
|
b: 255 } |
|
|
end |
|
|
|
|
|
def input |
|
|
input_game |
|
|
input_god_mode |
|
|
end |
|
|
|
|
|
def input_game |
|
|
reset_game! if inputs.keyboard.key_down.r |
|
|
return if state.clock < 30 |
|
|
|
|
|
turn_magnitude = 1.0 |
|
|
|
|
|
if inputs.controller_one.left_analog_x_perc.abs > 0 |
|
|
turn_magnitude = inputs.controller_one.left_analog_x_perc * 2.0 |
|
|
if turn_magnitude > 1.0 |
|
|
turn_magnitude = 1.0 |
|
|
elsif turn_magnitude < -1.0 |
|
|
turn_magnitude = -1.0 |
|
|
end |
|
|
end |
|
|
|
|
|
drift_down = !!inputs.keyboard.space || |
|
|
!!inputs.controller_one.r1 || |
|
|
!!inputs.controller_one.r2 |
|
|
|
|
|
left_down = !!inputs.left || !!inputs.keyboard.j |
|
|
right_down = !!inputs.right || !!inputs.keyboard.k |
|
|
|
|
|
state.turn_magnitude = turn_magnitude |
|
|
|
|
|
if left_finger |
|
|
if left_finger.x < 320 |
|
|
outputs.borders << { x: 0, y: 0, w: 320, h: 720, r: 255, g: 255, b: 255, a: 128 } |
|
|
left_down = true |
|
|
right_down = false |
|
|
elsif left_finger.x > 320 |
|
|
outputs.borders << { x: 320, y: 0, w: 320, h: 720, r: 255, g: 255, b: 255, a: 128 } |
|
|
left_down = false |
|
|
right_down = true |
|
|
end |
|
|
end |
|
|
|
|
|
if right_finger |
|
|
outputs.borders << { x: 640, y: 0, w: 640, h: 720, r: 255, g: 255, b: 255, a: 128 } |
|
|
drift_down = true |
|
|
end |
|
|
|
|
|
if left_down |
|
|
state.steering = :left |
|
|
elsif right_down |
|
|
state.steering = :right |
|
|
else |
|
|
state.steering = :released |
|
|
end |
|
|
|
|
|
if drift_down |
|
|
state.drift_mode = :on |
|
|
else |
|
|
state.drift_mode = :off |
|
|
end |
|
|
end |
|
|
|
|
|
def input_god_mode |
|
|
input_god_mode_enabled |
|
|
input_god_mode_toggle |
|
|
end |
|
|
|
|
|
def input_god_mode_enabled |
|
|
return if state.god_mode != :enabled |
|
|
input_god_mode_enabled_mouse |
|
|
input_god_mode_enabled_keyboard |
|
|
end |
|
|
|
|
|
def input_god_mode_enabled_mouse |
|
|
return if !inputs.mouse.click |
|
|
|
|
|
if inputs.keyboard.x |
|
|
to_delete = state.collision_points.find do |r| |
|
|
inputs.mouse.inside_rect? (relative_to_car x: r.x - 30, y: r.y - 30, w: 60, h: 60) |
|
|
end |
|
|
|
|
|
if to_delete |
|
|
state.collision_points.reject! { |p| p.x == to_delete.x && p.y == to_delete.y } |
|
|
state.track_rects = state.collision_points.map { |p| { x: p.x - 30, y: p.y - 30, w: 60, h: 60 } } |
|
|
save_map! |
|
|
end |
|
|
else |
|
|
point = [inputs.mouse.x + state.x - 640, inputs.mouse.y + state.y - 360] |
|
|
state.collision_points << point |
|
|
state.track_rects = state.collision_points.map { |p| { x: p.x - 30, y: p.y - 30, w: 60, h: 60 } } |
|
|
state.x = point.x |
|
|
state.y = point.y |
|
|
save_map! |
|
|
end |
|
|
end |
|
|
|
|
|
def input_god_mode_enabled_keyboard |
|
|
load_map! if inputs.keyboard.key_down.e |
|
|
if inputs.keyboard.space |
|
|
state.angle_r += inputs.left_right * 0.01 |
|
|
else |
|
|
state.y += inputs.keyboard.up_down * 5 |
|
|
state.x += inputs.keyboard.left_right * 5 |
|
|
end |
|
|
end |
|
|
|
|
|
def input_god_mode_toggle |
|
|
return if !inputs.keyboard.key_down.g |
|
|
|
|
|
if state.god_mode == :disabled |
|
|
state.god_mode = :enabled |
|
|
else |
|
|
state.god_mode = :disabled |
|
|
end |
|
|
end |
|
|
|
|
|
def calc |
|
|
return if state.god_mode == :enabled |
|
|
return if state.won |
|
|
state.clock += 1 |
|
|
calc_camera |
|
|
calc_steering |
|
|
calc_physics |
|
|
calc_smoke |
|
|
calc_car |
|
|
calc_prism_stones |
|
|
calc_game_over |
|
|
end |
|
|
|
|
|
def calc_camera |
|
|
state.camera.target_center_x = -((state.angle_r.to_degrees + 90).vector_x * 340) |
|
|
state.camera.target_center_y = ((state.angle_r.to_degrees + 90).vector_y * 260) |
|
|
state.camera.center_x += (state.camera.target_center_x - state.camera.center_x) * 0.01 |
|
|
state.camera.center_y += (state.camera.target_center_y - state.camera.center_y) * 0.01 |
|
|
end |
|
|
|
|
|
def calc_steering |
|
|
case state.steering |
|
|
when :right |
|
|
if state.steer_angle_r > state.steering_wheel_range * -1.0 |
|
|
state.steer_angle_r = state.steer_angle_r - steering_wheel_delta * state.turn_magnitude.abs |
|
|
end |
|
|
when :left |
|
|
if state.steer_angle_r < state.steering_wheel_range |
|
|
state.steer_angle_r = state.steer_angle_r + steering_wheel_delta * state.turn_magnitude.abs |
|
|
end |
|
|
when :released |
|
|
if state.steer_angle_r < 0 |
|
|
state.steer_angle_r = state.steer_angle_r + steering_wheel_delta * state.steering_wheel_reset_perc |
|
|
elsif state.steer_angle_r > 0 |
|
|
state.steer_angle_r = state.steer_angle_r - steering_wheel_delta * state.steering_wheel_reset_perc |
|
|
end |
|
|
end |
|
|
|
|
|
state.steer_angle_r = state.steer_angle_r.round(4).to_f |
|
|
end |
|
|
|
|
|
def calc_physics |
|
|
if state.drift_mode == :on |
|
|
if state.steer_angle_r > 0 |
|
|
state.drift_percentage_right *= state.momentum_toward_normal |
|
|
state.drift_percentage_left *= state.momentum_away_from_normal |
|
|
elsif state.steer_angle_r < 0 |
|
|
state.drift_percentage_right *= state.momentum_away_from_normal |
|
|
state.drift_percentage_left *= state.momentum_toward_normal |
|
|
else |
|
|
state.drift_percentage_right *= state.momentum_decay_out_of_drift |
|
|
state.drift_percentage_left *= state.momentum_decay_out_of_drift |
|
|
end |
|
|
else |
|
|
state.drift_percentage_right *= state.momentum_decay_out_of_drift |
|
|
state.drift_percentage_left *= state.momentum_decay_out_of_drift |
|
|
end |
|
|
|
|
|
state.drift_percentage_right = state.drift_minimum if state.drift_mode == :on && state.drift_percentage_right < state.drift_minimum |
|
|
state.drift_percentage_left = state.drift_minimum if state.drift_mode == :on && state.drift_percentage_left < state.drift_minimum |
|
|
|
|
|
state.drift_percentage_right = state.drift_maximum if state.drift_percentage_right > state.drift_maximum |
|
|
state.drift_percentage_left = state.drift_maximum if state.drift_percentage_left > state.drift_maximum |
|
|
|
|
|
state.drift_percentage_right = state.drift_percentage_right.round(4) |
|
|
state.drift_percentage_left = state.drift_percentage_left.round(4) |
|
|
end |
|
|
|
|
|
def calc_smoke |
|
|
state.smoke.each do |p| |
|
|
p.w += 2 |
|
|
p.h += 2 |
|
|
p.x -= 1 |
|
|
p.y -= 1 |
|
|
p.x -= p.dx |
|
|
p.y -= p.dy |
|
|
p.a -= 2 |
|
|
end |
|
|
state.smoke.reject! { |p| p.a <= 0 } |
|
|
return if state.drift_mode == :off |
|
|
return if !state.tick_count.zmod?(5) |
|
|
state.smoke << sprites_smoke_new |
|
|
end |
|
|
|
|
|
def calc_car |
|
|
velocity_x = state.speed * Math.sin(state.angle_r + state.steer_angle_r) * (1.0 - state.drift_percentage_right - state.drift_percentage_left) |
|
|
velocity_y = state.speed * Math.cos(state.angle_r + state.steer_angle_r) * (1.0 - state.drift_percentage_right - state.drift_percentage_left) |
|
|
|
|
|
normal_x_right = state.speed * Math.sin((PI / 2.0 ) - state.angle_r) * state.drift_percentage_right |
|
|
normal_y_right = state.speed * Math.cos((PI / 2.0 ) - state.angle_r) * state.drift_percentage_right * -1.0 |
|
|
|
|
|
normal_x_left = state.speed * Math.sin((PI / 2.0 ) - state.angle_r) * state.drift_percentage_left * -1.0 |
|
|
normal_y_left = state.speed * Math.cos((PI / 2.0 ) - state.angle_r) * state.drift_percentage_left |
|
|
|
|
|
state.x += velocity_x + normal_x_right + normal_x_left |
|
|
state.y += velocity_y + normal_y_right + normal_y_left |
|
|
|
|
|
state.angle_r -= state.steer_angle_r |
|
|
end |
|
|
|
|
|
def calc_prism_stones |
|
|
state.deaths.each do |stones| |
|
|
stones.each do |stone| |
|
|
stone.w ||= 8 |
|
|
stone.h ||= 8 |
|
|
stone.x ||= stone.original_x |
|
|
stone.y ||= stone.original_y |
|
|
stone.path ||= "sprites/#{stone.type}-stone.png" |
|
|
stone.a = ((stone.t - stone.current_t).fdiv stone.t) * 255 |
|
|
stone.lifetime ||= 300 * 60 |
|
|
stone.lifetime -= 1 |
|
|
if !stone.still |
|
|
if stone.current_t == stone.t |
|
|
stone.x = stone.original_x |
|
|
stone.y = stone.original_y |
|
|
stone.current_t = 0 |
|
|
else |
|
|
stone.y += stone.dy |
|
|
stone.x += stone.dx |
|
|
stone.current_t += 1 |
|
|
end |
|
|
end |
|
|
end |
|
|
end |
|
|
end |
|
|
|
|
|
def calc_game_over |
|
|
if state.track_rects.length > 1 && (state.track_rects.last.intersect_rect? car_collision_rect) |
|
|
state.won = true |
|
|
elsif !state.track_rects.any? { |c| c.intersect_rect? car_collision_rect } |
|
|
state.deaths << new_prism_stones if state.x != 155 && state.y != 1258 |
|
|
reset_game! |
|
|
end |
|
|
end |
|
|
|
|
|
def reset_game! |
|
|
state.deaths ||= [] |
|
|
state.deaths.reject! { |d| d.any? { |s| s.lifetime && s.lifetime <= 0 } } |
|
|
state.god_mode = :disabled |
|
|
state.x = 155 |
|
|
state.y = 730 |
|
|
state.speed = 1 |
|
|
state.angle_r = 0 |
|
|
state.steering = :released |
|
|
state.steer_angle_r = 0.0 |
|
|
state.drift_percentage_right = 0.0 |
|
|
state.drift_percentage_left = 0.0 |
|
|
state.drift_mode = :off |
|
|
state.speed = 4.0 |
|
|
state.clock = 0 |
|
|
state.won = false |
|
|
state.drift_percentage_left = 0 |
|
|
state.drift_percentage_right = 0 |
|
|
state.drift_sound_debounce = 0 |
|
|
state.car_after_images = [] |
|
|
state.smoke = [] |
|
|
state.camera.target_center_x = 0 |
|
|
state.camera.target_center_y = 0 |
|
|
state.camera.center_x = 0 |
|
|
state.camera.center_y = 0 |
|
|
end |
|
|
|
|
|
def steering_wheel_delta |
|
|
return state.steering_wheel_delta_drifting if state.drift_mode == :on |
|
|
return state.steering_wheel_delta_not_drifting |
|
|
end |
|
|
|
|
|
def sprites_car |
|
|
{ |
|
|
x: -state.sprite.width.half, |
|
|
y: -state.sprite.height.half, |
|
|
w: state.sprite.width, |
|
|
h: state.sprite.height, |
|
|
path: 'sprites/86.png', |
|
|
angle: 90 + (state.angle_r.to_degrees * -1), |
|
|
rotation_anchor_x: 0.7, |
|
|
rotation_anchor_y: 0.5, |
|
|
} |
|
|
end |
|
|
|
|
|
def sprites_car_after_image |
|
|
sprites_car.merge(x: state.x - state.sprite.width.half, |
|
|
y: state.y - state.sprite.height.half, |
|
|
angle: 90 + (state.angle_r.to_degrees * -1), |
|
|
a: 200) |
|
|
end |
|
|
|
|
|
def sprites_smoke_new |
|
|
angle = 90 + (state.angle_r.to_degrees * -1) |
|
|
sprites_car.merge x: state.x - state.sprite.width.half, |
|
|
y: state.y - state.sprite.height.half, |
|
|
dx: angle.vector_x * 0.5, |
|
|
dy: angle.vector_y * 0.5, |
|
|
angle: angle, |
|
|
a: 255, |
|
|
path: ["sprites/smoke-1.png", "sprites/smoke-2.png", "sprites/smoke-3.png"].sample |
|
|
end |
|
|
|
|
|
def car_collision_rect |
|
|
{ |
|
|
x: state.x - 6, |
|
|
y: state.y - 6, |
|
|
w: 12, |
|
|
h: 12, |
|
|
r: 255 |
|
|
} |
|
|
end |
|
|
|
|
|
def sprites_map |
|
|
{ |
|
|
x: 0, |
|
|
y: 0, |
|
|
w: 6400, |
|
|
h: 6400, |
|
|
path: 'sprites/map.png' |
|
|
} |
|
|
end |
|
|
|
|
|
def load_map! |
|
|
state.collision_points = (gtk.deserialize_state 'data/map.txt').collision_points if gtk.read_file 'data/map.txt' |
|
|
state.track_rects = state.collision_points.map { |p| { x: p.x - 30, y: p.y - 30, w: 60, h: 60 } } |
|
|
end |
|
|
|
|
|
def save_map! |
|
|
gtk.serialize_state 'data/map.txt', state |
|
|
end |
|
|
|
|
|
def relative_to_car point |
|
|
return nil if !point.x || !point.y |
|
|
point.merge x: point.x - state.x + 640, |
|
|
y: point.y - state.y + 360 |
|
|
end |
|
|
|
|
|
def new_prism_stones |
|
|
c = [:blue, :red, :yellow, :teal, :green, :white].sample |
|
|
[ |
|
|
{ original_x: state.x, original_y: state.y, w: 8, h: 8, |
|
|
type: c, current_t: 0, t: 120, dy: 0, dx: 0, still: true }, |
|
|
{ original_x: state.x - 1, original_y: state.y, |
|
|
type: c, current_t: 0, t: 120, dy: 0.25, dx: 0 }, |
|
|
{ original_x: state.x + 1, original_y: state.y, |
|
|
type: c, current_t: 0, t: 137, dy: 0.2, dx: 0 } |
|
|
] |
|
|
end |
|
|
|
|
|
def sprites_scene_offset_x |
|
|
-((1280 * state.camera.scale) - 1280).half - state.camera.center_x |
|
|
end |
|
|
|
|
|
def sprites_scene_offset_y |
|
|
-(( 720 * state.camera.scale) - 720).half - state.camera.center_y |
|
|
end |
|
|
|
|
|
def sprites_map_viewport |
|
|
x = 0 |
|
|
y = 0 |
|
|
source_x = state.x - 640 |
|
|
source_y = state.y - 360 |
|
|
|
|
|
if state.x < 640 |
|
|
source_x = 0 |
|
|
x = 640 - state.x |
|
|
end |
|
|
|
|
|
if state.y < 720 |
|
|
source_y = 0 |
|
|
y = 360 - state.y |
|
|
end |
|
|
|
|
|
{ |
|
|
x: x, |
|
|
y: y, |
|
|
w: 2560, |
|
|
h: 1440, |
|
|
source_x: source_x, |
|
|
source_y: source_y, |
|
|
source_w: 2560, |
|
|
source_h: 1440, |
|
|
path: 'sprites/map.png' |
|
|
} |
|
|
end |
|
|
|
|
|
def left_finger |
|
|
if inputs.finger_one && inputs.finger_one.x < 640 |
|
|
return inputs.finger_one |
|
|
elsif inputs.finger_two && inputs.finger_two.x < 640 |
|
|
return inputs.finger_two |
|
|
else |
|
|
return nil |
|
|
end |
|
|
end |
|
|
|
|
|
def right_finger |
|
|
if inputs.finger_one && inputs.finger_one.x > 640 |
|
|
return inputs.finger_one |
|
|
elsif inputs.finger_two && inputs.finger_two.x > 640 |
|
|
return inputs.finger_two |
|
|
else |
|
|
return nil |
|
|
end |
|
|
end |
|
|
end |
|
|
|
|
|
def tick args |
|
|
$game ||= Game.new |
|
|
$game.args = args |
|
|
$game.tick |
|
|
end |