road_to_rubykaigi 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.standard.yml +18 -0
- data/CHANGELOG.md +3 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +49 -0
- data/Rakefile +10 -0
- data/bin/road_to_rubykaigi +5 -0
- data/lib/road_to_rubykaigi/ansi.rb +22 -0
- data/lib/road_to_rubykaigi/fireworks.rb +63 -0
- data/lib/road_to_rubykaigi/game.rb +97 -0
- data/lib/road_to_rubykaigi/graphics/fireworks.rb +16 -0
- data/lib/road_to_rubykaigi/graphics/fireworks.txt +369 -0
- data/lib/road_to_rubykaigi/graphics/map.rb +11 -0
- data/lib/road_to_rubykaigi/graphics/map.txt +30 -0
- data/lib/road_to_rubykaigi/graphics/mask.rb +11 -0
- data/lib/road_to_rubykaigi/graphics/mask.txt +30 -0
- data/lib/road_to_rubykaigi/graphics/player.rb +81 -0
- data/lib/road_to_rubykaigi/manager/collision_manager.rb +133 -0
- data/lib/road_to_rubykaigi/manager/drawing_manager.rb +48 -0
- data/lib/road_to_rubykaigi/manager/game_manager.rb +55 -0
- data/lib/road_to_rubykaigi/manager/update_manager.rb +27 -0
- data/lib/road_to_rubykaigi/map.rb +120 -0
- data/lib/road_to_rubykaigi/opening_screen.rb +56 -0
- data/lib/road_to_rubykaigi/score_board.rb +17 -0
- data/lib/road_to_rubykaigi/sprite/attack.rb +72 -0
- data/lib/road_to_rubykaigi/sprite/bonus.rb +127 -0
- data/lib/road_to_rubykaigi/sprite/deadline.rb +57 -0
- data/lib/road_to_rubykaigi/sprite/effect.rb +88 -0
- data/lib/road_to_rubykaigi/sprite/enemy.rb +178 -0
- data/lib/road_to_rubykaigi/sprite/player.rb +178 -0
- data/lib/road_to_rubykaigi/sprite/sprite.rb +22 -0
- data/lib/road_to_rubykaigi/version.rb +5 -0
- data/lib/road_to_rubykaigi.rb +46 -0
- data/sig/road_to_rubykaigi.rbs +4 -0
- metadata +82 -0
@@ -0,0 +1,133 @@
|
|
1
|
+
module RoadToRubykaigi
|
2
|
+
module Manager
|
3
|
+
class CollisionManager
|
4
|
+
def process
|
5
|
+
event = [player_meet_enemy] # player must hit enemy before land
|
6
|
+
player_fall
|
7
|
+
player_land
|
8
|
+
|
9
|
+
event += [
|
10
|
+
player_meet_deadline,
|
11
|
+
player_meet_bonus,
|
12
|
+
attack_hit_bonus,
|
13
|
+
attack_hit_enemy,
|
14
|
+
]
|
15
|
+
if event.include?(:game_over)
|
16
|
+
:game_over
|
17
|
+
elsif event.include?(:bonus)
|
18
|
+
:bonus
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def initialize(background, foreground)
|
25
|
+
@map = background
|
26
|
+
@player, @deadline, @bonuses, @enemies, @attacks, @effects = foreground.layers
|
27
|
+
end
|
28
|
+
|
29
|
+
def player_fall
|
30
|
+
bounding_box = @player.bounding_box
|
31
|
+
foot_y = bounding_box[:y] + bounding_box[:height]
|
32
|
+
center_x = bounding_box[:x] + bounding_box[:width] / 2.0
|
33
|
+
if @map.passable_at?(center_x, foot_y + 1)
|
34
|
+
@player.fall
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def player_land
|
39
|
+
bounding_box = @player.bounding_box
|
40
|
+
foot_y = bounding_box[:y] + bounding_box[:height]
|
41
|
+
foot_y = foot_y.clamp(bounding_box[:height], RoadToRubykaigi::Sprite::Player::BASE_Y)
|
42
|
+
(bounding_box[:x]...(bounding_box[:x] + bounding_box[:width])).each do |col|
|
43
|
+
unless @map.passable_at?(col, foot_y)
|
44
|
+
break @player.land(foot_y)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# @returns [:game_over, Nil]
|
50
|
+
def player_meet_deadline
|
51
|
+
find_collision_item(@player, @deadline) && :game_over
|
52
|
+
end
|
53
|
+
|
54
|
+
def player_meet_bonus
|
55
|
+
if (collided_item = find_collision_item(@player, @bonuses))
|
56
|
+
@effects.heart(
|
57
|
+
@player.x + @player.width - 1,
|
58
|
+
@player.y,
|
59
|
+
)
|
60
|
+
@bonuses.delete(collided_item)
|
61
|
+
:bonus
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# @returns [:bonus, false]
|
66
|
+
def attack_hit_bonus
|
67
|
+
collided = !@attacks.dup.select do |attack|
|
68
|
+
if (collided_item = find_collision_item(attack, @bonuses))
|
69
|
+
@effects.heart(
|
70
|
+
@player.x + @player.width - 1,
|
71
|
+
@player.y,
|
72
|
+
)
|
73
|
+
@bonuses.delete(collided_item)
|
74
|
+
@attacks.delete(attack)
|
75
|
+
end
|
76
|
+
end.empty?
|
77
|
+
collided && :bonus
|
78
|
+
end
|
79
|
+
|
80
|
+
# @returns [:bonus, Nil]
|
81
|
+
def attack_hit_enemy
|
82
|
+
collided = !@attacks.dup.select do |attack|
|
83
|
+
if (collided_item = find_collision_item(attack, @enemies))
|
84
|
+
@effects.note(
|
85
|
+
@player.x + @player.width - 1,
|
86
|
+
@player.y,
|
87
|
+
)
|
88
|
+
@enemies.delete(collided_item)
|
89
|
+
@attacks.delete(attack)
|
90
|
+
end
|
91
|
+
end.empty?
|
92
|
+
collided && :bonus
|
93
|
+
end
|
94
|
+
|
95
|
+
# @returns [:bonus, Nil]
|
96
|
+
def player_meet_enemy
|
97
|
+
if (collided_item = find_collision_item(@player, @enemies))
|
98
|
+
if @player.stompable?
|
99
|
+
@effects.note(
|
100
|
+
@player.x + @player.width - 1,
|
101
|
+
@player.y,
|
102
|
+
)
|
103
|
+
@enemies.delete(collided_item)
|
104
|
+
@player.vy = @player.class::JUMP_INITIAL_VELOCITY
|
105
|
+
:bonus
|
106
|
+
else
|
107
|
+
@effects.lightning(
|
108
|
+
@player.x + @player.width - 1,
|
109
|
+
@player.y,
|
110
|
+
)
|
111
|
+
@enemies.delete(collided_item)
|
112
|
+
@player.stun
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def find_collision_item(entity, others)
|
118
|
+
others.find do |other|
|
119
|
+
collided?(entity.bounding_box, other.bounding_box)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def collided?(box1, box2)
|
124
|
+
!(
|
125
|
+
box1[:x] + box1[:width] <= box2[:x] ||
|
126
|
+
box1[:x] >= box2[:x] + box2[:width] ||
|
127
|
+
box1[:y] + box1[:height] <= box2[:y] ||
|
128
|
+
box1[:y] >= box2[:y] + box2[:height]
|
129
|
+
)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module RoadToRubykaigi
|
2
|
+
module Manager
|
3
|
+
class DrawingManager
|
4
|
+
MAP_X_START = 1
|
5
|
+
MAP_Y_START = 2
|
6
|
+
|
7
|
+
def draw(offset_x:)
|
8
|
+
buffer = Array.new(@viewport_height) { Array.new(@viewport_width) { "" } }
|
9
|
+
@layers.each do |layer|
|
10
|
+
merge_buffer(buffer, layer, offset_x: offset_x)
|
11
|
+
end
|
12
|
+
|
13
|
+
ANSI.home
|
14
|
+
ANSI.background_color
|
15
|
+
ANSI.default_text_color
|
16
|
+
print @score_board.render
|
17
|
+
@viewport_height.times do |row|
|
18
|
+
@viewport_width.times do |col|
|
19
|
+
unless buffer[row][col] == @preview_buffer[row][col]
|
20
|
+
print "\e[#{row+MAP_Y_START};#{col+MAP_X_START}H" + ANSI::BACKGROUND_COLOR + ANSI::DEFAULT_TEXT_COLOR + buffer[row][col]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
@preview_buffer = buffer.map(&:dup)
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def initialize(score_board, background, foreground, fireworks)
|
30
|
+
@viewport_width = Map::VIEWPORT_WIDTH
|
31
|
+
@viewport_height = Map::VIEWPORT_HEIGHT
|
32
|
+
@preview_buffer = Array.new(@viewport_height) { Array.new(@viewport_width) { "" } }
|
33
|
+
@score_board = score_board
|
34
|
+
@background = background
|
35
|
+
@foreground = foreground
|
36
|
+
@layers = [background, *foreground.layers, fireworks]
|
37
|
+
end
|
38
|
+
|
39
|
+
def merge_buffer(buffer, layer, offset_x:)
|
40
|
+
layer.build_buffer(offset_x: offset_x).each_with_index do |row, i|
|
41
|
+
row.each_with_index do |tile, j|
|
42
|
+
buffer[i][j] = tile unless tile == ""
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module RoadToRubykaigi
|
2
|
+
module Manager
|
3
|
+
class GameManager
|
4
|
+
GOAL_X = 650
|
5
|
+
STATE = {
|
6
|
+
playing: 0,
|
7
|
+
pause: 1,
|
8
|
+
game_over: 2,
|
9
|
+
ending: 3,
|
10
|
+
finished: 4,
|
11
|
+
}
|
12
|
+
attr_reader :fireworks
|
13
|
+
|
14
|
+
def update
|
15
|
+
@deadline.activate(player_x: @player.x)
|
16
|
+
@enemies.activate if player_moved?
|
17
|
+
if @player.x >= GOAL_X && playing?
|
18
|
+
game_clear
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def finish
|
23
|
+
@state = STATE[:finished]
|
24
|
+
end
|
25
|
+
|
26
|
+
def finished?
|
27
|
+
@state == STATE[:finished]
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def initialize(player, deadline, enemies)
|
33
|
+
@player = player
|
34
|
+
@deadline = deadline
|
35
|
+
@enemies = enemies
|
36
|
+
@fireworks = RoadToRubykaigi::Fireworks.new(self)
|
37
|
+
@state = STATE[:playing]
|
38
|
+
end
|
39
|
+
|
40
|
+
def player_moved?
|
41
|
+
@player_initial_x ||= @player.x
|
42
|
+
@player_initial_x != @player.x
|
43
|
+
end
|
44
|
+
|
45
|
+
def playing?
|
46
|
+
@state == STATE[:playing]
|
47
|
+
end
|
48
|
+
|
49
|
+
def game_clear
|
50
|
+
@state = STATE[:ending]
|
51
|
+
@fireworks.shoot
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module RoadToRubykaigi
|
2
|
+
module Manager
|
3
|
+
class UpdateManager
|
4
|
+
def update(offset_x:)
|
5
|
+
enemies = @entities[3]
|
6
|
+
enemies.each do |enemy|
|
7
|
+
enemy.activate_with_offset(offset_x)
|
8
|
+
end
|
9
|
+
@entities.each(&:update)
|
10
|
+
enforce_boundary(offset_x: offset_x)
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def initialize(map, foreground, fireworks)
|
16
|
+
@map = map
|
17
|
+
@entities = foreground.layers + [fireworks]
|
18
|
+
end
|
19
|
+
|
20
|
+
def enforce_boundary(offset_x:)
|
21
|
+
@entities.each do |entity|
|
22
|
+
entity.respond_to?(:enforce_boundary) && entity.enforce_boundary(@map, offset_x:)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
module RoadToRubykaigi
|
2
|
+
class Map
|
3
|
+
VIEWPORT_WIDTH = 100
|
4
|
+
VIEWPORT_HEIGHT = 30
|
5
|
+
attr_reader :width, :height
|
6
|
+
|
7
|
+
def build_buffer(offset_x:)
|
8
|
+
(0...VIEWPORT_HEIGHT).map do |row|
|
9
|
+
@tiles[row][offset_x, VIEWPORT_WIDTH].map(&:character)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def clamp_position(x:, y:, width:, height:, dx:, dy:)
|
14
|
+
clamped_x = x.clamp(2, @width - width)
|
15
|
+
clamped_y = y.clamp(2, @height - height)
|
16
|
+
return [clamped_x, clamped_y] if box_passable?(clamped_x, clamped_y, width, height)
|
17
|
+
|
18
|
+
attempt_count = 10
|
19
|
+
delta_x = nil
|
20
|
+
delta_y = nil
|
21
|
+
unless dx == 0
|
22
|
+
(1..attempt_count).each do |i|
|
23
|
+
attempt_x = clamped_x + i * dx
|
24
|
+
if box_passable?(attempt_x, clamped_y, width, height)
|
25
|
+
break delta_x = attempt_x - clamped_x
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
unless dy == 0
|
30
|
+
(1..attempt_count).each do |i|
|
31
|
+
attempt_y = [clamped_y + i * dy, RoadToRubykaigi::Sprite::Player::BASE_Y].min
|
32
|
+
if box_passable?(clamped_x, attempt_y, width, height)
|
33
|
+
break delta_y = attempt_y - clamped_y
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
case
|
39
|
+
when delta_x && delta_y
|
40
|
+
if delta_x.abs <= delta_y.abs
|
41
|
+
[clamped_x + delta_x, clamped_y]
|
42
|
+
else
|
43
|
+
[clamped_x, clamped_y + delta_y]
|
44
|
+
end
|
45
|
+
when delta_x && !delta_y
|
46
|
+
[clamped_x + delta_x, clamped_y]
|
47
|
+
when !delta_x && delta_y
|
48
|
+
[clamped_x, clamped_y + delta_y]
|
49
|
+
else
|
50
|
+
coordinates = (1..attempt_count).select do |i|
|
51
|
+
attempt_x = clamped_x + i * dx
|
52
|
+
attempt_y = [clamped_y + i * dy, RoadToRubykaigi::Sprite::Player::BASE_Y].min
|
53
|
+
if box_passable?(attempt_x, attempt_y, width, height)
|
54
|
+
break [attempt_x, attempt_y]
|
55
|
+
end
|
56
|
+
end
|
57
|
+
coordinates.empty? ? [clamped_x + dx, clamped_y + dy] : coordinates
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def passable_at?(col, row)
|
62
|
+
@tiles[row-1][col-1].passable?
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def initialize
|
68
|
+
map_data = RoadToRubykaigi::Graphics::Map.data
|
69
|
+
mask_data = RoadToRubykaigi::Graphics::Mask.data
|
70
|
+
@tiles = map_data.each_with_index.map do |line, row|
|
71
|
+
line.chars.each_with_index.map do |ch, col|
|
72
|
+
Tile.new(ch, mask: mask_data[row][col])
|
73
|
+
end
|
74
|
+
end
|
75
|
+
@height = @tiles.size
|
76
|
+
@width = @tiles.first.size
|
77
|
+
end
|
78
|
+
|
79
|
+
def box_passable?(x, y, width, height)
|
80
|
+
(y...(y + height)).all? do |row|
|
81
|
+
(x...(x + width)).all? do |col|
|
82
|
+
passable_at?(col, row)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
class Layer
|
89
|
+
attr_reader :layers
|
90
|
+
|
91
|
+
def build_buffer(offset_x:)
|
92
|
+
@layers.map { |layer| layer.build_buffer(offset_x: offset_x) }
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
def initialize(player:, deadline:, bonuses:, enemies:, attacks:, effects:)
|
98
|
+
@layers = [player, deadline, bonuses, enemies, attacks, effects]
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
class Tile
|
103
|
+
MASK_CHAR = "#"
|
104
|
+
|
105
|
+
def character
|
106
|
+
@symbol
|
107
|
+
end
|
108
|
+
|
109
|
+
def passable?
|
110
|
+
@mask != MASK_CHAR
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
|
115
|
+
def initialize(symbol, mask:)
|
116
|
+
@symbol = symbol
|
117
|
+
@mask = mask
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module RoadToRubykaigi
|
2
|
+
class OpeningScreen
|
3
|
+
WIDTH = 10
|
4
|
+
OFFSET = 30
|
5
|
+
DELAY = 0.75
|
6
|
+
LOGO =<<~LOGO
|
7
|
+
╔═══════╗
|
8
|
+
║ ║
|
9
|
+
║ ║ ║
|
10
|
+
║ ║ ║
|
11
|
+
╠═════╦═╝ ╔═══════╗ ╔═══════║ ╔══════╣
|
12
|
+
║ ╚═╗ ║ ║ ║ ║ ╔╝ ║ ══╬══ ╔═══╗
|
13
|
+
║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║
|
14
|
+
║ ║ ╚═══════╝ ╚═══════║ ╚═══════╝ ║ ╚═══╝
|
15
|
+
|
16
|
+
╔═══════╗ ║ ║
|
17
|
+
║ ║ ║ ║
|
18
|
+
║ ║ ║ ║ ║ ║ ║ ╔═══════║
|
19
|
+
║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║
|
20
|
+
╠═════╦═╝ ║ ║ ╠══════╗ ║ ║ ╠═════╦═╝ ╔═══════║ ║ ║
|
21
|
+
║ ╚═╗ ║ ║ ║ ╚╗ ╚═══════╣ ║ ╚═╗ ║ ║ ║ ╚═══════╣ ║
|
22
|
+
║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║
|
23
|
+
║ ║ ╚═══════║ ╚═══════╝ ════════╝ ║ ║ ╚═══════║ ║ ════════╝ ║
|
24
|
+
LOGO
|
25
|
+
PLAYER =<<~PLAYER
|
26
|
+
╭──────╮
|
27
|
+
│。・◡・│_◢◤
|
28
|
+
╰ᜊ───ᜊ─╯
|
29
|
+
PLAYER
|
30
|
+
|
31
|
+
def display
|
32
|
+
x = 0
|
33
|
+
direction = 1
|
34
|
+
|
35
|
+
loop do
|
36
|
+
ANSI.clear
|
37
|
+
puts "\e[6;1H" + LOGO
|
38
|
+
puts [
|
39
|
+
PLAYER.lines.map.with_index do |line, i|
|
40
|
+
"\e[#{i+1};#{x+OFFSET}H" + line
|
41
|
+
end.join,
|
42
|
+
"\e[4;1H" + "Press Space to start...",
|
43
|
+
]
|
44
|
+
if $stdin.raw { $stdin.read_nonblock(1, exception: false) == " " }
|
45
|
+
break true
|
46
|
+
end
|
47
|
+
|
48
|
+
x += direction
|
49
|
+
if x >= WIDTH || x <= 0
|
50
|
+
direction = -direction
|
51
|
+
end
|
52
|
+
sleep DELAY
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require "forwardable"
|
2
|
+
|
3
|
+
module RoadToRubykaigi
|
4
|
+
module Sprite
|
5
|
+
class Attacks
|
6
|
+
extend Forwardable
|
7
|
+
def_delegators :@attacks, :each, :delete, :select
|
8
|
+
|
9
|
+
def add(x, y)
|
10
|
+
@attacks << Attack.new(x, y)
|
11
|
+
end
|
12
|
+
|
13
|
+
def update
|
14
|
+
@attacks.each(&:move)
|
15
|
+
end
|
16
|
+
|
17
|
+
def enforce_boundary(map, offset_x:)
|
18
|
+
@attacks.reject! do |attack|
|
19
|
+
attack.reach_border?(map, offset_x: offset_x)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def build_buffer(offset_x:)
|
24
|
+
buffer = Array.new(Map::VIEWPORT_HEIGHT) { Array.new(Map::VIEWPORT_WIDTH) { "" } }
|
25
|
+
@attacks.each do |attack|
|
26
|
+
bounding_box = attack.bounding_box
|
27
|
+
relative_x = bounding_box[:x] - offset_x - 1
|
28
|
+
relative_y = bounding_box[:y] - 1
|
29
|
+
next if relative_x < 1
|
30
|
+
attack.characters.each_with_index do |chara, j|
|
31
|
+
buffer[relative_y][relative_x+j] = chara
|
32
|
+
end
|
33
|
+
end
|
34
|
+
buffer
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def initialize
|
40
|
+
@attacks = []
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class Attack < Sprite
|
45
|
+
SYMBOL = ".˖"
|
46
|
+
|
47
|
+
def move
|
48
|
+
@x += 1
|
49
|
+
end
|
50
|
+
|
51
|
+
def characters
|
52
|
+
super { SYMBOL.chars }
|
53
|
+
end
|
54
|
+
|
55
|
+
def reach_border?(map, offset_x:)
|
56
|
+
(@x - offset_x + SYMBOL.size - 1) > Map::VIEWPORT_WIDTH ||
|
57
|
+
(@x + SYMBOL.size) > map.width
|
58
|
+
end
|
59
|
+
|
60
|
+
def bounding_box
|
61
|
+
{ x: @x, y: @y, width: SYMBOL.size, height: 1 }
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def initialize(x, y)
|
67
|
+
@x = x
|
68
|
+
@y = y
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
require "forwardable"
|
2
|
+
|
3
|
+
module RoadToRubykaigi
|
4
|
+
module Sprite
|
5
|
+
class Bonuses
|
6
|
+
extend Forwardable
|
7
|
+
def_delegators :@bonuses, :to_a, :find, :delete
|
8
|
+
BONUSES_DATA = {
|
9
|
+
Basic: [
|
10
|
+
{ x: 39, y: 22, character: :ruby },
|
11
|
+
{ x: 46, y: 22, character: :ruby },
|
12
|
+
{ x: 53, y: 22, character: :ruby },
|
13
|
+
{ x: 107, y: 23, character: :coffee },
|
14
|
+
{ x: 110, y: 23, character: :book },
|
15
|
+
{ x: 142, y: 16, character: :ruby },
|
16
|
+
{ x: 146, y: 16, character: :ruby },
|
17
|
+
{ x: 205, y: 19, character: :money },
|
18
|
+
{ x: 212, y: 19, character: :money },
|
19
|
+
{ x: 205, y: 19, character: :money },
|
20
|
+
{ x: 212, y: 19, character: :money },
|
21
|
+
{ x: 223, y: 17, character: :money },
|
22
|
+
{ x: 231, y: 17, character: :money },
|
23
|
+
{ x: 243, y: 13, character: :money },
|
24
|
+
{ x: 250, y: 13, character: :money },
|
25
|
+
{ x: 260, y: 10, character: :sushi },
|
26
|
+
{ x: 265, y: 10, character: :meat },
|
27
|
+
{ x: 270, y: 10, character: :fish },
|
28
|
+
{ x: 260, y: 10, character: :sushi },
|
29
|
+
{ x: 265, y: 10, character: :meat },
|
30
|
+
{ x: 270, y: 10, character: :fish },
|
31
|
+
{ x: 275, y: 10, character: :sushi },
|
32
|
+
{ x: 280, y: 10, character: :meat },
|
33
|
+
{ x: 285, y: 10, character: :fish },
|
34
|
+
{ x: 290, y: 10, character: :sushi },
|
35
|
+
{ x: 295, y: 10, character: :meat },
|
36
|
+
{ x: 300, y: 10, character: :fish },
|
37
|
+
{ x: 358, y: 15, character: :money },
|
38
|
+
{ x: 363, y: 13, character: :money },
|
39
|
+
{ x: 368, y: 15, character: :money },
|
40
|
+
{ x: 373, y: 13, character: :money },
|
41
|
+
{ x: 378, y: 15, character: :money },
|
42
|
+
{ x: 383, y: 13, character: :money },
|
43
|
+
{ x: 388, y: 15, character: :money },
|
44
|
+
],
|
45
|
+
Alcohol: [
|
46
|
+
{ x: 217, y: 28, character: :beer },
|
47
|
+
{ x: 220, y: 28, character: :beer },
|
48
|
+
{ x: 223, y: 28, character: :beer },
|
49
|
+
],
|
50
|
+
Laptop: [
|
51
|
+
{ x: 298, y: 23, character: :laptop },
|
52
|
+
]
|
53
|
+
}
|
54
|
+
|
55
|
+
def build_buffer(offset_x:)
|
56
|
+
buffer = Array.new(Map::VIEWPORT_HEIGHT) { Array.new(Map::VIEWPORT_WIDTH) { "" } }
|
57
|
+
@bonuses.each do |bonus|
|
58
|
+
bounding_box = bonus.bounding_box
|
59
|
+
relative_x = bounding_box[:x] - offset_x - 1
|
60
|
+
relative_y = bounding_box[:y] - 1
|
61
|
+
next if relative_x < 1
|
62
|
+
bonus.characters.each_with_index do |character, j|
|
63
|
+
next if relative_x + j >= Map::VIEWPORT_WIDTH - 1
|
64
|
+
buffer[relative_y][relative_x+j] = character
|
65
|
+
end
|
66
|
+
end
|
67
|
+
buffer
|
68
|
+
end
|
69
|
+
|
70
|
+
def update
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def initialize
|
76
|
+
@bonuses = BONUSES_DATA.map do |key, bonuses|
|
77
|
+
bonuses.map do |bonus|
|
78
|
+
Bonus.new(
|
79
|
+
bonus[:x],
|
80
|
+
bonus[:y],
|
81
|
+
bonus[:character],
|
82
|
+
)
|
83
|
+
end
|
84
|
+
end.flatten
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
class Bonus < Sprite
|
89
|
+
CHARACTER = {
|
90
|
+
ruby: "💎",
|
91
|
+
money: "💰",
|
92
|
+
coffee: "☕️",
|
93
|
+
book: "📚",
|
94
|
+
sushi: "🍣",
|
95
|
+
meat: "🍖",
|
96
|
+
fish: "🐟",
|
97
|
+
beer: "🍺",
|
98
|
+
sake: "🍶",
|
99
|
+
laptop: "💻",
|
100
|
+
}
|
101
|
+
|
102
|
+
def bounding_box
|
103
|
+
{ x: @x, y: @y, width: width, height: height }
|
104
|
+
end
|
105
|
+
|
106
|
+
def characters
|
107
|
+
super { [CHARACTER[@character]] }
|
108
|
+
end
|
109
|
+
|
110
|
+
def width
|
111
|
+
2
|
112
|
+
end
|
113
|
+
|
114
|
+
def height
|
115
|
+
1
|
116
|
+
end
|
117
|
+
|
118
|
+
private
|
119
|
+
|
120
|
+
def initialize(x, y, character)
|
121
|
+
@x = x
|
122
|
+
@y = y
|
123
|
+
@character = character
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|