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
|