Hmm, the vectorized version of TankSpank now fails with:
<<inline: Picture 1.png>>
Any idea what happened? require 'matrix' ... BOUNDS = Vector[1500.0, 1250.0] SCREEN_SIZE = Vector[700.0, 500.0] ... 263: center = Vector[(east+west)/2.0, (south+north)/2.0] 264: size = Vector[(east-west)/2.0, (south-north)/2.0] -enp
# Tankspank version 5, 11-April-2008 # # This code is hereby released into the public domain # http://creativecommons.org/licenses/publicdomain/ # # Originally by kevin conner <[EMAIL PROTECTED]> # # Updated by Ernest Prabhakar <[EMAIL PROTECTED]> # Notable Features: # * Abstract classes "Shape" for static objects, "Sprite" for moving objects # * Damage, drag both calculated dynamically based on size # * Converts between "game" and "screen" coordinates using moving Camera # * Use isometric projection (draw_box) for 2.5D rendering # * Miniature "Radar" display to help locate enemy tanks # * AppTest proxy object allows sanity-checking outside Shoes runtime require 'matrix' require 'logger' # # Constants # $debug = true $debug2 = false $log = Logger.new(STDERR) #$log.level = Logger::DEBUG X=0 Y=1 BOUNDS = Vector[1500.0, 1250.0] SCREEN_SIZE = Vector[700.0, 500.0] SCREEN_ORIGIN = SCREEN_SIZE * 0.5 Z_SCALE = 600.0 RADAR_SCALE = 0.05 FRAMES_PER_SECOND=10 EXPLODE_RATE = 3.0/FRAMES_PER_SECOND SHELL_SPEED = 20 SHELL_SIZE = Vector[2.0,2.0] TANK_SIZE = Vector[10.0,15.0] MUZZLE_SIZE = Vector[25.0,0.0] AIM_RADIUS = 10 DEST_RADIUS = 20 LIGHT_RADIUS = 3 TIGHTNESS = 0.1 IMPULSE = 10.0/FRAMES_PER_SECOND DRAG_MAX = 0.20 N_ENEMIES = 5 # # Extensions to built-in Ruby classes # class Matrix def self.rotation(angle) cos_a, sin_a = Math.cos(angle), Math.sin(angle) Matrix[ [cos_a, -sin_a], [sin_a, cos_a] ] end def self.reflection; Matrix[ [1,0], [0, -1] ]; end end class Array def to_s return self.inspect if not self[0].is_a? Float inject("[") {|p,q| p += sprintf(" %.1f",q)} + "]" end end class Vector # def abs; [EMAIL PROTECTED], @elements[Y].abs]; end def rotateBy radians; Matrix.rotation(radians) * self; end def reflection; [EMAIL PROTECTED], [EMAIL PROTECTED]; end def reflectBy radians; reflection.rotateBy(radians); end def angle; Math.atan2(@elements[Y], @elements[X]); end def ratio; @elements[Y] / @elements[X]; end def area; @elements.inject(4) {|x,y| x*y}; end def within?(size) each2(size) { |self_i, size_i| return false if self_i.abs > size_i } true end def to_s; to_a.to_s; end end def bound(x, high) return -high if x < -high return high if x > high x end def angle_bound(t) return t + 2*Math::PI if t < -Math::PI return t - 2*Math::PI if t > Math::PI t end # # Mixin Modules # module Drawable def self.rand_color color = (1..3).collect { 0.2 + 0.4 * rand } color << 1.0 $app.rgb *color end def draw_oval; $app.oval *base_extent; end def draw_rect; $app.rect *base_extent; end def draw_rect_with_corners screen_corners base = screen_corners[2] extent = screen_corners[0] - screen_corners[2] $app.rect *(base.to_a + extent.to_a) end def draw_poly(points) points.inject(points[-1]) do |p, q| $app.line *(p.to_a + q.to_a); q end end def draw_outline points = screen_corners() draw_poly(points) end def draw_along(vector) p = screen_coord() q = p + vector $app.line *(p.to_a + q.to_a) end def draw_box bottom = screen_corners() top = screen_corners(@height) $log.debug "#{$camera}" if $debug2 $log.debug "\nbottom| #{bottom.to_s}\n top| #{top.to_s}" if $debug2 draw_outline 4.times { |i| $app.line *(bottom[i].to_a + top[i].to_a) } draw_rect_with_corners top end def draw $log.debug "\nDrawing #{self}" if $debug $app.stroke @stroke_color $app.fill @fill_color draw_content end def draw_content; draw_rect; end end module Hurtable attr_reader :health def reset_health; @health = mass(); end def strength; @health / mass(); end def dead?; @health <= 0; end def hurt damage; @health -= damage; end def encounter obstacle # Sprites only damage = [energy(), obstacle.energy()].max hurt damage obstacle.hurt damage collide(obstacle) if mass < obstacle.mass end end # # Static "Shapes" # class Shape include Drawable include Hurtable attr_accessor :center, :size, :stroke_color, :fill_color def initialize(center, size) @center = center.clone() @size = size @height = @size.r * (0.25 + rand * 1.5) @dz = 1 + (2/Math::PI) * Math.atan(@height / Z_SCALE) @stroke_color = @fill_color = Drawable::rand_color reset_health() end def to_s s = "#{self.class}:[EMAIL PROTECTED]@size.to_s}" s += sprintf(" x %.0f | ", @height) s += "fill: [EMAIL PROTECTED] stroke: [EMAIL PROTECTED]" end def front; @height * @size[X]; end # def volume; @height * @size.area; end def mass; @size.area; end def energy; 0; end def screen_coord(dz=1); SCREEN_ORIGIN + (@center - $camera.center) * dz; end def base_extent; (screen_coord - @size).to_a + (@size * 2).to_a; end def native_base_extent; (@center - @size).to_a + (@size * 2).to_a; end # Center +- sizes/ reflected size def cornerize(ctr, s, sR); [ctr+s, ctr-sR, ctr-s, ctr+sR]; end def corners; cornerize(@center, @size, @size.reflection); end def back_corners; c = corners(); [c[1],c[2]]; end def faces; cornerize(@center, [EMAIL PROTECTED],0], Vector[0,@size[Y]]); end def screen_corners(height = 0) dz = (0 == height) ? 1 : @dz ctr = screen_coord(dz) psize = @size * dz cornerize(ctr, psize, psize.reflection) end # rectangular def contain? point; (point - @center).within? @size; end def intersect? shape self.corners.any? { |point| shape.contain? point } or shape.corners.any? { |point| self.contain? point } end end class Building < Shape def initialize(west, east, north, south) center = Vector[(east+west)/2.0, (south+north)/2.0] size = Vector[(east-west)/2.0, (south-north)/2.0] super(center, size) end def draw_content; draw_box; end end class Boundary < Building def initialize(west, east, north, south) super @fill_color = $app.black @stroke_color = $app.silver @height = Z_SCALE/2 end end class Circle < Shape def initialize(center, radius, color) size = Vector[radius, radius] super(center, size) @stroke_color = color @fill_color = color end def draw_content; draw_oval; end def corners; faces(); end def contain? point; (point - @center).r < @size.r; end end # # Moving "Sprites" # class Sprite < Shape attr_reader :facing, :goal def turn_radius; 2.0 * @size[Y]; end def drag; [DRAG_MAX, @size[Y]/100].min; end def initialize(center, size) super @last = @center @facing = @acceleration = @speed = 0.0 @turn_max = 1.0/turn_radius @decay = 1 - drag @goal = nil @goal_color = $app.green end def set_goal pos, radius @goal = Circle.new pos, radius, @goal_color @goal.stroke_color = @stroke_color end def energy; @speed**2; end def turn return if @goal.nil? delta = @goal.center - @center turn_by = angle_bound(delta.angle - @facing) @facing += bound(turn_by, @turn_max) @acceleration = Math.cos(turn_by) * IMPULSE * strength() end def move @speed += @acceleration @speed *= @decay @last = @center @center += Vector[1,0].rotateBy(@facing) * @speed end def arrive @acceleration = 0 @goal = nil end def collide obstacle $log.debug "Stopping: #{self}" arrive @center = @last @speed = 0 end def at_goal?; not @goal.nil? and self.intersect?(@goal); end def update time; turn(); move(); arrive if at_goal?; nil; end def draw_content; draw_oval; end end class Camera < Sprite def self.rand_within(x); rand(2*x) - x; end def self.game_pos Vector[rand_within(BOUNDS[X]), rand_within(BOUNDS[Y])] end def self.screen_pos Vector[rand_within(SCREEN_ORIGIN[X]), rand_within(SCREEN_ORIGIN[Y])] end def self.origin $camera.center end def initialize to_follow super to_follow.center, SCREEN_ORIGIN @goal = to_follow end def update time; @center += (@goal.center - @center) * TIGHTNESS; nil; end def game_coord(screen_x, screen_y) Vector[screen_x,screen_y] - SCREEN_ORIGIN + @center end end class Explosion < Sprite def initialize shape super(shape.center, shape.size*2) @r_min = @size.r / 4.0 @fill_color = $app.orange @stroke_color = shape.stroke_color end def update time @size = @size * (1-EXPLODE_RATE) nil end def dead?; @size.r < @r_min ; end end class Shell < Sprite def initialize position, angle position += MUZZLE_SIZE.rotateBy(angle) super(position, SHELL_SIZE) @facing = angle @speed = SHELL_SPEED @fill_color = @stroke_color = $app.red end def dead?; @speed < 1; end end class Turret < Sprite def reset_goal; set_goal(Vector[0,0], AIM_RADIUS); end def initialize(center, size) super @goal_color = $app.red reset_goal end def update_goal pos reset_goal if @goal.nil? @goal.center = pos @goal.stroke_color = @stroke_color end def alpha= level; nil; end def draw_content super muzzle = MUZZLE_SIZE.rotateBy(@facing) draw_along(muzzle) end end class TankCore < Sprite def initialize(center, color) super(center, TANK_SIZE) @turret = Turret.new(center, TANK_SIZE*0.5) @stroke_color = @turret.stroke_color = color @taillights = back_corners().inject([]) do |l,c| l << Circle.new(center, LIGHT_RADIUS, $app.darkred) end @will_fire = false @firing_cycle = FRAMES_PER_SECOND end def move_to pos; set_goal(pos, DEST_RADIUS); end def aim_at pos; @turret.update_goal pos; end def health_color; :silver; end def hurt damage super @turret.fill_color = $app.send(health_color(), strength()) end def update time super time @turret.turn() @turret.center = @center back_corners().each_with_index {|fc, i| @taillights[i].center = fc} if @will_fire and 0 == time % @firing_cycle @will_fire = false return Shell.new(@center, @turret.facing) else return nil end end def corners cornerize(@center, @size.rotateBy(@facing), @size.reflectBy(@facing)) end def screen_corners cornerize(screen_coord(), @size.rotateBy(@facing), @size.reflectBy(@facing)) end def draw_content draw_outline @turret.draw @taillights.each {|b| b.draw} end end class TankPlayer < TankCore def initialize center super center, $app.blue hurt 0 end def health_color; :green; end def fire; @will_fire = true; end def draw_content super @goal.draw if not @goal.nil? @turret.goal.draw if not @turret.goal.nil? end end class TankBot < TankCore def initialize center super center, Drawable::rand_color @firing_cycle = FRAMES_PER_SECOND * N_ENEMIES aim_at Camera::origin() hurt 0 end def health_color; :red; end # bounce away! def collide obstacle arrive @center = @last @speed += SHELL_SPEED outbound = @center - obstacle.center @facing = outbound.angle end def update time move_to Camera::screen_pos() if @goal.nil? aim_at Camera::origin() @will_fire = true super time end end # # Game Management # class Radar < Shape def initialize(tanks) size = BOUNDS * RADAR_SCALE offset = SCREEN_SIZE - size super offset, size @stroke_color = $app.silver @fill_color = $app.black @tanks = tanks @extent = [4,4] # view_size = SCREEN_ORIGIN * RADAR_SCALE end def radar_coord pos; @center + pos * RADAR_SCALE; end def draw_content $app.rect *native_base_extent @tanks.each do |tank| $app.stroke $app.black $app.fill tank.stroke_color $app.rect *(radar_coord(tank.center).to_a + @extent) if not tank.dead? end end end class TankSpank attr :time def initialize @time = 0 @boundary = Boundary.new(-BOUNDS[X], BOUNDS[X], -BOUNDS[Y], BOUNDS[Y]) @shapes = [ [-1000, -750, -750, -250], [-500, 250, -750, -250], [500, 1000, -750, -500], [750, 1250, -250, 0], [750, 1250, 250, 750], [250, 500, 0, 750], [-250, 0, 0, 500], [-500, 0, 750, 1000], [-1000, -500, 0, 500], [400, 600, -350, -150] ].collect { |p| Building.new *p } @tank = TankPlayer.new(SCREEN_ORIGIN + Vector[-150,-350]) @shapes << @tank $camera = Camera.new(@tank) N_ENEMIES.times do p = Camera::game_pos() redo if @shapes.any? {|s| s.contain? p} @shapes << TankBot.new(p) end @radar = Radar.new tanks end def tanks; sprites = @shapes.select {|s| s.is_a? TankCore}; end def enemies; sprites = @shapes.select {|s| s.is_a? TankBot}; end def playing?; not @tank.dead? and enemies.size > 0; end def status s = "" s += sprintf "Time: %5.2f ", @time*1.0/FRAMES_PER_SECOND s += sprintf "⢠Strength: %.0f%% ", @tank.strength * 100 # s += sprintf "â¢Â Position: %s ", @tank.center s += sprintf "⢠Enemies: %d ", enemies.size end def fire; @tank.fire; end def follow x,y; @tank.move_to $camera.game_coord(x,y); end def check shape if shape.dead? and not shape.is_a? Explosion @shapes.delete shape @shapes << Explosion.new(shape) $log.debug "Exploded: #{shape}" end end def update x,y @tank.aim_at $camera.game_coord(x,y) @time += 1 sprites = @shapes.select {|s| s.is_a? Sprite} visible_objects = @shapes.select {|s| s.intersect? $camera} sprites.each do |sprite| result = sprite.update @time @shapes << result if not result.nil? obstacles = @shapes.find_all {|s| s.intersect? sprite and s != sprite } obstacles.each do |obstacle| $log.debug "#{sprite} vs.\n\t#{obstacle}\n" if $debug sprite.encounter obstacle check sprite check obstacle end @shapes.delete sprite if sprite.dead? # in case died of natural causes end end def draw $log.debug "[EMAIL PROTECTED] @ #{$camera}" if $debug $app.clear do $camera.update @time @boundary.draw @shapes.each { |s| s.draw if s.is_a? Shape and $camera.intersect? s } end @radar.draw end end # # Test Helpers for when NOT running inside Shoes # class AppTest def self.app(param); yield; end def self.method_missing sym, *args s = args.collect{|x|sprintf(" %.0f",x)} $log << "\t#{sym}: #{s}\n" if $debug end def self.black (*args); [1]; end def self.red (*args); [1]; end def self.green (*args); [1]; end def self.blue (*args); [1]; end def self.cyan (*args); [1]; end def self.yellow (*args); [1]; end def self.rgb (*args); args; end def self.stroke (*args); end def self.fill (*args); end def self.clear; yield; end end def stack; end; def para x; $log << x; end def banner x; $log << x; end; def caption x; $log << x; end; def mouse(); [1] + SCREEN_ORIGIN.to_a; end def keypress; yield "x"; end; def click; yield mouse(); end; def animate(n); 3.times {yield}; end # # User Interaction APP = Shoes rescue AppTest APP.app :width => SCREEN_SIZE[X], :height => SCREEN_SIZE[Y] do $app = self $app = APP if $app.class == Object @game = TankSpank.new stack do para "Game Starting...", :stroke => blue end @paused = false keypress do |key| case key when "1", "z" button, x, y = mouse() @game.follow x,y if @game.playing? when "2", "x", " " @game.fire if @game.playing? when "p" @paused = ! @paused when "n" @game = TankSpank.new stack do banner "New Game", :stroke => white, :margin => 10 end end end click do |button, x, y| if 1 == button @game.follow x, y if @game.playing? else @game.fire if @game.playing? end end animate(FRAMES_PER_SECOND) do button, x, y = mouse() if not @paused @game.update x,y @game.draw end $debug = false if @game.playing? stack do para @game.status, :stroke => orange, :margin => 10 para "Mouse => aim, Click/z => move, Space/x => fire, p => pause, n => new\n", :stroke => silver, :margin => 10 para "Paused", :stroke => yellow if @paused end else stack do banner "Game Over", :stroke => white, :margin => 10 if @game.enemies.size > 0 caption "Learn to drive!", :stroke => white, :margin => 20 else caption "Congratulations!You totally r00l!", :stroke => white, :margin => 20 end end end # else end # animate end # app
On May 21, 2008, at 11:32 PM, _why wrote:
Okay, I made it through that GTK-related bug. So, some new builds and the tarball: * http://code.whytheluckystiff.net/dist/shoes-0.r605.exe (windows xp/vista + video support; 7M) * http://code.whytheluckystiff.net/dist/shoes-0.r605-intel.dmg (osx intel + video support; 7.7M) * http://code.whytheluckystiff.net/dist/shoes-0.r605.tar.gz (source; 421K) Other builds are listed at <http://code.whytheluckystiff.net/shoes/wiki/RecentBuilds> No PPC build this time, still trying to work it out. -- New in this release: transparent scroll windows. Previously, scroll windows (such as the link list in samples/good-vjot.rb) had a white background. In this release, scroll windows are clear, which makes them much more like HTML divs styled with {overflow-y: auto}. Also, if you have curved rectangles (backgrounds or borders,) you'll need to use :curve instead of :radius in your style. The :radius style is now used for radial gradients. # black background with curved corners background black, :curve => 5 This release also includes both SQLite3 and Hpricot. _why
