From b4852fbbd651238c7f8ca484ca5f40c73ac9bc1f Mon Sep 17 00:00:00 2001 From: Cyberarm Date: Thu, 7 Jun 2018 10:03:07 -0500 Subject: [PATCH] Initial Commit --- .gitignore | 8 + .travis.yml | 5 + Gemfile | 6 + LICENSE.txt | 21 ++ README.md | 43 +++ Rakefile | 10 + bin/console | 14 + bin/setup | 8 + cyberarm_engine.gemspec | 38 +++ lib/cyberarm_engine.rb | 15 + lib/cyberarm_engine/engine.rb | 80 +++++ lib/cyberarm_engine/game_object.rb | 302 ++++++++++++++++++ lib/cyberarm_engine/game_state.rb | 64 ++++ .../objects/multi_line_text.rb | 59 ++++ lib/cyberarm_engine/objects/text.rb | 95 ++++++ lib/cyberarm_engine/ui/button.rb | 122 +++++++ lib/cyberarm_engine/ui/check_box.rb | 73 +++++ lib/cyberarm_engine/ui/container.rb | 157 +++++++++ lib/cyberarm_engine/ui/input.rb | 117 +++++++ lib/cyberarm_engine/version.rb | 3 + test/cyberarm_engine_test.rb | 11 + test/test_helper.rb | 4 + 22 files changed, 1255 insertions(+) create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 Gemfile create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 Rakefile create mode 100644 bin/console create mode 100644 bin/setup create mode 100644 cyberarm_engine.gemspec create mode 100644 lib/cyberarm_engine.rb create mode 100644 lib/cyberarm_engine/engine.rb create mode 100644 lib/cyberarm_engine/game_object.rb create mode 100644 lib/cyberarm_engine/game_state.rb create mode 100644 lib/cyberarm_engine/objects/multi_line_text.rb create mode 100644 lib/cyberarm_engine/objects/text.rb create mode 100644 lib/cyberarm_engine/ui/button.rb create mode 100644 lib/cyberarm_engine/ui/check_box.rb create mode 100644 lib/cyberarm_engine/ui/container.rb create mode 100644 lib/cyberarm_engine/ui/input.rb create mode 100644 lib/cyberarm_engine/version.rb create mode 100644 test/cyberarm_engine_test.rb create mode 100644 test/test_helper.rb diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9106b2a --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +/.bundle/ +/.yardoc +/_yardoc/ +/coverage/ +/doc/ +/pkg/ +/spec/reports/ +/tmp/ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..6a8e36f --- /dev/null +++ b/.travis.yml @@ -0,0 +1,5 @@ +sudo: false +language: ruby +rvm: + - 2.5.0 +before_install: gem install bundler -v 1.16.1 diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..5718269 --- /dev/null +++ b/Gemfile @@ -0,0 +1,6 @@ +source "https://rubygems.org" + +git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } + +# Specify your gem's dependencies in cyberarm_engine.gemspec +gemspec diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..83f87b0 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2018 Cyberarm + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..28e0efa --- /dev/null +++ b/README.md @@ -0,0 +1,43 @@ +# CyberarmEngine + +Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/cyberarm_engine`. To experiment with that code, run `bin/console` for an interactive prompt. + +TODO: Delete this and the text above, and describe your gem + +## Installation + +Add this line to your application's Gemfile: + +```ruby +gem 'cyberarm_engine' +``` + +And then execute: + + $ bundle + +Or install it yourself as: + + $ gem install cyberarm_engine + +## Usage + +TODO: Write usage instructions here + +## Development + +After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. + +To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). + +## Contributing + +Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/cyberarm_engine. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. + +## License + +The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). + +## Code of Conduct + +Everyone interacting in the CyberarmEngine project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/cyberarm_engine/blob/master/CODE_OF_CONDUCT.md). diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..d433a1e --- /dev/null +++ b/Rakefile @@ -0,0 +1,10 @@ +require "bundler/gem_tasks" +require "rake/testtask" + +Rake::TestTask.new(:test) do |t| + t.libs << "test" + t.libs << "lib" + t.test_files = FileList["test/**/*_test.rb"] +end + +task :default => :test diff --git a/bin/console b/bin/console new file mode 100644 index 0000000..25dce29 --- /dev/null +++ b/bin/console @@ -0,0 +1,14 @@ +#!/usr/bin/env ruby + +require "bundler/setup" +require "cyberarm_engine" + +# You can add fixtures and/or initialization code here to make experimenting +# with your gem easier. You can also use a different console, if you like. + +# (If you use this, don't forget to add pry to your Gemfile!) +# require "pry" +# Pry.start + +require "irb" +IRB.start(__FILE__) diff --git a/bin/setup b/bin/setup new file mode 100644 index 0000000..dce67d8 --- /dev/null +++ b/bin/setup @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail +IFS=$'\n\t' +set -vx + +bundle install + +# Do any other automated setup that you need to do here diff --git a/cyberarm_engine.gemspec b/cyberarm_engine.gemspec new file mode 100644 index 0000000..6aac5f2 --- /dev/null +++ b/cyberarm_engine.gemspec @@ -0,0 +1,38 @@ + +lib = File.expand_path("../lib", __FILE__) +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) +require "cyberarm_engine/version" + +Gem::Specification.new do |spec| + spec.name = "cyberarm_engine" + spec.version = CyberarmEngine::VERSION + spec.authors = ["Cyberarm"] + spec.email = ["matthewlikesrobots@gmail.com"] + + spec.summary = %q{TODO: Write a short summary, because RubyGems requires one.} + spec.description = %q{TODO: Write a longer description or delete this line.} + spec.homepage = "TODO: Put your gem's website or public repo URL here." + spec.license = "MIT" + + # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host' + # to allow pushing to a single host or delete this section to allow pushing to any host. + if spec.respond_to?(:metadata) + spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'" + else + raise "RubyGems 2.0 or newer is required to protect against " \ + "public gem pushes." + end + + spec.files = `git ls-files -z`.split("\x0").reject do |f| + f.match(%r{^(test|spec|features)/}) + end + spec.bindir = "exe" + spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } + spec.require_paths = ["lib"] + + spec.add_dependency "gosu", "~> 0.13.3" + + spec.add_development_dependency "bundler", "~> 1.16" + spec.add_development_dependency "rake", "~> 10.0" + spec.add_development_dependency "minitest", "~> 5.0" +end diff --git a/lib/cyberarm_engine.rb b/lib/cyberarm_engine.rb new file mode 100644 index 0000000..56d1320 --- /dev/null +++ b/lib/cyberarm_engine.rb @@ -0,0 +1,15 @@ +require "gosu" + +require_relative "cyberarm_engine/version" + +require_relative "cyberarm_engine/game_object" +require_relative "cyberarm_engine/game_state" +require_relative "cyberarm_engine/engine" + +require_relative "cyberarm_engine/objects/text" +require_relative "cyberarm_engine/objects/multi_line_text" + +require_relative "cyberarm_engine/ui/button" +require_relative "cyberarm_engine/ui/check_box" +require_relative "cyberarm_engine/ui/input" +require_relative "cyberarm_engine/ui/container" diff --git a/lib/cyberarm_engine/engine.rb b/lib/cyberarm_engine/engine.rb new file mode 100644 index 0000000..372975b --- /dev/null +++ b/lib/cyberarm_engine/engine.rb @@ -0,0 +1,80 @@ +module CyberarmEngine + class Engine < Gosu::Window + attr_accessor :show_cursor + attr_reader :current_game_state, :last_game_state, :last_frame_time + + def self.now + Gosu.milliseconds + end + + def self.dt + $window.last_frame_time/1000.0 + end + + def initialize(width = 800, height = 600, fullscreen = false, update_interval = 1000.0/60) + @show_cursor = false + + super(width, height, fullscreen, update_interval) + $window = self + @last_frame_time = Gosu.milliseconds-1 + @current_frame_time = Gosu.milliseconds + self.caption = "CyberarmEngine #{CyberarmEngine::VERSION} #{Gosu.language}" + + setup if defined?(setup) + end + + def draw + if @current_game_state.is_a?(GameState) + @current_game_state.draw + end + end + + def update + if @current_game_state.is_a?(GameState) + @current_game_state.update + end + @last_frame_time = Gosu.milliseconds-@current_frame_time + @current_frame_time = Gosu.milliseconds + end + + def needs_cursor? + @show_cursor + end + + def dt + @last_frame_time/1000.0 + end + + def button_up(id) + @current_game_state.button_up(id) if @current_game_state + end + + def push_game_state(klass, options={}) + @last_game_state = @current_game_state if @current_game_state + if klass.instance_of?(klass.class) && defined?(klass.options) + @current_game_state = klass + else + klass.new(options) + end + end + + def set_game_state(klass_instance) + @current_game_state = klass_instance + end + + def previous_game_state + # current_game_state = @current_game_state + # @current_game_state = @last_frame_time + # @last_game_state = current_game_state + @last_game_state + end + + # Sourced from https://gist.github.com/ippa/662583 + def draw_circle(cx,cy,r, z = 9999,color = Gosu::Color::GREEN, step = 10) + 0.step(360, step) do |a1| + a2 = a1 + step + draw_line(cx + Gosu.offset_x(a1, r), cy + Gosu.offset_y(a1, r), color, cx + Gosu.offset_x(a2, r), cy + Gosu.offset_y(a2, r), color, z) + end + end + end +end \ No newline at end of file diff --git a/lib/cyberarm_engine/game_object.rb b/lib/cyberarm_engine/game_object.rb new file mode 100644 index 0000000..0bd675f --- /dev/null +++ b/lib/cyberarm_engine/game_object.rb @@ -0,0 +1,302 @@ +module CyberarmEngine + class GameObject + INSTANCES = [] + IMAGES = {} + SAMPLES= {} + Vertex = Struct.new(:x, :y) + attr_accessor :image, :x, :y, :z, :angle, :center_x, :center_y, :scale_x, :scale_y, + :color, :alpha, :mode, :options, :paused, :radius, :last_x, :last_y + attr_reader :world_center_point + def initialize(options={}) + if options[:auto_manage] || options[:auto_manage] == nil + INSTANCES.push(self) + $window.current_game_state.add_game_object(self) + end + + @options = options + @image = options[:image] ? image(options[:image]) : nil + @x = options[:x] ? options[:x] : 0 + @y = options[:y] ? options[:y] : 0 + @z = options[:z] ? options[:z] : 0 + @last_x = 0 + @last_y = 0 + @angle = options[:angle] ? options[:angle] : 0 + @center_x = options[:center_x] ? options[:center_x] : 0.5 + @center_y = options[:center_y] ? options[:center_y] : 0.5 + @scale_x = options[:scale_x] ? options[:scale_x] : 1 + @scale_y = options[:scale_y] ? options[:scale_y] : 1 + @color = options[:color] ? options[:color] : Gosu::Color.argb(0xff_ffffff) + @alpha = options[:alpha] ? options[:alpha] : 255 + @mode = options[:mode] ? options[:mode] : :default + @paused = false + @speed = 0 + @debug_color = Gosu::Color::GREEN + @world_center_point = Vertex.new(0,0) + setup + @debug_text = MultiLineText.new("", color: @debug_color, y: self.y-(self.height*self.scale), z: 9999) + @debug_text.x = self.x + if @radius == 0 || @radius == nil + @radius = options[:radius] ? options[:radius] : defined?(@image.width) ? ((@image.width+@image.height)/4)*scale : 1 + end + end + + def draw + if @image + @image.draw_rot(@x, @y, @z, @angle, @center_x, @center_y, @scale_x, @scale_y, @color, @mode) + end + + if $debug + show_debug_heading if $heading + $window.draw_circle(self.x, self.y, radius, 9999, @debug_color) + if @debug_text.text != "" + $window.draw_rect(@debug_text.x-10, (@debug_text.y-10), @debug_text.width+20, @debug_text.height+20, Gosu::Color.rgba(0,0,0,200), 9999) + @debug_text.draw + end + end + end + + def update + end + + def image(image_path) + image = nil + GameObject::IMAGES.detect do |img, instance| + if img == image_path + image = instance + true + end + end + + unless image + instance = Gosu::Image.new(image_path) + GameObject::IMAGES[image_path] = instance + image = instance + end + + return image + end + + def sample(sample_path) + sample = nil + GameObject::SAMPLES.detect do |smp, instance| + if smp == sample_path + sample = instance + true + end + end + + unless sample + instance = Gosu::Sample.new(sample_path) + GameObject::SAMPLES[sample_path] = instance + sample = instance + end + + return sample + end + + def debug_text(text) + @debug_text.text = text + end + + def update_debug_text + @debug_text.x = self.x-(@debug_text.width/2) + @debug_text.y = self.y-((self.height)*self.scale) + end + + def scale + if @scale_x == @scale_y + return @scale_x + else + false + # maths? + end + end + + def scale=(int) + self.scale_x = int + self.scale_y = int + self.radius = ((@image.width+@image.height)/4)*self.scale + end + + def x=(i) + @last_x = @x + @x = i + end + + def y=(i) + @last_y = @y + @y = i + end + + def visible + true + # if _x_visible + # if _y_visible + # true + # else + # false + # end + # else + # false + # end + end + + def _x_visible + self.x.between?(($window.width/2)-(@world_center_point.x), ($window.width/2)+@world_center_point.x) || + self.x.between?(((@world_center_point.x)-$window.width/2), ($window.width/2)+@world_center_point.x) + end + + def _y_visible + self.y.between?(($window.height/2)-(@world_center_point.y), ($window.height/2)+@world_center_point.y) || + self.y.between?((@world_center_point.y)-($window.height/2), ($window.height/2)+@world_center_point.y) + end + + def heading(ahead_by = 100, object = nil, angle_only = false) + direction = ((Gosu.angle(@last_x, @last_y, self.x, self.y)) - 90.0) * (Math::PI / 180.0) + ahead_by+object.speed*Engine.dt if object + _x = @x+(ahead_by*Math.cos(direction)) + _y = @y+(ahead_by*Math.sin(direction)) + return direction if angle_only + return Vertex.new(_x, _y) unless angle_only + end + + def show_debug_heading + _heading = heading + $window.draw_line(x, y, @debug_color, _heading.x, _heading.y, @debug_color, 9999) + end + + def width + @image ? @image.width : 0 + end + + def height + @image ? @image.height : 0 + end + + def pause + @paused = true + end + + def unpause + @paused = false + end + + def rotate(int) + self.angle+=int + self.angle%=360 + end + + def alpha=int # 0-255 + @alpha = int + @alpha = 255 if @alpha > 255 + @color = Gosu::Color.rgba(@color.red, @color.green, @color.blue, int) + end + + def draw_rect(x, y, width, height, color, z = 0) + $window.draw_rect(x,y,width,height,color,z) + end + + def button_up(id) + end + + def button_down?(id) + end + + def find_closest(game_object_class) + best_object = nil + best_distance = 100_000_000_000 # Huge default number + + game_object_class.all.each do |object| + distance = Gosu::distance(self.x, self.y, object.x, object.y) + if distance <= best_distance + best_object = object + best_distance = distance + end + end + + return best_object + end + + def look_at(object) + # TODO: Implement + end + + def circle_collision?(object) + distance = Gosu.distance(self.x, self.y, object.x, object.y) + if distance <= self.radius+object.radius + true + else + false + end + end + + # Duplication... so DRY. + def each_circle_collision(object, resolve_with = :width, &block) + if object.class != Class && object.instance_of?(object.class) + $window.current_game_state.game_objects.select {|i| i.class == object.class}.each do |o| + distance = Gosu.distance(self.x, self.y, object.x, object.y) + if distance <= self.radius+object.radius + block.call(o, object) if block + end + end + else + list = $window.current_game_state.game_objects.select {|i| i.class == object} + list.each do |o| + next if self == o + distance = Gosu.distance(self.x, self.y, o.x, o.y) + if distance <= self.radius+o.radius + block.call(self, o) if block + end + end + end + end + + def destroy + INSTANCES.delete(self) + if $window.current_game_state + $window.current_game_state.game_objects.each do |o| + if o.is_a?(self.class) && o == self + $window.current_game_state.game_objects.delete(o) + end + end + end + end + + # NOTE: This could be implemented more reliably + def self.all + INSTANCES.select {|i| i.class == self} + end + + def self.each_circle_collision(object, resolve_with = :width, &block) + if object.class != Class && object.instance_of?(object.class) + $window.current_game_state.game_objects.select {|i| i.class == self}.each do |o| + distance = Gosu.distance(o.x, o.y, object.x, object.y) + if distance <= o.radius+object.radius + block.call(o, object) if block + end + end + else + lista = $window.current_game_state.game_objects.select {|i| i.class == self} + listb = $window.current_game_state.game_objects.select {|i| i.class == object} + lista.product(listb).each do |o, o2| + next if o == o2 + distance = Gosu.distance(o.x, o.y, o2.x, o2.y) + if distance <= o.radius+o2.radius + block.call(o, o2) if block + end + end + end + end + + def self.destroy_all + INSTANCES.clear + if $window.current_game_state + $window.current_game_state.game_objects.each do |o| + if o.is_a?(self.class) + $window.current_game_state.game_objects.delete(o) + end + end + end + end + end +end \ No newline at end of file diff --git a/lib/cyberarm_engine/game_state.rb b/lib/cyberarm_engine/game_state.rb new file mode 100644 index 0000000..e01436f --- /dev/null +++ b/lib/cyberarm_engine/game_state.rb @@ -0,0 +1,64 @@ +class GameState + SCALE_X_BASE = 1920.0 + SCALE_Y_BASE = 1080.0 + attr_accessor :options, :global_pause + attr_reader :game_objects + + def initialize(options={}) + $window.set_game_state(self) + @options = options unless @options + @game_objects = [] + @global_pause = false + + setup + @_4ship = Ship.all.first if Ship.all.is_a?(Array) + end + + def setup + end + + def draw + # count = 0 + @game_objects.each do |o| + o.draw if o.visible + # p o.class if o.visible + # count+=1 if o.visible + end + # puts "Num visible objects: #{count} of #{@game_objects.count}" + end + + def update + @game_objects.each do |o| + unless o.paused || @global_pause + o.world_center_point.x = @_4ship.x + o.world_center_point.y = @_4ship.y + + o.update + o.update_debug_text if $debug + end + end + end + + def destroy + @options = nil + @game_objects = nil + end + + def button_up(id) + @game_objects.each do |o| + o.button_up(id) unless o.paused + end + end + + def push_game_state(klass, options={}) + $window.push_game_state(klass, options) + end + + def draw_rect(x, y, width, height, color, z = 0) + $window.draw_rect(x,y,width,height,color,z) + end + + def add_game_object(object) + @game_objects << object + end +end diff --git a/lib/cyberarm_engine/objects/multi_line_text.rb b/lib/cyberarm_engine/objects/multi_line_text.rb new file mode 100644 index 0000000..f1252b1 --- /dev/null +++ b/lib/cyberarm_engine/objects/multi_line_text.rb @@ -0,0 +1,59 @@ +module CyberarmEngine + class MultiLineText + attr_accessor :options, :x, :y, :width, :height + + def initialize(text, options={}) + @texts = [] + text.split("\n").each_with_index do |line, i| + _options = options + _options[:y]+=_options[:size] + @texts << Text.new(line, _options) + end + @options = options + @x = @texts.first ? @texts.first.x : 0 + @y = @texts.first ? @texts.first.y : 0 + @width = 0 + @height = 0 + calculate_boundry + end + + def draw + @texts.each(&:draw) + end + + def text + string = "" + @texts.each {|t| string << t.text} + return string + end + + def text=(text) + text.split("\n").each_with_index do |line, i| + if @texts[i] + @texts[i].text = line + else + @texts << Text.new(line, @options) + end + end + + calculate_boundry + end + + def x=(int) + @x = int + @texts.each {|t| t.x = int} + end + + def y=(int) + @y = int + @texts.each_with_index {|t, i| t.y=int+(i*t.size)} + end + + def calculate_boundry + @width = 0 + @height= 0 + @texts.each {|t| @width = t.width if t.width > @width} + @texts.each {|t| @height+=t.height} + end + end +end \ No newline at end of file diff --git a/lib/cyberarm_engine/objects/text.rb b/lib/cyberarm_engine/objects/text.rb new file mode 100644 index 0000000..4741362 --- /dev/null +++ b/lib/cyberarm_engine/objects/text.rb @@ -0,0 +1,95 @@ +module CyberarmEngine + class Text + CACHE = {} + + attr_accessor :text, :x, :y, :z, :size, :factor_x, :factor_y, :color, :shadow, :shadow_size, :options + attr_reader :textobject + + def initialize(text, options={}) + @text = text || "" + @options = options + @size = options[:size] || 18 + @font = options[:font] || Gosu.default_font_name + @x = options[:x] || 0 + @y = options[:y] || 0 + @z = options[:z] || 1025 + @factor_x = options[:factor_x] || 1 + @factor_y = options[:factor_y] || 1 + @color = options[:color] || Gosu::Color::WHITE + @alignment= options[:alignment] || nil + @shadow = true if options[:shadow] == true + @shadow = false if options[:shadow] == false + @shadow = true if options[:shadow] == nil + @shadow_size = options[:shadow_size] ? options[:shadow_size] : 1 + @shadow_alpha= options[:shadow_alpha] ? options[:shadow_alpha] : 30 + @textobject = check_cache(@size, @font) + + if @alignment + case @alignment + when :left + @x = 0+BUTTON_PADDING + when :center + @x = ($window.width/2)-(@textobject.text_width(@text)/2) + when :right + @x = $window.width-BUTTON_PADDING-@textobject.text_width(@text) + end + end + + return self + end + + def check_cache(size, font_name) + available = false + font = nil + + if CACHE[size] + if CACHE[size][font_name] + font = CACHE[size][font_name] + available = true + else + available = false + end + else + available = false + end + + unless available + font = Gosu::Font.new(@size, name: @font) + CACHE[@size] = {} unless CACHE[@size].is_a?(Hash) + CACHE[@size][@font] = font + end + + return font + end + + def width + textobject.text_width(@text) + end + + def height + textobject.height + end + + def draw + if @shadow && !ARGV.join.include?("--no-shadow") + _color = Gosu::Color.rgba(@color.red, @color.green, @color.blue, @shadow_alpha) if @shadow_alpha <= @color.alpha + _color = Gosu::Color.rgba(@color.red, @color.green, @color.blue, @color.alpha) unless @shadow_alpha <= @color.alpha + @textobject.draw(@text, @x-@shadow_size, @y, @z, @factor_x, @factor_y, _color) + @textobject.draw(@text, @x-@shadow_size, @y-@shadow_size, @z, @factor_x, @factor_y, _color) + + @textobject.draw(@text, @x, @y-@shadow_size, @z, @factor_x, @factor_y, _color) + @textobject.draw(@text, @x+@shadow_size, @y-@shadow_size, @z, @factor_x, @factor_y, _color) + + @textobject.draw(@text, @x, @y+@shadow_size, @z, @factor_x, @factor_y, _color) + @textobject.draw(@text, @x-@shadow_size, @y+@shadow_size, @z, @factor_x, @factor_y, _color) + + @textobject.draw(@text, @x+@shadow_size, @y, @z, @factor_x, @factor_y, _color) + @textobject.draw(@text, @x+@shadow_size, @y+@shadow_size, @z, @factor_x, @factor_y, _color) + end + + @textobject.draw(@text, @x, @y, @z, @factor_x, @factor_y, @color) + end + + def update; end + end +end \ No newline at end of file diff --git a/lib/cyberarm_engine/ui/button.rb b/lib/cyberarm_engine/ui/button.rb new file mode 100644 index 0000000..1b3d534 --- /dev/null +++ b/lib/cyberarm_engine/ui/button.rb @@ -0,0 +1,122 @@ +module CyberarmEngine + BUTTON_TEXT_COLOR = Gosu::Color::WHITE + BUTTON_TEXT_ACTIVE_COLOR = Gosu::Color::BLACK + BUTTON_COLOR = Gosu::Color.rgb(12,12,12) + BUTTON_HOVER_COLOR = Gosu::Color.rgb(100, 100, 100) + BUTTON_ACTIVE_COLOR = Gosu::Color.rgb(50, 50, 50) + BUTTON_TEXT_SIZE = 20 + BUTTON_PADDING = 10 + + class Button + attr_accessor :text, :x, :y, :offset_x, :offset_y, :tooltip, :block + + def initialize(text, x, y, auto_manage = true, tooltip = "", &block) + @text = Text.new(text, x: x, y: y, size: BUTTON_TEXT_SIZE, color: BUTTON_TEXT_COLOR, shadow: true) + @tooltip=Text.new(tooltip, x: x, y: y-(height/4*3), z: 10_000, size: BUTTON_TEXT_SIZE, color: BUTTON_TEXT_COLOR, shadow: false) + @x = x + @y = y + _x_ = @x+(@text.textobject.text_width(@text.text)/2)-(@tooltip.textobject.text_width(@tooltip.text)/2) + @tooltip.x = _x_+BUTTON_PADDING + auto_adjust_tooltip_position + @offset_x, @offset_y = 0, 0 + if block + @block = Proc.new{yield(self)} + else + @block = Proc.new {} + end + + Window.instance.elements.push(self) if auto_manage + + return self + end + + def update_position_toolip + _x_ = @x+(@text.textobject.text_width(@text.text)/2)-(@tooltip.textobject.text_width(@tooltip.text)/2) + @tooltip.x = _x_+BUTTON_PADDING + auto_adjust_tooltip_position + end + + def auto_adjust_tooltip_position + if @tooltip.x <= 1 + @tooltip.x = 2 + elsif @tooltip.x+@tooltip.textobject.text_width(@tooltip.text) > $window.width-(BUTTON_PADDING+1) + @tooltip.x = $window.width-@tooltip.textobject.text_width(@tooltip.text) + end + end + + def draw + @text.draw + + $window.draw_rect(@x, @y, width, height, BUTTON_COLOR) + + if mouse_clicked_on_check + $window.draw_rect(@x+1, @y+1, width-2, height-2, BUTTON_ACTIVE_COLOR) + elsif mouse_over? + $window.draw_rect(@x+1, @y+1, width-2, height-2, BUTTON_HOVER_COLOR) + show_tooltip + else + $window.draw_rect(@x+1, @y+1, width-2, height-2, BUTTON_COLOR) + end + + end + + def update + @text.x = @x+BUTTON_PADDING + @text.y = @y+BUTTON_PADDING + end + + def button_up(id) + case id + when Gosu::MsLeft + click_check + end + end + + def click_check + if mouse_over? + puts "Clicked: #{@text.text}" + @block.call if @block.is_a?(Proc) + end + end + + def mouse_clicked_on_check + if mouse_over? && Gosu.button_down?(Gosu::MsLeft) + true + end + end + + def mouse_over? + if $window.mouse_x.between?(@x+@offset_x, @x+@offset_x+width) + if $window.mouse_y.between?(@y+@offset_y, @y+@offset_y+height) + true + end + end + end + + def show_tooltip + if @tooltip.text != "" + x = @tooltip.x-BUTTON_PADDING + + $window.draw_rect(x, @y-height, width(@tooltip), height(@tooltip), BUTTON_ACTIVE_COLOR, 9_999) + $window.draw_rect(x-1, @y-height-1, width(@tooltip)+2, height(@tooltip)+2, Gosu::Color::WHITE, 9_998) + @tooltip.draw + end + end + + def width(text_object = @text) + text_object.textobject.text_width(text_object.text)+BUTTON_PADDING*2 + end + + def height(text_object = @text) + text_object.textobject.height+BUTTON_PADDING*2 + end + + def set_offset(x, y) + @offset_x, @offset_y = x, y + end + + def update_text(string) + @text.text = string + end + end +end \ No newline at end of file diff --git a/lib/cyberarm_engine/ui/check_box.rb b/lib/cyberarm_engine/ui/check_box.rb new file mode 100644 index 0000000..5c34d6d --- /dev/null +++ b/lib/cyberarm_engine/ui/check_box.rb @@ -0,0 +1,73 @@ +module CyberarmEngine + class CheckBox + SIZE = 22 + + attr_accessor :x, :y, :checked + attr_reader :text + + def initialize(x, y, checked = false, size = CheckBox::SIZE) + @x, @y = x, y + @checked = checked + @size = size + @text = Text.new("✔", false, x: x, y: y, size: size, color: BUTTON_TEXT_COLOR, shadow: true) + return self + end + + def x=(int) + @x = int + @text.x = int + end + + def y=(int) + @y = int + @text.y = int + end + + def draw + $window.draw_rect(@x, @y, width, height, Gosu::Color::BLACK) + if mouse_over? + $window.draw_rect(@x+1, @y+1, width-2, height-2, BUTTON_HOVER_COLOR) + else + if @checked + $window.draw_rect(@x+1, @y+1, width-2, height-2, BUTTON_ACTIVE_COLOR) + else + $window.draw_rect(@x+1, @y+1, width-2, height-2, BUTTON_COLOR) + end + end + if @checked + @text.draw + end + end + + def update + @text.x = @x+BUTTON_PADDING + @text.y = @y+BUTTON_PADDING + end + + def button_up(id) + if mouse_over? && id == Gosu::MsLeft + if @checked + @checked = false + else + @checked = true + end + end + end + + def mouse_over? + if $window.mouse.x.between?(@x, @x+width) + if $window.mouse.y.between?(@y, @y+height) + true + end + end + end + + def width(text_object = @text) + text_object.textobject.text_width(text_object.text)+BUTTON_PADDING*2 + end + + def height(text_object = @text) + text_object.textobject.height+BUTTON_PADDING*2 + end + end +end \ No newline at end of file diff --git a/lib/cyberarm_engine/ui/container.rb b/lib/cyberarm_engine/ui/container.rb new file mode 100644 index 0000000..edd4916 --- /dev/null +++ b/lib/cyberarm_engine/ui/container.rb @@ -0,0 +1,157 @@ +module CyberarmEngine + class Container + attr_accessor :text_color + attr_reader :elements, :x, :y, :width, :height, :options + attr_reader :scroll_x, :scroll_y, :internal_width, :internal_height + + def initialize(x = 0, y = 100, width = $window.width, height = $window.height, options = {}) + @x, @y, @width, @height, @internal_width, @internal_height = x, y, width, height-y, width, height-y + @scroll_x, @scroll_y = 0, 0 + @scroll_speed = 10 + puts "#{self.class}: width #{width}, height #{@height}" + + @options = {} + @allow_recreation_on_resize = true + @text_color = options[:text_color] || Gosu::Color::WHITE + @elements = [] + + if defined?(self.setup); setup; end + end + + def draw + Gosu.clip_to(x, y, width, height) do + Gosu.translate(scroll_x, scroll_y) do + @elements.each(&:draw) + end + end + end + + def update + @elements.each(&:update) + end + + def button_up(id) + if $window.mouse_x.between?(@x, @x+@width) + if $window.mouse_y.between?(@y, @y+@height) + case id + when Gosu::MsWheelUp + @scroll_y+=@scroll_speed + @scroll_y = 0 if @scroll_y > 0 + @elements.each {|e| e.set_offset(@scroll_x, @scroll_y) if e.is_a?(Button) } + when Gosu::MsWheelDown + @scroll_y-=@scroll_speed + if $window.height-@internal_height-y > 0 + @scroll_y = 0 + p "H: #{@height-@internal_height}", "Y: #{@scroll_y}" + else + @scroll_y = @height-@internal_height if @scroll_y <= @height-@internal_height + end + + @elements.each {|e| e.set_offset(@scroll_x, @scroll_y) if e.is_a?(Button) } + end + end + end + + @elements.each {|e| if defined?(e.button_up); e.button_up(id); end} + end + + def build(&block) + yield(self) + end + + def text(text, x, y, size = 18, color = self.text_color, alignment = nil, font = nil) + relative_x = @x+x + relative_y = @y+y + _text = Text.new(text, x: relative_x, y: relative_y, size: size, color: color, alignment: alignment, font: font) + @elements.push(_text) + if _text.y-(_text.height*2) > @internal_height + @internal_height+=_text.height + end + + return _text + end + + def button(text, x, y, tooltip = "", &block) + relative_x = @x+x + relative_y = @y+y + _button = Button.new(text, relative_x, relative_y, false, tooltip) { if block.is_a?(Proc); block.call; end } + @elements.push(_button) + if _button.y-(_button.height*2) > @internal_height + @internal_height+=_button.height + end + + return _button + end + + def input(text, x, y, width = Input::WIDTH, size = 18, color = Gosu::Color::BLACK, tooltip = "") + relative_x = @x+x + relative_y = @y+y + _input = Input.new(text, relative_x, relative_y, width, size, color) + @elements.push(_input) + if _input.y-(_input.height*2) > @internal_height + @internal_height+=_input.height + end + + return _input + end + + def check_box(x, y, checked = false, size = CheckBox::SIZE) + relative_x = @x+x + relative_y = @y+y + _check_box = CheckBox.new(relative_x, relative_y, checked, size) + @elements.push(_check_box) + if _check_box.y-(_check_box.height*2) > @internal_height + @internal_height+=_check_box.height + end + + return _check_box + end + + def resize + if @allow_recreation_on_resize + $window.active_container = self.class.new + end + end + + # Fills container background with color + def fill(color = Gosu::Color::BLACK, z = 0) + $window.draw_rect(@x, @y, @width, @height, color, z) + end + + def set_layout_y(start, spacing) + @layout_y_start = start + @layout_y_spacing = spacing + @layout_y_count = 0 + end + + def layout_y(stay = false) + i = @layout_y_start+(@layout_y_spacing*@layout_y_count) + @layout_y_count+=1 unless stay + return i + end + + # Return X position relative to container + def relative_x(int) + int-self.x + end + + # Return Y position relative to container + def relative_y(int) + int-self.y + end + + def calc_percentage(positive, total) + begin + i = ((positive.to_f/total.to_f)*100.0).round(2) + if !i.nan? + return "#{i}%" + else + "N/A" + end + rescue ZeroDivisionError => e + puts e + return "N/A" # 0 / 0, safe to assume no actionable data + end + end + end +end \ No newline at end of file diff --git a/lib/cyberarm_engine/ui/input.rb b/lib/cyberarm_engine/ui/input.rb new file mode 100644 index 0000000..6d3ec06 --- /dev/null +++ b/lib/cyberarm_engine/ui/input.rb @@ -0,0 +1,117 @@ +module CyberarmEngine + class Input + WIDTH = 200 + FOCUS_BACKGROUND_COLOR = Gosu::Color.rgb(150,150,144) + NO_FOCUS_BACKGROUND_COLOR = Gosu::Color.rgb(130,130,130) + + attr_accessor :text, :x, :y, :width, :size, :color, :type, :focus + attr_reader :text_object, :text_input, :height, :fixed_x + + def initialize(text, x, y, width = WIDTH, size = Text::SIZE, color = Gosu::Color::BLACK, tooltip = "", type = nil) + @text = text + @x, @y= x, y + @width= width + @size = size + @color= color + @tooltip=tooltip + @type = type + + @focus = false + + @text_object = Text.new(text, x: x, y: y, size: size, color: color, shadow: true) + @height = @text_object.height + @text_input = Gosu::TextInput.new + @text_input.text = @text + + @background_color = NO_FOCUS_BACKGROUND_COLOR + @fixed_x = @x + @x_offset= 0 + + @carot_ticks = 0 + @carot_width = 2.5 + @carot_height= @text_object.height + @carot_color = Gosu::Color.rgb(50,50,25) + @carot_show_ticks = 25 + @show_carot = true + + return self + end + + def text=(string) + @text = string + @text_input.text, @text_object.text = @text, @text + end + + def draw + $window.draw_rect(x, y, width, height, Gosu::Color::BLACK) + $window.draw_rect(x+1, y+1, width-2, height-2, @background_color) + Gosu.clip_to(x, @text_object.y, width, @text_object.height) do + @text_object.draw + + # Carot (Cursor) + $window.draw_rect((@x+@text_object.width)-@x_offset, @text_object.y, @carot_width, @carot_height, @carot_color) if @show_carot && @focus + end + + end + + def update + @text_object.y = @y+BUTTON_PADDING + + if (@text_object.width+@carot_width)-@width >= 0 + @x_offset = (@text_object.width+@carot_width)-@width + else + @x_offset = 0 + end + + @text = @text_object.text + @carot_ticks+=1 + if @carot_ticks >= @carot_show_ticks + if @show_carot + @show_carot = false + else + @show_carot = true + end + + @carot_ticks = 0 + end + + if @focus + @text_object.text = @text_input.text + $window.text_input = @text_input unless $window.text_input == @text_input + end + + if mouse_over? && $window.button_down?(Gosu::MsLeft) + @focus = true + @background_color = FOCUS_BACKGROUND_COLOR + end + if !mouse_over? && $window.button_down?(Gosu::MsLeft) + @focus = false + $window.text_input = nil + @background_color = NO_FOCUS_BACKGROUND_COLOR + end + + if @text_object.width >= @width + @text_object.x = self.fixed_x-@x_offset + else + @text_object.x = self.fixed_x + end + end + + def mouse_over? + if $window.mouse_x.between?(@x, @x+width) + if $window.mouse_y.between?(@y, @y+height) + true + end + end + end + + def width(text_object = @text_object) + # text_object.textobject.text_width(text_object.text)+BUTTON_PADDING*2 + @width + end + + def height(text_object = @text_object) + text_object.textobject.height+BUTTON_PADDING*2 + end + end +end \ No newline at end of file diff --git a/lib/cyberarm_engine/version.rb b/lib/cyberarm_engine/version.rb new file mode 100644 index 0000000..ba40af5 --- /dev/null +++ b/lib/cyberarm_engine/version.rb @@ -0,0 +1,3 @@ +module CyberarmEngine + VERSION = "0.1.0" +end diff --git a/test/cyberarm_engine_test.rb b/test/cyberarm_engine_test.rb new file mode 100644 index 0000000..cdce3e2 --- /dev/null +++ b/test/cyberarm_engine_test.rb @@ -0,0 +1,11 @@ +require "test_helper" + +class CyberarmEngineTest < Minitest::Test + def test_that_it_has_a_version_number + refute_nil ::CyberarmEngine::VERSION + end + + def test_it_does_something_useful + assert false + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb new file mode 100644 index 0000000..e94a2eb --- /dev/null +++ b/test/test_helper.rb @@ -0,0 +1,4 @@ +$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__) +require "cyberarm_engine" + +require "minitest/autorun"