red_bird 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +13 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +23 -0
- data/LICENSE.txt +21 -0
- data/README.md +47 -0
- data/Rakefile +10 -0
- data/bin/setup +8 -0
- data/ext/red_bird/bird.c +15 -0
- data/ext/red_bird/bird.h +10 -0
- data/ext/red_bird/color.c +95 -0
- data/ext/red_bird/color.h +27 -0
- data/ext/red_bird/dynamic_sprite.c +163 -0
- data/ext/red_bird/dynamic_sprite.h +30 -0
- data/ext/red_bird/engine.c +354 -0
- data/ext/red_bird/engine.h +40 -0
- data/ext/red_bird/extconf.rb +9 -0
- data/ext/red_bird/font.c +94 -0
- data/ext/red_bird/font.h +26 -0
- data/ext/red_bird/input_device.c +100 -0
- data/ext/red_bird/input_device.h +15 -0
- data/ext/red_bird/keycode.c +42 -0
- data/ext/red_bird/keycode.h +12 -0
- data/ext/red_bird/loader.c +154 -0
- data/ext/red_bird/loader.h +54 -0
- data/ext/red_bird/main.c +38 -0
- data/ext/red_bird/main.h +12 -0
- data/ext/red_bird/palette.c +132 -0
- data/ext/red_bird/palette.h +23 -0
- data/ext/red_bird/rect.c +257 -0
- data/ext/red_bird/rect.h +20 -0
- data/ext/red_bird/render.c +130 -0
- data/ext/red_bird/render.h +25 -0
- data/ext/red_bird/sprite.c +130 -0
- data/ext/red_bird/sprite.h +27 -0
- data/ext/red_bird/text.c +212 -0
- data/ext/red_bird/text.h +31 -0
- data/ext/red_bird/texture.c +157 -0
- data/ext/red_bird/texture.h +33 -0
- data/ext/red_bird/texture_imp.cpp +49 -0
- data/ext/red_bird/texture_imp.hpp +29 -0
- data/ext/red_bird/timer.c +134 -0
- data/ext/red_bird/timer.h +25 -0
- data/lib/red_bird.rb +15 -0
- data/lib/red_bird/animation.rb +133 -0
- data/lib/red_bird/camera.rb +61 -0
- data/lib/red_bird/controller.rb +44 -0
- data/lib/red_bird/dynamic_sprite.rb +38 -0
- data/lib/red_bird/engine.rb +81 -0
- data/lib/red_bird/entity.rb +74 -0
- data/lib/red_bird/entity_collision.rb +31 -0
- data/lib/red_bird/input_device.rb +86 -0
- data/lib/red_bird/palette.rb +23 -0
- data/lib/red_bird/relative_entity.rb +95 -0
- data/lib/red_bird/sprite.rb +40 -0
- data/lib/red_bird/stage.rb +60 -0
- data/lib/red_bird/tile_map.rb +118 -0
- data/lib/red_bird/tile_set.rb +56 -0
- data/lib/red_bird/uibox.rb +143 -0
- data/lib/red_bird/version.rb +3 -0
- data/lib/red_bird/vertical_menu.rb +110 -0
- data/red_bird.gemspec +37 -0
- metadata +149 -0
@@ -0,0 +1,33 @@
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
2
|
+
#ifndef RED_BIRD_TEXTURE_H
|
3
|
+
#define RED_BIRD_TEXTURE_H 1
|
4
|
+
|
5
|
+
#include "main.h"
|
6
|
+
|
7
|
+
extern VALUE bird_cTexture;
|
8
|
+
|
9
|
+
struct bird_texture_data
|
10
|
+
{
|
11
|
+
SDL_Texture *data;
|
12
|
+
int width, height;
|
13
|
+
};
|
14
|
+
|
15
|
+
VALUE
|
16
|
+
bird_alloc_texture(VALUE klass);
|
17
|
+
|
18
|
+
VALUE
|
19
|
+
bird_cTexture_initialize(VALUE self, VALUE file_path, VALUE palette);
|
20
|
+
|
21
|
+
VALUE
|
22
|
+
bird_cTexture_width(VALUE self);
|
23
|
+
|
24
|
+
VALUE
|
25
|
+
bird_cTexture_height(VALUE self);
|
26
|
+
|
27
|
+
struct bird_texture_data*
|
28
|
+
bird_cTexture_get_data(VALUE self);
|
29
|
+
|
30
|
+
void
|
31
|
+
Init_red_bird_texture(void);
|
32
|
+
|
33
|
+
#endif /* RED_BIRD_TEXTURE_H */
|
@@ -0,0 +1,49 @@
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
2
|
+
#include "texture_imp.hpp"
|
3
|
+
|
4
|
+
#include <charconv>
|
5
|
+
#include <fstream>
|
6
|
+
#include <string>
|
7
|
+
|
8
|
+
SDL_bool
|
9
|
+
bird_cTexture_PGM_load(
|
10
|
+
struct bird_cTexture_PGM *pgm_img, const char *file_path)
|
11
|
+
{
|
12
|
+
int wh_pos;
|
13
|
+
std::string line;
|
14
|
+
std::ifstream file(file_path);
|
15
|
+
|
16
|
+
if(!file) return SDL_FALSE;
|
17
|
+
|
18
|
+
// Read file magic.
|
19
|
+
std::getline(file, line);
|
20
|
+
if(line != "P5") return SDL_FALSE;
|
21
|
+
|
22
|
+
// Read file comment.
|
23
|
+
std::getline(file, line);
|
24
|
+
|
25
|
+
// Read file width and height.
|
26
|
+
std::getline(file, line);
|
27
|
+
wh_pos = line.find(" ");
|
28
|
+
std::from_chars(line.data(), line.data() + wh_pos, pgm_img->width);
|
29
|
+
std::from_chars(
|
30
|
+
line.data() + wh_pos + 1, line.data() + line.size(), pgm_img->height);
|
31
|
+
|
32
|
+
// Read file maximum value.
|
33
|
+
std::getline(file, line);
|
34
|
+
std::from_chars(line.data(), line.data() + line.size(), pgm_img->max_value);
|
35
|
+
|
36
|
+
// Read file values.
|
37
|
+
std::getline(file, line);
|
38
|
+
pgm_img->data_size = line.size();
|
39
|
+
pgm_img->data = new char[pgm_img->data_size];
|
40
|
+
memcpy(pgm_img->data, line.data(), pgm_img->data_size);
|
41
|
+
|
42
|
+
return SDL_TRUE;
|
43
|
+
}
|
44
|
+
|
45
|
+
void
|
46
|
+
bird_cTexture_PGM_unload(bird_cTexture_PGM *pgm_img)
|
47
|
+
{
|
48
|
+
delete[] pgm_img->data;
|
49
|
+
}
|
@@ -0,0 +1,29 @@
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
2
|
+
#ifndef RED_BIRD_TEXTURE_IMP_H
|
3
|
+
#define RED_BIRD_TEXTURE_IMP_H 1
|
4
|
+
|
5
|
+
#ifdef __cplusplus
|
6
|
+
extern "C"
|
7
|
+
{
|
8
|
+
#endif
|
9
|
+
|
10
|
+
#include "main.h"
|
11
|
+
|
12
|
+
struct bird_cTexture_PGM
|
13
|
+
{
|
14
|
+
int width, height, max_value, data_size;
|
15
|
+
char *data;
|
16
|
+
};
|
17
|
+
|
18
|
+
SDL_bool
|
19
|
+
bird_cTexture_PGM_load(
|
20
|
+
struct bird_cTexture_PGM *pgm_img, const char *file_path);
|
21
|
+
|
22
|
+
void
|
23
|
+
bird_cTexture_PGM_unload(struct bird_cTexture_PGM *pgm_img);
|
24
|
+
|
25
|
+
#ifdef __cplusplus
|
26
|
+
}
|
27
|
+
#endif
|
28
|
+
|
29
|
+
#endif /* RED_BIRD_TEXTURE_IMP_H */
|
@@ -0,0 +1,134 @@
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
2
|
+
#include "timer.h"
|
3
|
+
|
4
|
+
#include "engine.h"
|
5
|
+
|
6
|
+
/*
|
7
|
+
Document-class: RedBird::Timer
|
8
|
+
|
9
|
+
A Timer controls the speed of execution of a frame; it prevents that a frame
|
10
|
+
run too quickly and give you compensation when a frame runs too slow.
|
11
|
+
|
12
|
+
When creating a new instance, you must define the maximum frame rate that you
|
13
|
+
want; then, you should call {#tick} at every frame to get a delta time. The
|
14
|
+
delta time is proportional to the frame rate defined during the creation of
|
15
|
+
the instance or is higher when the last frame was slower than expected.
|
16
|
+
|
17
|
+
@author Frederico Linhares
|
18
|
+
*/
|
19
|
+
VALUE bird_cTimer;
|
20
|
+
|
21
|
+
/*
|
22
|
+
Basic functions all Ruby classes need.
|
23
|
+
*/
|
24
|
+
|
25
|
+
static void
|
26
|
+
bird_free_timer(void* obj)
|
27
|
+
{
|
28
|
+
struct bird_timer_data *ptr = obj;
|
29
|
+
|
30
|
+
free(ptr);
|
31
|
+
}
|
32
|
+
|
33
|
+
static size_t
|
34
|
+
bird_memsize_timer(const void* obj)
|
35
|
+
{
|
36
|
+
// TODO
|
37
|
+
return 0;
|
38
|
+
}
|
39
|
+
|
40
|
+
static const rb_data_type_t
|
41
|
+
bird_timer_type = {
|
42
|
+
"red_bird_timer",
|
43
|
+
{0, bird_free_timer, bird_memsize_timer,},
|
44
|
+
0, 0,
|
45
|
+
RUBY_TYPED_FREE_IMMEDIATELY,
|
46
|
+
};
|
47
|
+
|
48
|
+
static VALUE
|
49
|
+
bird_alloc_timer(VALUE klass)
|
50
|
+
{
|
51
|
+
VALUE obj;
|
52
|
+
struct bird_timer_data *ptr;
|
53
|
+
|
54
|
+
obj = TypedData_Make_Struct(klass, struct bird_timer_data, &bird_timer_type,
|
55
|
+
ptr);
|
56
|
+
|
57
|
+
return obj;
|
58
|
+
}
|
59
|
+
|
60
|
+
/*
|
61
|
+
@param max_fps [Integer] the maximum of frames to be rendered per second.
|
62
|
+
@author Frederico Linhares
|
63
|
+
*/
|
64
|
+
VALUE
|
65
|
+
bird_cTimer_initialize(VALUE self, VALUE max_fps)
|
66
|
+
{
|
67
|
+
struct bird_timer_data *ptr;
|
68
|
+
|
69
|
+
if(!engine_initialized)
|
70
|
+
rb_raise(rb_eRuntimeError, "%s",
|
71
|
+
"can not create a RedBird::Timer instance before "
|
72
|
+
"RedBird::Engine is started");
|
73
|
+
|
74
|
+
RB_INTEGER_TYPE_P(max_fps);
|
75
|
+
|
76
|
+
TypedData_Get_Struct(self, struct bird_timer_data, &bird_timer_type, ptr);
|
77
|
+
ptr->max_frame_duration_ms = 1000 / NUM2INT(max_fps);
|
78
|
+
ptr->max_frame_duration_sec = ptr->max_frame_duration_ms/1000.0;
|
79
|
+
ptr->frame_start = SDL_GetTicks();
|
80
|
+
|
81
|
+
return self;
|
82
|
+
}
|
83
|
+
|
84
|
+
/*
|
85
|
+
Every time you call this method, it compares the current time with the last
|
86
|
+
time it was called; if the amount of time is inferior to the frame duration,
|
87
|
+
it waits to ensure the frame has the exact duration expected; otherwise, it
|
88
|
+
returns instantly and gives you a delta value that compensates for the extra
|
89
|
+
duration of the last frame.
|
90
|
+
|
91
|
+
@return [Float] represents the amount of time between this frame and the last
|
92
|
+
frame, the higher, the higher amount of time has passed.
|
93
|
+
@author Frederico Linhares
|
94
|
+
*/
|
95
|
+
VALUE
|
96
|
+
bird_cTimer_tick(VALUE self)
|
97
|
+
{
|
98
|
+
struct bird_timer_data *ptr;
|
99
|
+
Uint32 frame_stop;
|
100
|
+
Uint32 frame_duration;
|
101
|
+
VALUE delta;
|
102
|
+
|
103
|
+
TypedData_Get_Struct(self, struct bird_timer_data, &bird_timer_type, ptr);
|
104
|
+
|
105
|
+
frame_stop = SDL_GetTicks();
|
106
|
+
frame_duration = frame_stop - ptr->frame_start;
|
107
|
+
|
108
|
+
// If frame take less time than maximum allowed.
|
109
|
+
if(ptr->max_frame_duration_ms > frame_duration)
|
110
|
+
{
|
111
|
+
SDL_Delay(ptr->max_frame_duration_ms - frame_duration);
|
112
|
+
delta = rb_float_new(ptr->max_frame_duration_sec);
|
113
|
+
}
|
114
|
+
// If frame take too long time.
|
115
|
+
else
|
116
|
+
{
|
117
|
+
// SDL_GetTicks return time im miliseconds, so I need to divide by 1000 to
|
118
|
+
// get the time in seconds.
|
119
|
+
delta = rb_float_new((double)frame_duration/1000.0);
|
120
|
+
}
|
121
|
+
|
122
|
+
ptr->frame_start = frame_stop;
|
123
|
+
|
124
|
+
return delta;
|
125
|
+
}
|
126
|
+
|
127
|
+
void
|
128
|
+
Init_red_bird_timer(void)
|
129
|
+
{
|
130
|
+
bird_cTimer = rb_define_class_under(bird_m, "Timer", rb_cData);
|
131
|
+
rb_define_alloc_func(bird_cTimer, bird_alloc_timer);
|
132
|
+
rb_define_method(bird_cTimer, "initialize", bird_cTimer_initialize, 1);
|
133
|
+
rb_define_method(bird_cTimer, "tick", bird_cTimer_tick, 0);
|
134
|
+
}
|
@@ -0,0 +1,25 @@
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
2
|
+
#ifndef RED_BIRD_TIMER_H
|
3
|
+
#define RED_BIRD_TIMER_H 1
|
4
|
+
|
5
|
+
#include "main.h"
|
6
|
+
|
7
|
+
extern VALUE bird_cTimer;
|
8
|
+
|
9
|
+
struct bird_timer_data
|
10
|
+
{
|
11
|
+
Uint32 frame_start;
|
12
|
+
Uint32 max_frame_duration_ms; // in miliseconds
|
13
|
+
double max_frame_duration_sec; // in seconds
|
14
|
+
};
|
15
|
+
|
16
|
+
VALUE
|
17
|
+
bird_cTimer_initialize(VALUE self, VALUE max_fps);
|
18
|
+
|
19
|
+
VALUE
|
20
|
+
bird_cTimer_tick(VALUE self);
|
21
|
+
|
22
|
+
void
|
23
|
+
Init_red_bird_timer(void);
|
24
|
+
|
25
|
+
#endif // RED_BIRD_TIMER_H
|
data/lib/red_bird.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
2
|
+
require_relative "red_bird/red_bird"
|
3
|
+
require_relative "red_bird/controller"
|
4
|
+
require_relative "red_bird/dynamic_sprite"
|
5
|
+
require_relative "red_bird/engine"
|
6
|
+
require_relative "red_bird/entity"
|
7
|
+
require_relative "red_bird/input_device"
|
8
|
+
require_relative "red_bird/palette"
|
9
|
+
require_relative "red_bird/sprite"
|
10
|
+
require_relative "red_bird/version"
|
11
|
+
|
12
|
+
module RedBird
|
13
|
+
class Error < StandardError; end
|
14
|
+
# Your code goes here...
|
15
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
2
|
+
module RedBird
|
3
|
+
# This module contains different kinds of animations.
|
4
|
+
module Animation
|
5
|
+
# Each frame has a sprite and a duration.
|
6
|
+
#
|
7
|
+
# @!attribute [r] duration
|
8
|
+
# @return [Integer] how long this frame lasts.
|
9
|
+
# @!attribute [r] sprite
|
10
|
+
# @return [RedBird::Sprite] sprite to display in this frame.
|
11
|
+
# @see RedBird::Animation::Base
|
12
|
+
# @author Frederico Linhares
|
13
|
+
class Frame
|
14
|
+
attr_reader :duration, :sprite
|
15
|
+
|
16
|
+
# @param duration [Integer] this animation frame will persist for as many
|
17
|
+
# game frames as defined by this value.
|
18
|
+
# @param sprite [RedBird::Sprite] sprite to be displayed in this frame.
|
19
|
+
# @author Frederico Linhares
|
20
|
+
def initialize(duration, sprite)
|
21
|
+
@duration = duration
|
22
|
+
@sprite = sprite
|
23
|
+
end
|
24
|
+
|
25
|
+
# Create an array of frames and return it. This method must receive an
|
26
|
+
# Array.
|
27
|
+
#
|
28
|
+
# @param frames [Array] each object in it must be an array containing the
|
29
|
+
# attributes for each frame returned.
|
30
|
+
# @author Frederico Linhares
|
31
|
+
def self.sequence(frames)
|
32
|
+
return frames.collect { |i| Frame.new(i[0], i[1]) }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Base class for different animations. An animation consists of a sequence
|
37
|
+
# of frames, a state to define which one to show and which comes next and
|
38
|
+
# an animation method to determine how the frames change.
|
39
|
+
#
|
40
|
+
# @author Frederico Linhares
|
41
|
+
class Base
|
42
|
+
# @return [RedBird::Sprite] the sprite for the current stage of the
|
43
|
+
# animation.
|
44
|
+
attr_reader :current_sprite
|
45
|
+
|
46
|
+
# @param frames [Array] an array of RedBird::Frame.
|
47
|
+
# @author Frederico Linhares
|
48
|
+
def initialize(frames)
|
49
|
+
@frames = frames
|
50
|
+
self.set
|
51
|
+
end
|
52
|
+
|
53
|
+
# Set this animation to the initial state.
|
54
|
+
#
|
55
|
+
# @author Frederico Linhares
|
56
|
+
def reset
|
57
|
+
self.set
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# This animation starts in the first frame and goes through every frame
|
62
|
+
# until the last, then it goes back to the first frame and starts again.
|
63
|
+
#
|
64
|
+
# @author Frederico Linhares
|
65
|
+
class Loop < Base
|
66
|
+
# This method must be called for every tick. It changes the current
|
67
|
+
# displayed sprite.
|
68
|
+
def animate
|
69
|
+
@current_time += 1
|
70
|
+
if @current_time > @frames[@current_frame].duration then
|
71
|
+
@current_time -= @frames[@current_frame].duration
|
72
|
+
@current_frame += 1
|
73
|
+
if @current_frame >= @frames.size then
|
74
|
+
@current_frame = 0
|
75
|
+
end
|
76
|
+
@current_sprite = @frames[@current_frame].sprite
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
protected
|
81
|
+
def set
|
82
|
+
@current_time = 0
|
83
|
+
@current_frame = 0
|
84
|
+
@current_sprite = @frames[@current_frame].sprite
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# This animation starts in the first frame and goes through every frame
|
89
|
+
# until the last; then, it moves backward through every frame and starts
|
90
|
+
# again.
|
91
|
+
#
|
92
|
+
# @author Frederico Linhares
|
93
|
+
class BackAndForth < Base
|
94
|
+
# This method must be called for every tick. It changes the current
|
95
|
+
# displayed sprite.
|
96
|
+
def animate
|
97
|
+
case @direction
|
98
|
+
when :forward
|
99
|
+
@current_time += 1
|
100
|
+
if @current_time > @frames[@current_frame].duration then
|
101
|
+
@current_time -= @frames[@current_frame].duration
|
102
|
+
@current_frame += 1
|
103
|
+
if @current_frame >= @frames.size then
|
104
|
+
@direction = :backward
|
105
|
+
@current_frame = @frames.size - 2
|
106
|
+
end
|
107
|
+
@current_sprite = @frames[@current_frame].sprite
|
108
|
+
end
|
109
|
+
when :backward
|
110
|
+
@current_time += 1
|
111
|
+
if @current_time > @frames[@current_frame].duration then
|
112
|
+
@current_time -= @frames[@current_frame].duration
|
113
|
+
@current_frame -= 1
|
114
|
+
if @current_frame < 0 then
|
115
|
+
@direction = :forward
|
116
|
+
@current_frame = 1
|
117
|
+
end
|
118
|
+
@current_sprite = @frames[@current_frame].sprite
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
protected
|
124
|
+
def set
|
125
|
+
@direction = :forward
|
126
|
+
@current_time = 0
|
127
|
+
@current_frame = 0
|
128
|
+
@current_sprite = @frames[@current_frame].sprite
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
2
|
+
module RedBird
|
3
|
+
# Because most of the time the tilemap used as the scenario is larger than
|
4
|
+
# the screen, you need to choose an entity to use as a reference. A Camera
|
5
|
+
# control the scenario offset and makes the position of the scene rendered in
|
6
|
+
# the screen be relative to an entity.
|
7
|
+
#
|
8
|
+
# @author Frederico Linhares
|
9
|
+
class Camera
|
10
|
+
|
11
|
+
# @param focus [RedBird::RelativeEntity] entity to use as reference for the
|
12
|
+
# scenario
|
13
|
+
# @param scenario [RedBird::TileMap]
|
14
|
+
# @author Frederico Linhares
|
15
|
+
def initialize(focus, scenario)
|
16
|
+
@scenario = scenario
|
17
|
+
@max_hor_offset = @scenario.total_width - @scenario.width
|
18
|
+
@max_ver_offset = @scenario.total_height - @scenario.height
|
19
|
+
self.focus = focus
|
20
|
+
end
|
21
|
+
|
22
|
+
# Change the camere focus.
|
23
|
+
#
|
24
|
+
# @param focus [RedBird::RelativeEntity]
|
25
|
+
# @author Frederico Linhares
|
26
|
+
def focus=(focus)
|
27
|
+
@focus = focus
|
28
|
+
set_center
|
29
|
+
end
|
30
|
+
|
31
|
+
# Updates scenario offset according to entity position.
|
32
|
+
#
|
33
|
+
# @author Frederico Linhares
|
34
|
+
def call
|
35
|
+
hor_offset = (@focus.relative_pos_x - @hor_center).to_i
|
36
|
+
if hor_offset < 0 then
|
37
|
+
@scenario.hor_offset = 0
|
38
|
+
elsif hor_offset > @max_hor_offset then
|
39
|
+
@scenario.hor_offset = @max_hor_offset
|
40
|
+
else
|
41
|
+
@scenario.hor_offset = hor_offset
|
42
|
+
end
|
43
|
+
|
44
|
+
ver_offset = (@focus.relative_pos_y - @ver_center).to_i
|
45
|
+
if ver_offset < 0 then
|
46
|
+
@scenario.ver_offset = 0
|
47
|
+
elsif ver_offset > @max_ver_offset then
|
48
|
+
@scenario.ver_offset = @max_ver_offset
|
49
|
+
else
|
50
|
+
@scenario.ver_offset = ver_offset
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def set_center
|
57
|
+
@hor_center = @scenario.width / 2 - @focus.width / 2
|
58
|
+
@ver_center = @scenario.height / 2 - @focus.height / 2
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|