orange_zest 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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,2 @@
1
+ [*.rb]
2
+ indent_size = 2
@@ -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
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2022-08-27
4
+
5
+ - Initial release
@@ -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
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in orange_zest.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
9
+
10
+ gem "rspec", "~> 3.0"
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
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OrangeZest
4
+ VERSION = "0.1.0"
5
+ end
@@ -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
@@ -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"
@@ -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: []