orange_zest 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.editorconfig +2 -0
- data/.github/workflows/main.yml +16 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +36 -0
- data/LICENSE.txt +21 -0
- data/README.md +45 -0
- data/Rakefile +8 -0
- data/examples/basic.rb +86 -0
- data/examples/res/ball.png +0 -0
- data/examples/ui.rb +43 -0
- data/lib/orange_zest/animation.rb +52 -0
- data/lib/orange_zest/box.rb +26 -0
- data/lib/orange_zest/component.rb +32 -0
- data/lib/orange_zest/entity.rb +88 -0
- data/lib/orange_zest/group.rb +49 -0
- data/lib/orange_zest/input.rb +37 -0
- data/lib/orange_zest/point.rb +43 -0
- data/lib/orange_zest/scheduler.rb +48 -0
- data/lib/orange_zest/trigger_condition.rb +39 -0
- data/lib/orange_zest/ui/tooltip.rb +43 -0
- data/lib/orange_zest/ui.rb +14 -0
- data/lib/orange_zest/version.rb +5 -0
- data/lib/orange_zest/window.rb +33 -0
- data/lib/orange_zest.rb +21 -0
- data/orange_zest.gemspec +29 -0
- metadata +89 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 86f58491ecd24a1b7bc7f00fdf6ef8ededee32319660e83b4c97816a5a2d6263
|
4
|
+
data.tar.gz: fd151abcfe7ecbeac7fe08459d5bb12eda93d1f05f5044c50ee354212e8a1bf8
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 733200a7e3ffd918dd961e90dc6a6185a5be1aa2fcb45a8a1c608c146955d8fd315411a3550edafe38a41954961733b5570653bc7f1cfabc12da9971eb201b44
|
7
|
+
data.tar.gz: 7110079d1c76dfc8de9a05b5c50d02c8d85798d3ab0652191ad8aabf549fa60b8f84de526360ead66a6163f489ff6005c679ee0d16ea91980d6afeeda73a91bb
|
data/.editorconfig
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
name: Ruby
|
2
|
+
|
3
|
+
on: [push,pull_request]
|
4
|
+
|
5
|
+
jobs:
|
6
|
+
build:
|
7
|
+
runs-on: ubuntu-latest
|
8
|
+
steps:
|
9
|
+
- uses: actions/checkout@v2
|
10
|
+
- name: Set up Ruby
|
11
|
+
uses: ruby/setup-ruby@v1
|
12
|
+
with:
|
13
|
+
ruby-version: 3.0.2
|
14
|
+
bundler-cache: true
|
15
|
+
- name: Run the default task
|
16
|
+
run: bundle exec rake
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/CHANGELOG.md
ADDED
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
# Contributor Covenant Code of Conduct
|
2
|
+
|
3
|
+
## Our Pledge
|
4
|
+
|
5
|
+
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
6
|
+
|
7
|
+
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
|
8
|
+
|
9
|
+
## Our Standards
|
10
|
+
|
11
|
+
Examples of behavior that contributes to a positive environment for our community include:
|
12
|
+
|
13
|
+
* Demonstrating empathy and kindness toward other people
|
14
|
+
* Being respectful of differing opinions, viewpoints, and experiences
|
15
|
+
* Giving and gracefully accepting constructive feedback
|
16
|
+
* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
|
17
|
+
* Focusing on what is best not just for us as individuals, but for the overall community
|
18
|
+
|
19
|
+
Examples of unacceptable behavior include:
|
20
|
+
|
21
|
+
* The use of sexualized language or imagery, and sexual attention or
|
22
|
+
advances of any kind
|
23
|
+
* Trolling, insulting or derogatory comments, and personal or political attacks
|
24
|
+
* Public or private harassment
|
25
|
+
* Publishing others' private information, such as a physical or email
|
26
|
+
address, without their explicit permission
|
27
|
+
* Other conduct which could reasonably be considered inappropriate in a
|
28
|
+
professional setting
|
29
|
+
|
30
|
+
## Enforcement Responsibilities
|
31
|
+
|
32
|
+
Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
|
33
|
+
|
34
|
+
Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
|
35
|
+
|
36
|
+
## Scope
|
37
|
+
|
38
|
+
This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
|
39
|
+
|
40
|
+
## Enforcement
|
41
|
+
|
42
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at aaronc20000@gmail.com. All complaints will be reviewed and investigated promptly and fairly.
|
43
|
+
|
44
|
+
All community leaders are obligated to respect the privacy and security of the reporter of any incident.
|
45
|
+
|
46
|
+
## Enforcement Guidelines
|
47
|
+
|
48
|
+
Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
|
49
|
+
|
50
|
+
### 1. Correction
|
51
|
+
|
52
|
+
**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
|
53
|
+
|
54
|
+
**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
|
55
|
+
|
56
|
+
### 2. Warning
|
57
|
+
|
58
|
+
**Community Impact**: A violation through a single incident or series of actions.
|
59
|
+
|
60
|
+
**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
|
61
|
+
|
62
|
+
### 3. Temporary Ban
|
63
|
+
|
64
|
+
**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
|
65
|
+
|
66
|
+
**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
|
67
|
+
|
68
|
+
### 4. Permanent Ban
|
69
|
+
|
70
|
+
**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
|
71
|
+
|
72
|
+
**Consequence**: A permanent ban from any sort of public interaction within the community.
|
73
|
+
|
74
|
+
## Attribution
|
75
|
+
|
76
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0,
|
77
|
+
available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
78
|
+
|
79
|
+
Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
|
80
|
+
|
81
|
+
[homepage]: https://www.contributor-covenant.org
|
82
|
+
|
83
|
+
For answers to common questions about this code of conduct, see the FAQ at
|
84
|
+
https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
orange_zest (0.1.0)
|
5
|
+
gosu
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
diff-lcs (1.5.0)
|
11
|
+
gosu (9.9.9)
|
12
|
+
rake (13.0.6)
|
13
|
+
rspec (3.11.0)
|
14
|
+
rspec-core (~> 3.11.0)
|
15
|
+
rspec-expectations (~> 3.11.0)
|
16
|
+
rspec-mocks (~> 3.11.0)
|
17
|
+
rspec-core (3.11.0)
|
18
|
+
rspec-support (~> 3.11.0)
|
19
|
+
rspec-expectations (3.11.0)
|
20
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
21
|
+
rspec-support (~> 3.11.0)
|
22
|
+
rspec-mocks (3.11.1)
|
23
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
24
|
+
rspec-support (~> 3.11.0)
|
25
|
+
rspec-support (3.11.0)
|
26
|
+
|
27
|
+
PLATFORMS
|
28
|
+
arm64-darwin-21
|
29
|
+
|
30
|
+
DEPENDENCIES
|
31
|
+
orange_zest!
|
32
|
+
rake (~> 13.0)
|
33
|
+
rspec (~> 3.0)
|
34
|
+
|
35
|
+
BUNDLED WITH
|
36
|
+
2.2.22
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2022 Aaron Christiansen
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
# OrangeZest
|
2
|
+
|
3
|
+
OrangeZest is a **lightweight layer on top of the excellent
|
4
|
+
[Gosu game library](https://www.libgosu.org/)**.
|
5
|
+
|
6
|
+
I absolutely love Gosu because it provides the perfect level of abstraction for you to approach a
|
7
|
+
game's architecture however you like. _This_ is the architecture that I've enjoyed using for the
|
8
|
+
[last](https://github.com/AaronC81/pet-peeve)
|
9
|
+
[three](https://github.com/AaronC81/the-arcane-king)
|
10
|
+
[games](https://github.com/AaronC81/mini-mall-maker)
|
11
|
+
I've created for the Gosu Game Jam, and I've finally decided to break it out into a separate gem to
|
12
|
+
make it more reusable.
|
13
|
+
|
14
|
+
OrangeZest encourages using OOP rather than ECS, although it's lightweight and unopinionated enough
|
15
|
+
that you might be able to implement ECS on top of it. (The words "component" and "entity" in
|
16
|
+
OrangeZest _do not_ refer to the ECS concepts - they just happen to fit OrangeZest's concepts too!)
|
17
|
+
|
18
|
+
OrangeZest provides:
|
19
|
+
|
20
|
+
- _Components_, objects which can be instantiated into the game world to provide behaviour
|
21
|
+
- _Entities_, a subclass of components which has a position and animation
|
22
|
+
- _Groups_, to bundle together related components into one
|
23
|
+
- A very simple animation system
|
24
|
+
- Simple mathematical primitives (points and boxes)
|
25
|
+
|
26
|
+
## Installation
|
27
|
+
|
28
|
+
This gem is available on RubyGems as `orange_zest`. Add it to your Gemfile as `gem 'orange_zest'`,
|
29
|
+
or install it globally with `gem install orange_zest`.
|
30
|
+
|
31
|
+
## Getting Started
|
32
|
+
|
33
|
+
See the `examples` directory for some sample scripts which use OrangeZest.
|
34
|
+
|
35
|
+
## Contributing
|
36
|
+
|
37
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/AaronC81/orange_zest. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/AaronC81/orange_zest/blob/main/CODE_OF_CONDUCT.md).
|
38
|
+
|
39
|
+
## License
|
40
|
+
|
41
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
42
|
+
|
43
|
+
## Code of Conduct
|
44
|
+
|
45
|
+
Everyone interacting in the OrangeZest project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/AaronC81/orange_zest/blob/main/CODE_OF_CONDUCT.md).
|
data/Rakefile
ADDED
data/examples/basic.rb
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'gosu'
|
2
|
+
require 'orange_zest'
|
3
|
+
|
4
|
+
include OrangeZest
|
5
|
+
|
6
|
+
WIDTH = 640
|
7
|
+
HEIGHT = 480
|
8
|
+
|
9
|
+
# First, we'll define a component responsible for clearing the screen. Components can define
|
10
|
+
# `#update` and `#draw` to perform actions during Gosu's update and draw steps.
|
11
|
+
class ClearScreen < Component
|
12
|
+
def draw
|
13
|
+
Gosu.draw_rect(0, 0, WIDTH, HEIGHT, Gosu::Color::WHITE)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Now we'll define an entity, a special kind of component with a position and animation system.
|
18
|
+
class BouncingBall < Entity
|
19
|
+
IMAGE = Gosu::Image.new(File.join(__dir__, "res", "ball.png"))
|
20
|
+
SPEED = 5
|
21
|
+
|
22
|
+
def initialize(**kw)
|
23
|
+
super(
|
24
|
+
# Our "animation" is just a single frame, so we can use the `Animation.static` helper to
|
25
|
+
# create it easily.
|
26
|
+
# We've named this animation `:normal` - these names are used to switch between animations
|
27
|
+
# later if you've got more than one.
|
28
|
+
# However, we don't need to worry about that here - the first animation is started by default
|
29
|
+
# when the entity is created.
|
30
|
+
animations: {
|
31
|
+
normal: Animation.static(IMAGE)
|
32
|
+
},
|
33
|
+
**kw
|
34
|
+
)
|
35
|
+
|
36
|
+
@velocity = [SPEED, SPEED]
|
37
|
+
end
|
38
|
+
|
39
|
+
# Override `#update` to implement our bouncy-ball logic by updating the entity's position
|
40
|
+
# (Remember to call `super`, or animations won't work as expected!)
|
41
|
+
def update
|
42
|
+
super
|
43
|
+
|
44
|
+
# Move the ball
|
45
|
+
position.x += @velocity[0]
|
46
|
+
position.y += @velocity[1]
|
47
|
+
|
48
|
+
# Bounce if we've hit left or right walls
|
49
|
+
# (`#image` retrives the current animation frame)
|
50
|
+
if position.x <= 0
|
51
|
+
@velocity[0] = SPEED
|
52
|
+
elsif position.x + image.width >= WIDTH
|
53
|
+
@velocity[0] = -SPEED
|
54
|
+
end
|
55
|
+
|
56
|
+
# Bounce if we've hit top or bottom walls
|
57
|
+
if position.y <= 0
|
58
|
+
@velocity[1] = SPEED
|
59
|
+
elsif position.y + image.height >= HEIGHT
|
60
|
+
@velocity[1] = -SPEED
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Now we can define our window - `OrangeZest::Window` is a subclass of `Gosu::Window` which makes
|
66
|
+
# our life easier.
|
67
|
+
class Game < OrangeZest::Window
|
68
|
+
def initialize
|
69
|
+
super WIDTH, HEIGHT
|
70
|
+
|
71
|
+
# Instantiate our components, and call `#register` to add them to the main group.
|
72
|
+
# Groups are components which can contain other components.
|
73
|
+
# Components fire in the order they are registered, so we must register `ClearScreen` first.
|
74
|
+
ClearScreen.new.register
|
75
|
+
BouncingBall.new(position: Point.new(50, 50)).register
|
76
|
+
end
|
77
|
+
|
78
|
+
# There's nothing else to do!
|
79
|
+
# We added our components to the main group, and one of the key things that `OrangeZest::Window`
|
80
|
+
# does is to update and draw this group for us.
|
81
|
+
# The main group always exists and is the default for `#register`, but you can create more groups
|
82
|
+
# and manage them manually if you need to. `#register` can take a different group as an argument.
|
83
|
+
end
|
84
|
+
|
85
|
+
# Let's go!
|
86
|
+
Game.new.show
|
Binary file
|
data/examples/ui.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'gosu'
|
2
|
+
require 'orange_zest'
|
3
|
+
|
4
|
+
include OrangeZest
|
5
|
+
|
6
|
+
WIDTH = 640
|
7
|
+
HEIGHT = 480
|
8
|
+
|
9
|
+
class ClearScreen < Component
|
10
|
+
def draw
|
11
|
+
Gosu.draw_rect(0, 0, WIDTH, HEIGHT, Gosu::Color::WHITE)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class BouncingBall < Entity
|
16
|
+
IMAGE = Gosu::Image.new(File.join(__dir__, "res", "ball.png"))
|
17
|
+
|
18
|
+
include UI::Tooltip
|
19
|
+
|
20
|
+
def initialize(**kw)
|
21
|
+
super(animations: { normal: Animation.static(IMAGE) }, **kw)
|
22
|
+
@tooltip = "Hello"
|
23
|
+
end
|
24
|
+
|
25
|
+
def draw
|
26
|
+
super
|
27
|
+
draw_tooltip
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class Game < OrangeZest::Window
|
32
|
+
def initialize
|
33
|
+
super WIDTH, HEIGHT
|
34
|
+
|
35
|
+
UI.default_font = Gosu::Font.new(16, name: "Arial")
|
36
|
+
|
37
|
+
ClearScreen.new.register
|
38
|
+
BouncingBall.new(position: Point.new(50, 50)).register
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Let's go!
|
43
|
+
Game.new.show
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module OrangeZest
|
2
|
+
# An animation which can be attached to an `Entity`.
|
3
|
+
class Animation
|
4
|
+
# The number of ticks to display each frame for. If -1, the current frame will be displayed
|
5
|
+
# forever.
|
6
|
+
# @return [Integer]
|
7
|
+
attr_accessor :ticks_per_image
|
8
|
+
|
9
|
+
# The images to cycle through as part of this animation.
|
10
|
+
# @return [<Gosu::Image>]
|
11
|
+
attr_accessor :images
|
12
|
+
|
13
|
+
def initialize(images, ticks_per_image)
|
14
|
+
@images = images
|
15
|
+
@ticks_per_image = ticks_per_image
|
16
|
+
|
17
|
+
reset
|
18
|
+
end
|
19
|
+
|
20
|
+
# A helper method to create an animation with a single static frame.
|
21
|
+
# @param [Gosu::Image] image
|
22
|
+
def self.static(image)
|
23
|
+
new([image], -1)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Resets this animation to its first frame.
|
27
|
+
def reset
|
28
|
+
@ticks = 0
|
29
|
+
@image_idx = 0
|
30
|
+
end
|
31
|
+
|
32
|
+
# Ticks this animation, advancing it to the next frame if enough ticks have passed.
|
33
|
+
# (Despite implementing this method, this is not a `Component`, as it does not make sense for
|
34
|
+
# it to exist on its own.)
|
35
|
+
def update
|
36
|
+
return if @ticks_per_image == -1
|
37
|
+
|
38
|
+
@ticks += 1
|
39
|
+
if @ticks >= @ticks_per_image
|
40
|
+
@image_idx += 1
|
41
|
+
@image_idx %= @images.length
|
42
|
+
@ticks = 0
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# The current frame.
|
47
|
+
# @return [Gosu::Image]
|
48
|
+
def image
|
49
|
+
@images[@image_idx]
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module OrangeZest
|
2
|
+
# A box, with an origin in the top-left corner.
|
3
|
+
class Box
|
4
|
+
def initialize(origin, width, height)
|
5
|
+
@origin = origin
|
6
|
+
@width = width
|
7
|
+
@height = height
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_accessor :origin, :width, :height
|
11
|
+
|
12
|
+
# Returns true if this box overlaps another box.
|
13
|
+
def overlaps?(other)
|
14
|
+
self.origin.x < other.origin.x + other.width \
|
15
|
+
&& other.origin.x < self.origin.x + self.width \
|
16
|
+
&& self.origin.y < other.origin.y + other.height \
|
17
|
+
&& other.origin.y < self.origin.y + self.height
|
18
|
+
end
|
19
|
+
|
20
|
+
# Returns true if a point is inside this box.
|
21
|
+
def point_inside?(point)
|
22
|
+
point.x >= origin.x && point.x <= origin.x + width \
|
23
|
+
&& point.y >= origin.y && point.y <= origin.y + height
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module OrangeZest
|
2
|
+
# An object which can exist in the game world, and responds to Gosu's `#update` and `#draw`
|
3
|
+
# methods.
|
4
|
+
class Component
|
5
|
+
# The group which this component is registered with, if any.
|
6
|
+
# @return [Group, nil]
|
7
|
+
attr_reader :group
|
8
|
+
|
9
|
+
# Called during Gosu's update phase. A stub to be overridden.
|
10
|
+
def update; end
|
11
|
+
|
12
|
+
# Called during Gosu's draw phase. A stub to be overridden.
|
13
|
+
def draw; end
|
14
|
+
|
15
|
+
# Adds this component to a group. If no group is given, uses the `Main` group.
|
16
|
+
# Returns `self` so you can chain this with an assignment.
|
17
|
+
# @param [Group, nil] group
|
18
|
+
# @return [Component] Itself.
|
19
|
+
def register(group=nil)
|
20
|
+
group ||= Group::Main
|
21
|
+
group.add(self)
|
22
|
+
@group = group
|
23
|
+
self
|
24
|
+
end
|
25
|
+
|
26
|
+
# Removes this component from its group.
|
27
|
+
def unregister
|
28
|
+
raise 'tried to unregister component which is not registered' unless group
|
29
|
+
group.remove(self)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require_relative 'animation'
|
2
|
+
require_relative 'point'
|
3
|
+
require_relative 'box'
|
4
|
+
|
5
|
+
module OrangeZest
|
6
|
+
# A subclass of `Component` with associated size, position, and animation information.
|
7
|
+
class Entity < Component
|
8
|
+
# The position of this object.
|
9
|
+
# @return [Point]
|
10
|
+
attr_accessor :position
|
11
|
+
|
12
|
+
# The animations which this object can perform.
|
13
|
+
# @return [{Object => Animation}]
|
14
|
+
attr_accessor :animations
|
15
|
+
|
16
|
+
# The scaling of this entity.
|
17
|
+
# @return [Numeric]
|
18
|
+
attr_accessor :scaling
|
19
|
+
|
20
|
+
# Whether this object should be mirrored along its X axis. Useful for flipping animations when
|
21
|
+
# creating a game with a 2D character.
|
22
|
+
# @return [Boolean]
|
23
|
+
attr_accessor :mirror_x
|
24
|
+
|
25
|
+
# The rotation of this entity.
|
26
|
+
# @return [Numeric]
|
27
|
+
attr_accessor :rotation
|
28
|
+
|
29
|
+
# The opacity of this entity. 255 is fully opaque, and 0 is fully transparent.
|
30
|
+
# @return [Integer]
|
31
|
+
attr_accessor :opacity
|
32
|
+
|
33
|
+
def initialize(position: nil, animations: nil, scaling: nil)
|
34
|
+
@position = position || Point.new(0, 0)
|
35
|
+
@animations = animations || {}
|
36
|
+
@scaling = scaling || 1
|
37
|
+
@rotation = 0
|
38
|
+
@opacity = 255
|
39
|
+
|
40
|
+
if animations.any?
|
41
|
+
@current_animation_name, @current_animation = animations.first
|
42
|
+
end
|
43
|
+
|
44
|
+
@mirror_x = false
|
45
|
+
end
|
46
|
+
|
47
|
+
# Retrieve the current frame of the currently-playing animation.
|
48
|
+
def image
|
49
|
+
@current_animation&.image
|
50
|
+
end
|
51
|
+
|
52
|
+
# Begin playing the animation with the given name, from the beginning.
|
53
|
+
# Raises an exception if an animation with that name does not exist in `#animations`.
|
54
|
+
# @param [Object] anim_name The name of the animation to begin, which should be a key in
|
55
|
+
# `#animations`.
|
56
|
+
def animation=(anim_name)
|
57
|
+
return if @current_animation_name == anim_name
|
58
|
+
|
59
|
+
@current_animation = @animations[anim_name]
|
60
|
+
@current_animation_name = anim_name
|
61
|
+
raise "no animation named #{anim_name}" if !@current_animation
|
62
|
+
|
63
|
+
@current_animation.reset
|
64
|
+
end
|
65
|
+
|
66
|
+
def update
|
67
|
+
@current_animation&.update
|
68
|
+
end
|
69
|
+
|
70
|
+
def draw
|
71
|
+
return unless image
|
72
|
+
|
73
|
+
image.draw_rot(
|
74
|
+
position.x + (mirror_x ? image.width * scaling : 0), position.y, position.z,
|
75
|
+
rotation, 0, 0,
|
76
|
+
scaling * (mirror_x ? -1 : 1),
|
77
|
+
scaling,
|
78
|
+
Gosu::Color.new(opacity * 255, 255, 255, 255),
|
79
|
+
)
|
80
|
+
end
|
81
|
+
|
82
|
+
# Returns a bounding box for this entity, starting at its position and spanning the size of its
|
83
|
+
# current frame.
|
84
|
+
def bounding_box
|
85
|
+
Box.new(position, image.width, image.height)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require_relative 'component'
|
2
|
+
|
3
|
+
module OrangeZest
|
4
|
+
# Wraps a collection of `Component`s into a single `Component`.
|
5
|
+
#
|
6
|
+
# The associated instance within this class, `Main`, is a special group available everywhere which
|
7
|
+
# components can be added to easily.
|
8
|
+
class Group < Component
|
9
|
+
# The items within this group.
|
10
|
+
# @return [<Component>]
|
11
|
+
attr_accessor :items
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
@items = []
|
15
|
+
@enabled = true
|
16
|
+
end
|
17
|
+
|
18
|
+
# Whether to call `#update` or `#draw` on the items in this group.
|
19
|
+
# @return [Boolean]
|
20
|
+
attr_accessor :enabled
|
21
|
+
alias enabled? enabled
|
22
|
+
|
23
|
+
# Adds a component to this group.
|
24
|
+
# @param [Component] component
|
25
|
+
def add(component)
|
26
|
+
@items << component
|
27
|
+
end
|
28
|
+
alias << add
|
29
|
+
|
30
|
+
# Removes a component from this group.
|
31
|
+
# @param [Component] component
|
32
|
+
def remove(component)
|
33
|
+
@items.delete(component)
|
34
|
+
end
|
35
|
+
alias delete remove
|
36
|
+
|
37
|
+
def update
|
38
|
+
return unless enabled?
|
39
|
+
items.each(&:update)
|
40
|
+
end
|
41
|
+
|
42
|
+
def draw
|
43
|
+
return unless enabled?
|
44
|
+
items.each(&:draw)
|
45
|
+
end
|
46
|
+
|
47
|
+
Main = new
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module OrangeZest
|
2
|
+
# Holds some input state so that it can be globally accessed.
|
3
|
+
#
|
4
|
+
# In Gosu, the mouse cursor position is only available to the `Gosu::Window`, which makes it
|
5
|
+
# tricky to break the UI into components. This module stores and shares the cursor position for
|
6
|
+
# global access.
|
7
|
+
module Input
|
8
|
+
# Should be called from your `Gosu::Window#button_down` implementation. Updates `.click?`.
|
9
|
+
# @param [Integer] id The keycode pressed.
|
10
|
+
def self.button_down(id)
|
11
|
+
@click = (id == Gosu::MS_LEFT)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Should be called from your `Gosu::Window'#update` implementation. Updates `.cursor`.
|
15
|
+
# @param [Gosu::Window] window The window this is being called from.
|
16
|
+
def self.update(window)
|
17
|
+
@cursor = Point.new(window.mouse_x, window.mouse_y)
|
18
|
+
end
|
19
|
+
|
20
|
+
# The mouse cursor position within the window.
|
21
|
+
def self.cursor
|
22
|
+
@cursor
|
23
|
+
end
|
24
|
+
|
25
|
+
# Whether the left mouse button was clicked this frame. Even if the mouse is held down, this
|
26
|
+
# will only be `true` for the first frame of the click.
|
27
|
+
def self.click?
|
28
|
+
@click
|
29
|
+
end
|
30
|
+
|
31
|
+
# Clears the click registered this frame, if any. This can be used to ensure that a click is
|
32
|
+
# only seen by one UI element.
|
33
|
+
def self.clear_click
|
34
|
+
@click = false
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module OrangeZest
|
2
|
+
# A point in the game's world.
|
3
|
+
class Point
|
4
|
+
attr_accessor :x, :y, :z
|
5
|
+
|
6
|
+
def initialize(x, y, z = 0)
|
7
|
+
@x = x
|
8
|
+
@y = y
|
9
|
+
@z = z
|
10
|
+
end
|
11
|
+
|
12
|
+
def +(other)
|
13
|
+
raise "can't add point to #{other}" unless other.is_a?(Point)
|
14
|
+
Point.new(x + other.x, y + other.y, z + other.z)
|
15
|
+
end
|
16
|
+
|
17
|
+
def -@
|
18
|
+
Point.new(-x, -y, -z)
|
19
|
+
end
|
20
|
+
|
21
|
+
def -(other)
|
22
|
+
self + -other
|
23
|
+
end
|
24
|
+
|
25
|
+
def ==(other)
|
26
|
+
other.is_a?(Point) &&
|
27
|
+
x == other.x &&
|
28
|
+
y == other.y &&
|
29
|
+
z == other.z
|
30
|
+
end
|
31
|
+
|
32
|
+
def hash
|
33
|
+
[x, y, z].hash
|
34
|
+
end
|
35
|
+
|
36
|
+
# Computes the distance between this point and another.
|
37
|
+
def distance(other)
|
38
|
+
x_dist = (x - other.x)
|
39
|
+
y_dist = (y - other.y)
|
40
|
+
Math.sqrt(x_dist**2 + y_dist**2)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module OrangeZest
|
2
|
+
# Provides utilities for starting a task which persists across multiple updates. Tasks can suspend
|
3
|
+
# themselves for a fixed number of frames.
|
4
|
+
#
|
5
|
+
# The order in which resumed tasks run is not guaranteed. Tasks are not processes/threads and do
|
6
|
+
# not run asynchronously.
|
7
|
+
module Scheduler
|
8
|
+
@@pending = []
|
9
|
+
|
10
|
+
WaitResult = Struct.new('WaitResult', :time)
|
11
|
+
|
12
|
+
# Starts a new task, which executes the given block, and executes it until its first
|
13
|
+
# {Scheduler.wait} call.
|
14
|
+
#
|
15
|
+
# If a task terminates without ever waiting, an exception is thrown, since this is likely a bug.
|
16
|
+
def self.start(&block)
|
17
|
+
fiber = Fiber.new(&block)
|
18
|
+
result = fiber.resume
|
19
|
+
raise 'Scheduler task never waited' unless result.is_a?(WaitResult)
|
20
|
+
@@pending << [result.time, fiber]
|
21
|
+
nil
|
22
|
+
end
|
23
|
+
|
24
|
+
# Suspends the current task (started with {Scheduler.start}) for a given number of frames.
|
25
|
+
# @param [Integer] ticks The number of frames to suspend for.
|
26
|
+
def self.wait(ticks)
|
27
|
+
Fiber.yield(WaitResult.new(ticks))
|
28
|
+
end
|
29
|
+
|
30
|
+
# Advance all suspended tasks by one frame. Called automatically by {OrangeZest::Window.update}.
|
31
|
+
def self.update
|
32
|
+
@@pending.map! do |(time, task)|
|
33
|
+
time = time - 1
|
34
|
+
if time <= 0
|
35
|
+
result = task.resume
|
36
|
+
if result.is_a?(WaitResult)
|
37
|
+
[result.time, task]
|
38
|
+
else
|
39
|
+
nil
|
40
|
+
end
|
41
|
+
else
|
42
|
+
[time, task]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
@@pending.compact!
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module OrangeZest
|
2
|
+
# Allows wrapping a boolean condition to monitor it for changes.
|
3
|
+
module TriggerCondition
|
4
|
+
ON = :on
|
5
|
+
OFF = :off
|
6
|
+
|
7
|
+
@@states = {}
|
8
|
+
|
9
|
+
# Watches the given boolean condition, and returns:
|
10
|
+
#
|
11
|
+
# - {TriggerCondition::ON} if the condition was previously false, and becomes true
|
12
|
+
# - {TriggerCondition::OFF} if the condition was previously true, and becomes false
|
13
|
+
# - `nil` if the condition has not changed
|
14
|
+
#
|
15
|
+
# Conditions are assumed to be false on the first call.
|
16
|
+
#
|
17
|
+
# Each separate condition is identified by its source file and line number, which means you
|
18
|
+
# **must not watch more than one condition on a single line**.
|
19
|
+
#
|
20
|
+
# @param [Boolean] condition The condition to watch.
|
21
|
+
# @return The change in condition, if it has changed.
|
22
|
+
def self.watch(condition)
|
23
|
+
c = caller.first
|
24
|
+
triggered = @@states[c]
|
25
|
+
|
26
|
+
if (!condition && !triggered) || (condition && triggered)
|
27
|
+
nil
|
28
|
+
elsif condition && !triggered
|
29
|
+
@@states[c] = true
|
30
|
+
TriggerCondition::ON
|
31
|
+
elsif !condition && triggered
|
32
|
+
@@states[c] = false
|
33
|
+
TriggerCondition::OFF
|
34
|
+
else
|
35
|
+
raise 'unexpected condition case'
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module OrangeZest::UI
|
2
|
+
# A mixin which enables an object to show a tooltip when hovered.
|
3
|
+
#
|
4
|
+
# The object must have a method named `#bounding_box` which returns a `Box`. Then, your component
|
5
|
+
# should call `#draw_tooltip` during `#draw`, which will display the tooltip if the mouse cursor
|
6
|
+
# is within the bounding box.
|
7
|
+
module Tooltip
|
8
|
+
# An optional tooltip to display when the button is hovered. The tooltip may have multiple
|
9
|
+
# lines.
|
10
|
+
attr_accessor :tooltip
|
11
|
+
|
12
|
+
# Draws the tooltip if the object is being hovered. If a block is passed, the block is executed
|
13
|
+
# after the tooltip is drawn.
|
14
|
+
# @param [Gosu::Font, nil] font The font to use to draw the tooltip, or nil to use
|
15
|
+
# `OrangeZest::UI.default_font`.
|
16
|
+
def draw_tooltip(font: nil, &block)
|
17
|
+
font = UI.default_font! unless font
|
18
|
+
|
19
|
+
if tooltip && bounding_box.point_inside?(Input.cursor)
|
20
|
+
# Find how tall the tooltip needs to be
|
21
|
+
lines = tooltip.split("\n")
|
22
|
+
text_width = lines.map { |l| font.text_width(l) }.max
|
23
|
+
text_height = lines.length * font.height
|
24
|
+
|
25
|
+
# Draw rectangle with some padding, clamp to edges of screen
|
26
|
+
padding = 10
|
27
|
+
origin_x = [Input.cursor.x, Window.current.width - (text_width + padding * 2)].min
|
28
|
+
origin_y = [Input.cursor.y, text_height + padding * 2].max
|
29
|
+
Gosu.draw_rect(
|
30
|
+
origin_x, origin_y - text_height - padding * 2,
|
31
|
+
text_width + padding * 2, text_height + padding * 2,
|
32
|
+
Gosu::Color.argb(0xDD, 0x00, 0x00, 0x00), 1000,
|
33
|
+
)
|
34
|
+
font.draw_text(
|
35
|
+
tooltip, origin_x + padding, origin_y - text_height - padding, 1000, 1, 1,
|
36
|
+
Gosu::Color::WHITE,
|
37
|
+
)
|
38
|
+
|
39
|
+
block&.()
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module OrangeZest
|
2
|
+
module UI
|
3
|
+
class << self
|
4
|
+
attr_accessor :default_font
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.default_font!
|
8
|
+
raise "tried to fall back to default font, but none is set" if default_font.nil?
|
9
|
+
default_font
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
require_relative "ui/tooltip"
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module OrangeZest
|
2
|
+
# A subclass of `Gosu::Window` which overrides a variety of default methods in order to:
|
3
|
+
# - Draw and update the main group
|
4
|
+
# - Update and provide inputs to {OrangeZest::Input}
|
5
|
+
class Window < Gosu::Window
|
6
|
+
def self.current
|
7
|
+
@@current
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(*args)
|
11
|
+
super(*args)
|
12
|
+
raise "OrangeZest only supports one window" if defined? @@current
|
13
|
+
@@current = self
|
14
|
+
end
|
15
|
+
|
16
|
+
def update
|
17
|
+
super
|
18
|
+
Input.update(self)
|
19
|
+
Scheduler.update
|
20
|
+
Group::Main.update
|
21
|
+
end
|
22
|
+
|
23
|
+
def draw
|
24
|
+
super
|
25
|
+
Group::Main.draw
|
26
|
+
end
|
27
|
+
|
28
|
+
def button_down(id)
|
29
|
+
super
|
30
|
+
Input.button_down(id)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/lib/orange_zest.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module OrangeZest; end
|
4
|
+
|
5
|
+
require "gosu"
|
6
|
+
|
7
|
+
require_relative "orange_zest/version"
|
8
|
+
|
9
|
+
require_relative "orange_zest/component"
|
10
|
+
require_relative "orange_zest/group"
|
11
|
+
|
12
|
+
require_relative "orange_zest/box"
|
13
|
+
require_relative "orange_zest/point"
|
14
|
+
require_relative "orange_zest/entity"
|
15
|
+
require_relative "orange_zest/scheduler"
|
16
|
+
|
17
|
+
require_relative "orange_zest/input"
|
18
|
+
require_relative "orange_zest/window"
|
19
|
+
require_relative "orange_zest/trigger_condition"
|
20
|
+
|
21
|
+
require_relative "orange_zest/ui"
|
data/orange_zest.gemspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lib/orange_zest/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "orange_zest"
|
7
|
+
spec.version = OrangeZest::VERSION
|
8
|
+
spec.authors = ["Aaron Christiansen"]
|
9
|
+
spec.email = ["aaronc20000@gmail.com"]
|
10
|
+
|
11
|
+
spec.summary = "A light wrapper for Gosu"
|
12
|
+
spec.homepage = "http://github.com/AaronC81/orange_zest"
|
13
|
+
spec.license = "MIT"
|
14
|
+
spec.required_ruby_version = ">= 2.4.0"
|
15
|
+
|
16
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
17
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
18
|
+
spec.metadata["changelog_uri"] = "#{spec.homepage}/CHANGELOG.md"
|
19
|
+
|
20
|
+
# Specify which files should be added to the gem when it is released.
|
21
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
22
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
23
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
|
24
|
+
end
|
25
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
26
|
+
spec.require_paths = ["lib"]
|
27
|
+
|
28
|
+
spec.add_dependency "gosu"
|
29
|
+
end
|
metadata
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: orange_zest
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Aaron Christiansen
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2022-12-30 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: gosu
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
description:
|
28
|
+
email:
|
29
|
+
- aaronc20000@gmail.com
|
30
|
+
executables: []
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- ".editorconfig"
|
35
|
+
- ".github/workflows/main.yml"
|
36
|
+
- ".gitignore"
|
37
|
+
- ".rspec"
|
38
|
+
- CHANGELOG.md
|
39
|
+
- CODE_OF_CONDUCT.md
|
40
|
+
- Gemfile
|
41
|
+
- Gemfile.lock
|
42
|
+
- LICENSE.txt
|
43
|
+
- README.md
|
44
|
+
- Rakefile
|
45
|
+
- examples/basic.rb
|
46
|
+
- examples/res/ball.png
|
47
|
+
- examples/ui.rb
|
48
|
+
- lib/orange_zest.rb
|
49
|
+
- lib/orange_zest/animation.rb
|
50
|
+
- lib/orange_zest/box.rb
|
51
|
+
- lib/orange_zest/component.rb
|
52
|
+
- lib/orange_zest/entity.rb
|
53
|
+
- lib/orange_zest/group.rb
|
54
|
+
- lib/orange_zest/input.rb
|
55
|
+
- lib/orange_zest/point.rb
|
56
|
+
- lib/orange_zest/scheduler.rb
|
57
|
+
- lib/orange_zest/trigger_condition.rb
|
58
|
+
- lib/orange_zest/ui.rb
|
59
|
+
- lib/orange_zest/ui/tooltip.rb
|
60
|
+
- lib/orange_zest/version.rb
|
61
|
+
- lib/orange_zest/window.rb
|
62
|
+
- orange_zest.gemspec
|
63
|
+
homepage: http://github.com/AaronC81/orange_zest
|
64
|
+
licenses:
|
65
|
+
- MIT
|
66
|
+
metadata:
|
67
|
+
homepage_uri: http://github.com/AaronC81/orange_zest
|
68
|
+
source_code_uri: http://github.com/AaronC81/orange_zest
|
69
|
+
changelog_uri: http://github.com/AaronC81/orange_zest/CHANGELOG.md
|
70
|
+
post_install_message:
|
71
|
+
rdoc_options: []
|
72
|
+
require_paths:
|
73
|
+
- lib
|
74
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
75
|
+
requirements:
|
76
|
+
- - ">="
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: 2.4.0
|
79
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - ">="
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '0'
|
84
|
+
requirements: []
|
85
|
+
rubygems_version: 3.2.22
|
86
|
+
signing_key:
|
87
|
+
specification_version: 4
|
88
|
+
summary: A light wrapper for Gosu
|
89
|
+
test_files: []
|