initial commit
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
class Character
|
||||
SPEED = 2.5
|
||||
SIZE = [25, 30]
|
||||
|
||||
def initialize
|
||||
@world_x = 60.0
|
||||
@world_y = 60.0
|
||||
|
||||
# Load the full sheet as tiles
|
||||
sheet = Gosu::Image.load_tiles("assets/images/player.png", 20, 40, retro: true)
|
||||
|
||||
rows = [
|
||||
:idle_front,
|
||||
:idle_back,
|
||||
:idle_right,
|
||||
:idle_left,
|
||||
:walk_front,
|
||||
:walk_back,
|
||||
:walk_right,
|
||||
:walk_left
|
||||
]
|
||||
|
||||
@animations = {}
|
||||
|
||||
rows.each_with_index do |name, row|
|
||||
start_index = row * 6
|
||||
@animations[name] = sheet[start_index, 6] # slice 6 frames
|
||||
end
|
||||
|
||||
@current_animation = :idle_front
|
||||
@facing = :front
|
||||
end
|
||||
|
||||
def rect
|
||||
w, h = SIZE
|
||||
[@world_x - w / 2, @world_y - h / 2, w, h]
|
||||
end
|
||||
|
||||
def collides?(rect)
|
||||
return true if intersects?(self.rect, rect)
|
||||
false
|
||||
end
|
||||
|
||||
def update
|
||||
dx = 0.0
|
||||
dy = 0.0
|
||||
|
||||
dx -= 1 if Gosu.button_down?(Gosu::KB_A)
|
||||
dx += 1 if Gosu.button_down?(Gosu::KB_D)
|
||||
dy -= 1 if Gosu.button_down?(Gosu::KB_W)
|
||||
dy += 1 if Gosu.button_down?(Gosu::KB_S)
|
||||
|
||||
moving = dx != 0 || dy != 0
|
||||
|
||||
if moving
|
||||
if dy < 0
|
||||
@facing = :back
|
||||
elsif dy > 0
|
||||
@facing = :front
|
||||
elsif dx < 0
|
||||
@facing = :left
|
||||
elsif dx > 0
|
||||
@facing = :right
|
||||
end
|
||||
end
|
||||
|
||||
@current_animation = moving ? :"walk_#{@facing}" : :"idle_#{@facing}"
|
||||
|
||||
# Normalize vector if moving diagonally
|
||||
if dx != 0 && dy != 0
|
||||
length = Math.sqrt(dx**2 + dy**2)
|
||||
dx /= length
|
||||
dy /= length
|
||||
end
|
||||
|
||||
# Apply speed
|
||||
dx *= SPEED
|
||||
dy *= SPEED
|
||||
|
||||
# Horizontal
|
||||
@world_x += dx
|
||||
if $bus.get(:collides?, rect)&.include?(:wall)
|
||||
@world_x -= dx
|
||||
end
|
||||
|
||||
# Vertical
|
||||
@world_y += dy
|
||||
if $bus.get(:collides?, rect)&.include?(:wall)
|
||||
@world_y -= dy
|
||||
end
|
||||
|
||||
# Camera follow
|
||||
cam_x = @world_x - SCREEN_SIZE[0] / 2.0
|
||||
cam_y = @world_y - SCREEN_SIZE[1] / 2.0
|
||||
$bus.emit(:player_move, [cam_x, cam_y])
|
||||
end
|
||||
|
||||
def draw
|
||||
cx = SCREEN_SIZE[0] / 2.0 - SIZE[0] / 2.0
|
||||
cy = SCREEN_SIZE[1] / 2.0 - SIZE[1] / 2.0
|
||||
|
||||
sprite_x = cx + SIZE[0] / 2.0 - 20 * 1.8 / 2
|
||||
sprite_y = cy + SIZE[1] / 2.0 - 50 * 1.8 / 2
|
||||
|
||||
elapsed = Gosu.milliseconds / 1000.0
|
||||
frame_index = ((elapsed * 7).floor) % 6
|
||||
@animations[@current_animation][frame_index].draw(sprite_x, sprite_y, sprite_y, 1.8, 1.8)
|
||||
|
||||
return unless DEBUG
|
||||
|
||||
Gosu.draw_rect(cx, cy, SIZE[0], SIZE[1], Gosu::Color.new(0x8000FF00), cy)
|
||||
end
|
||||
end
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
require_relative "../utils/maze"
|
||||
|
||||
class Maze
|
||||
def initialize
|
||||
@maze = MazeData.new(80, 80)
|
||||
@spritesheet = Gosu::Image.load_tiles("assets/images/walls.png", 20, 40, retro: true)
|
||||
puts @spritesheet.size
|
||||
end
|
||||
|
||||
def collides?(rect)
|
||||
x1 = ((rect[0] - 10) / 60).floor
|
||||
y1 = ((rect[1] - 10) / 60).floor
|
||||
x2 = ((rect[0] + rect[2] + 10) / 60).floor
|
||||
y2 = ((rect[1] + rect[3] + 10) / 60).floor
|
||||
|
||||
(x1..x2).each do |x|
|
||||
(y1..y2).each do |y|
|
||||
shapes = shapes_for(@maze.wall_type(x, y), x * 60, y * 60)
|
||||
shapes.each do |shape|
|
||||
return true if intersects?(shape, rect)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def shapes_for(mask, tx, ty)
|
||||
return [] if mask == 0
|
||||
|
||||
half = 30
|
||||
band = 10
|
||||
|
||||
shapes = []
|
||||
|
||||
case mask & (W | E)
|
||||
when W | E
|
||||
shapes << [tx, ty + 30 - band / 2, 60, band]
|
||||
when E
|
||||
if (mask & (N | S)) == 0
|
||||
shapes << [tx, ty + 30 - band / 2, 60, band]
|
||||
else
|
||||
shapes << [tx + half, ty + 30 - band / 2, half, band]
|
||||
end
|
||||
when W
|
||||
if (mask & (N | S)) == 0
|
||||
shapes << [tx, ty + 30 - band / 2, 60, band]
|
||||
else
|
||||
shapes << [tx, ty + 30 - band / 2, half, band]
|
||||
end
|
||||
end
|
||||
|
||||
band = 20
|
||||
|
||||
case mask & (N | S)
|
||||
when (N | S)
|
||||
shapes << [tx + 30 - band / 2, ty, band, 60]
|
||||
when S
|
||||
shapes << [tx + 30 - band / 2, ty + half, band, half]
|
||||
when N
|
||||
shapes << [tx + 30 - band / 2, ty, band, half]
|
||||
end
|
||||
|
||||
shapes
|
||||
end
|
||||
|
||||
def draw(camera)
|
||||
cam_x, cam_y = camera
|
||||
tile_size = 60
|
||||
scale = 3
|
||||
screen_width, screen_height = SCREEN_SIZE
|
||||
|
||||
# How many tiles fit on screen (+1 for partially visible tiles)
|
||||
tiles_x = (screen_width / tile_size) + 2
|
||||
tiles_y = (screen_height / tile_size) + 2
|
||||
|
||||
# Which tile to start drawing (top-left corner of camera)
|
||||
start_x = (cam_x / tile_size).floor - 1
|
||||
start_y = (cam_y / tile_size).floor - 1
|
||||
|
||||
(0...tiles_y).each do |j|
|
||||
(0...tiles_x).each do |i|
|
||||
gx = start_x + i
|
||||
gy = start_y + j
|
||||
|
||||
next if gx < 0 || gy < 0 || gx >= @maze.width || gy >= @maze.height
|
||||
|
||||
index = @maze.wall_type(gx, gy)
|
||||
next if index == 0
|
||||
|
||||
# Pixel position on screen
|
||||
screen_x = gx * tile_size - cam_x
|
||||
screen_y = gy * tile_size - cam_y
|
||||
|
||||
@spritesheet[index - 1].draw(screen_x, screen_y, screen_y, scale, scale)
|
||||
|
||||
next unless DEBUG
|
||||
|
||||
shapes = shapes_for(index, gx * tile_size, gy * tile_size)
|
||||
shapes.each do |shape|
|
||||
Gosu.draw_rect(shape[0] - cam_x, shape[1] - cam_y, shape[2], shape[3], 0x44ff0000, shape[1] - cam_y)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
require_relative 'state'
|
||||
require_relative 'maze'
|
||||
require_relative 'character'
|
||||
|
||||
require 'perlin'
|
||||
|
||||
class Game < Scene
|
||||
def initialize
|
||||
super
|
||||
@font = Gosu::Font.new(24)
|
||||
@noise = Perlin::Generator.new(rand(1000), 1.0, 1)
|
||||
@floor_image = Gosu::Image.new("assets/images/floor.png", retro: true)
|
||||
|
||||
@maze = Maze.new
|
||||
@character = Character.new
|
||||
|
||||
@camera = [0, 0]
|
||||
|
||||
$bus.on(:player_move) do |pos|
|
||||
@camera = pos
|
||||
end
|
||||
|
||||
$bus.on_retrievable(:collides?) do |rect|
|
||||
collisions = []
|
||||
collisions << :wall if @maze.collides?(rect)
|
||||
collisions << :character if @character.collides?(rect)
|
||||
next collisions
|
||||
end
|
||||
end
|
||||
|
||||
def draw
|
||||
draw_floor
|
||||
@maze.draw(@camera)
|
||||
@character.draw
|
||||
draw_fog(@camera)
|
||||
draw_debug! if DEBUG
|
||||
end
|
||||
|
||||
def draw_debug!
|
||||
now = Gosu.milliseconds
|
||||
@last_draw_time ||= now
|
||||
@draw_count ||= 0
|
||||
@fps ||= 0
|
||||
|
||||
@draw_count += 1
|
||||
|
||||
if now - @last_draw_time >= 1000
|
||||
@fps = @draw_count
|
||||
@draw_count = 0
|
||||
@last_draw_time = now
|
||||
end
|
||||
|
||||
@font.draw_text("FPS: #{@fps}", 5, 5, Float::INFINITY, 1, 1, Gosu::Color::YELLOW)
|
||||
end
|
||||
|
||||
def draw_fog(camera)
|
||||
cam_x, cam_y = camera
|
||||
cell = 10
|
||||
i_radius = 80.0
|
||||
o_radius = 360.0
|
||||
|
||||
# Player screen position
|
||||
px = SCREEN_SIZE[0] / 2.0
|
||||
py = SCREEN_SIZE[1] / 2.0
|
||||
|
||||
tiles_x = (SCREEN_SIZE[0] / cell) + 2
|
||||
tiles_y = (SCREEN_SIZE[1] / cell) + 2
|
||||
|
||||
tiles_y.times do |j|
|
||||
tiles_x.times do |i|
|
||||
sx = i * cell
|
||||
sy = j * cell
|
||||
|
||||
|
||||
dist = Math.sqrt((sx - px)**2 + (sy - py)**2)
|
||||
|
||||
if dist < i_radius
|
||||
next # fully clear
|
||||
elsif dist > o_radius
|
||||
alpha = 255
|
||||
else
|
||||
t = (dist - i_radius) / (o_radius - i_radius) # 0..1
|
||||
|
||||
world_x = (sx + cam_x) * 0.005
|
||||
world_y = (sy + cam_y) * 0.005
|
||||
|
||||
# Sample noise at world position so it scrolls with camera
|
||||
noise = (@noise[world_x + Gosu.milliseconds / 5000.0, world_y + Gosu.milliseconds / 6000.0] + 1.0) / 2.0
|
||||
alpha = (t * (1.0 + noise) * 255).to_i.clamp(0, 255)
|
||||
end
|
||||
|
||||
Gosu.draw_rect(sx, sy, cell, cell, Gosu::Color.new(alpha, 0, 0, 0), 10000 + sy)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def draw_floor
|
||||
cam_x, cam_y = @camera
|
||||
tile_size = 60
|
||||
|
||||
offset_x = cam_x % tile_size
|
||||
offset_y = cam_y % tile_size
|
||||
|
||||
tiles_x = (SCREEN_SIZE[0] / tile_size) + 2
|
||||
tiles_y = (SCREEN_SIZE[1] / tile_size) + 2
|
||||
|
||||
(0...tiles_y).each do |j|
|
||||
(0...tiles_x).each do |i|
|
||||
sx = i * tile_size - offset_x
|
||||
sy = j * tile_size - offset_y
|
||||
@floor_image.draw(sx, sy, -1000, 3, 3)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
@character.update
|
||||
end
|
||||
|
||||
def button_down(id, _pos)
|
||||
return $bus.emit(:change_scene, Menu.new) if id == Gosu::KB_ESCAPE
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,7 @@
|
||||
class State
|
||||
# This class holds state information
|
||||
# for example the player inventory / items
|
||||
# also their health, stealth, and memory bars
|
||||
# it can handle mouse input for inventory management and item usage
|
||||
# it can also handle keyboard input for quick item usage
|
||||
end
|
||||
Reference in New Issue
Block a user