#!/usr/bin/env ruby # coding: utf-8 =begin MISSLE DEFEAT Ver.0.99 - ATARI MISSILE COMMAND CLONE Copyright (C) 2013 Furutanian This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . =end require 'gtk2' #--------------------------------------------------------------- # # キャラクタ基底 # class Character attr_reader :pos_x, :pos_y attr_reader :points attr_reader :active def initialize(s) @s = s @active = true end def draw end def move end def check_hit end def destroy @active = false end end #--------------------------------------------------------------- # # 地形 # class Ground < Character def initialize(s) super(s) @form = []; [64, 320, 576].each {|x| @form += [[x - 64, 450], [x - 18, 428], [x - 12, 434], [x + 12, 434], [x + 18, 428], [x + 64, 450]] }; @form += [[640, 480], [0, 480]] end def draw @s[:GC].foreground = $color[6] @s[:WINDOW].draw_polygon(@s[:GC], true, @form) end end #--------------------------------------------------------------- # # 砲台 # class Batteries < Array # 砲台群 def initialize(s) @s = s [[64, 16], [320, 32], [576, 16]].each {|x, spec| self << Battery.new(@s, x, 434, spec) } end def invalid? # 弾切れ? self.each {|battery| battery.missiles != 0 and return(false) } true end def result self.each {|battery| battery.result and return(AntiballisticMissile.points) } false end end class Battery < Character # 各砲台 attr_reader :missiles def initialize(s, x, y, spec) super(s) @pos_x = x; @pos_y = y; @spec = spec @label = Label.new(s, x, y, 1); supply end def draw @label.draw end def update_label @label.set_text(@missiles.to_s + "\n" + (@missiles > 3 ? '' : (@missiles > 0 ? 'LOW' : 'OUT'))) end def launch(x, y) @missiles < 1 and return(false) @s[:OBJECTS][:ANTI_BALLISTIC_MISSILES] << AntiballisticMissile.new(@s, x, y, @pos_x, @pos_y, @spec) @missiles -= 1; update_label end def supply(missiles = 10) @missiles = missiles; update_label end def check_hit @s[:OBJECTS][:ENEMIES].each {|object| Math::sqrt((@pos_x - object.pos_x) ** 2 + (@pos_y - object.pos_y) ** 2) < 10 and self.destroy } end def destroy @s[:OBJECTS][:EXPLOSIONS] << Explosion.new(@s, @pos_x, @pos_y) @missiles = 0; update_label end def result @missiles > 0 ? (@missiles -= 1 and update_label) : false end end #--------------------------------------------------------------- # # 都市 # class Cities < Array # 都市群 def initialize(s) @s = s [128, 192, 256, 384, 448, 512].each {|x| self << City.new(@s, x, 450) }; restore end def bonus # 未実装 :-) every 10000 points end def wrecked? # 全滅? self.each {|city| city.wrecked or return(false) } true end def live? ((city = self[@p += 1]).wrecked or return(city)) while(@p < (self.size - 1)) false end def restore self.each {|city| city.restore }; @p = -1 end end class City < Character # 各都市 attr_reader :wrecked def self.points 200 end def initialize(s, x, y) super(s) @pos_x0 = x; @pos_y0 = y construct end def draw @s[:GC].foreground = $color[@wrecked ? 2 : 5] @s[:WINDOW].draw_rectangle(@s[:GC], true, @pos_x - 12, @pos_y - 8, 24, 16) end def check_hit @wrecked and return @s[:OBJECTS][:ENEMIES].each {|object| Math::sqrt((@pos_x - object.pos_x) ** 2 + (@pos_y - object.pos_y) ** 2) < 10 and self.destroy } end def construct @wrecked = false end def destroy @s[:OBJECTS][:EXPLOSIONS] << Explosion.new(@s, @pos_x, @pos_y) @wrecked = true end def replace(x, y) @pos_x = x; @pos_y = y end def restore @pos_x = @pos_x0; @pos_y = @pos_y0 end end #--------------------------------------------------------------- # # 線形移動物体 # module Liner def liner_init(tgt_x, tgt_y, apr_x, apr_y, speed) @pos_x = @apr_x = apr_x; @pos_y = @apr_y = apr_y; @tgt_x = tgt_x; @tgt_y = tgt_y @dif_x = @tgt_x - @apr_x; @dif_y = @tgt_y - @apr_y @rest = 1 << 16 # 65536 分率 @step = (@rest / (Math::sqrt(@dif_x ** 2 + @dif_y ** 2) / speed)).to_i end def liner_move @rest -= @step @pos_x = @tgt_x - (@dif_x * @rest >> 16) @pos_y = @tgt_y - (@dif_y * @rest >> 16) end end #--------------------------------------------------------------- # # ミサイル基底 # class Missile < Character include Liner def initialize(s, tgt_x, tgt_y, apr_x, apr_y, speed) super(s) liner_init(tgt_x, tgt_y, apr_x, apr_y, speed) end def draw(c) @active or return @s[:GC].set_line_attributes(2, Gdk::GC::LINE_SOLID, Gdk::GC::CAP_BUTT, Gdk::GC::JOIN_BEVEL) @s[:GC].foreground = $color[c] @s[:WINDOW].draw_line(@s[:GC], @pos_x, @pos_y, @apr_x, @apr_y) @s[:GC].foreground = $color[$jiffies % 8] # 弾頭を点滅 @s[:WINDOW].draw_arc(@s[:GC], true, @pos_x - 1, @pos_y - 1, 2, 2, 0, 360 * 64) end def move @active or return liner_move @rest <= 0 and destroy end end #--------------------------------------------------------------- # # 迎撃ミサイル # class AntiballisticMissile < Missile def self.points 5 end def draw @active or return @s[:GC].foreground = $color[$jiffies % 8] # 着弾位置の「x」描画 @s[:WINDOW].draw_line(@s[:GC], @tgt_x - 6, @tgt_y - 6, @tgt_x + 6, @tgt_y + 6) @s[:WINDOW].draw_line(@s[:GC], @tgt_x + 6, @tgt_y - 6, @tgt_x - 6, @tgt_y + 6) super(1) end def destroy @active or return @s[:OBJECTS][:EXPLOSIONS] << Explosion.new(@s, @tgt_x, @tgt_y) super end end #--------------------------------------------------------------- # # 弾道ミサイル(敵) # class BallisticMissile < Missile def initialize(s, tgt_x, tgt_y, apr_x, apr_y, speed) super @points = 25 end def draw @active or return super(2) end def destroy @active or return @s[:OBJECTS][:EXPLOSIONS] << Explosion.new(@s, @pos_x, @pos_y) super end end #--------------------------------------------------------------- # # 多弾頭弾道ミサイル(敵) # class Mirv < BallisticMissile # 未実装 :-) end #--------------------------------------------------------------- # # 爆撃機(敵) # class Bomber < Character include Liner @@images = [Gdk::Pixbuf.new('missile_defeat_images.png')] @@images << @@images[0].flip(true) # 左右反転 def initialize(s, tgt_x, tgt_y, apr_x, apr_y, speed) super(s) liner_init(tgt_x, tgt_y, apr_x, apr_y, speed) @equipped = true # 爆装 @points = 100 end def draw @active or return @s[:WINDOW].draw_pixbuf(@s[:GC], @@images[@pos_x < @tgt_x ? 0 : 1], 0, 0, @pos_x - 16, @pos_y - 16, 32, 32, Gdk::RGB::DITHER_NONE, 0, 0) end def move @active or return liner_move if(@equipped and @rest < 1 << 14) # 爆撃 p0 = rand(99); (p0..(p0 + 4)).each {|p| target = (it = @s[:OBJECTS][:TARGETS])[p % it.size] @s[:OBJECTS][:ENEMIES] << BallisticMissile.new(@s, target.pos_x, target.pos_y, @pos_x, @pos_y, 3) }; @equipped = false end @rest < -4000 and @active = false # 画面外へ end def destroy @active or return @s[:OBJECTS][:EXPLOSIONS] << Explosion.new(@s, @pos_x, @pos_y) super end end #--------------------------------------------------------------- # # キラー衛星(敵) # class KillerSatellite < Bomber def draw @active or return @s[:WINDOW].draw_pixbuf(@s[:GC], @@images[0], 0, 32, @pos_x - 16, @pos_y - 16, 32, 32, Gdk::RGB::DITHER_NONE, 0, 0) @s[:GC].foreground = $color[$jiffies % 8] # 点滅 @s[:WINDOW].draw_rectangle(@s[:GC], true, @pos_x - 13, @pos_y - 12, 2, 2) @s[:WINDOW].draw_rectangle(@s[:GC], true, @pos_x + 11, @pos_y - 12, 2, 2) @s[:WINDOW].draw_rectangle(@s[:GC], true, @pos_x - 13, @pos_y + 10, 2, 2) @s[:WINDOW].draw_rectangle(@s[:GC], true, @pos_x + 11, @pos_y + 10, 2, 2) end end #--------------------------------------------------------------- # # スマート爆弾(敵) # class SmartBomb < Character # 未実装 :-) end #--------------------------------------------------------------- # # 爆風 # class Explosion < Character def initialize(s, x, y) super(s) @pos_x = x; @pos_y = y; @rad = 0 @rest = 1 << 16 # 65536 分率 @step = 1 << 10 end def draw @active or return @s[:GC].foreground = $color[$jiffies % 8] # 点滅 @s[:WINDOW].draw_arc(@s[:GC], true, @pos_x - @rad, @pos_y - @rad, @rad * 2, @rad * 2, 0, 360 * 64) end def move @active or return @rest -= @step @rad = (@rest > (1 << 15) ? ((1 << 16) - @rest) : @rest) >> 10 @rest <= 0 and destroy end def check_hit @active or return @s[:OBJECTS][:ENEMIES].each {|object| if(Math::sqrt((@pos_x - object.pos_x) ** 2 + (@pos_y - object.pos_y) ** 2) < @rad) @s[:OBJECTS][:SCORE][1].win(object) object.destroy end } end end #--------------------------------------------------------------- # # 文字情報基底 # class Label < Character def initialize(s, x, y, c = 7) super(s) @pos_x = x; @pos_y = y; @color = c @font = Pango::FontDescription.new @font.family = 'Helvetica' @font.weight = Pango::WEIGHT_BOLD # WEIGHT_NORMAL:400, WEIGHT_BOLD: 700 @font.size = 10 * Pango::SCALE @stat = Pango::Layout.new(@s[:DRAW].pango_context) @stat.font_description = @font @stat.width = 1 @stat.alignment = Pango::Layout::ALIGN_CENTER @stat.text = '' end def set_text(text) @stat.text = text end def draw @active or return @s[:WINDOW].draw_layout(@s[:GC], @pos_x, @pos_y, @stat, $color[@color]) end end #--------------------------------------------------------------- # # タイトル(ゲームオーバー) # class Title < Label def initialize(s, x = 0, y = 120, c = 2, t = "MISSILE\nDEFEAT") super(s, x, y, c) @font.weight = Pango::WEIGHT_ULTRABOLD @font.size = 64 * Pango::SCALE @stat.font_description = @font @stat.width = 640 * Pango::SCALE @stat.text = t end end #--------------------------------------------------------------- # # 流れるメッセージ # class Message < Label def initialize(s) super(s, 640, 460, 1) @font.size = 12 * Pango::SCALE @stat.font_description = @font @stat.width = 640 * Pango::SCALE @stat.alignment = Pango::Layout::ALIGN_LEFT @stat.text = 'GAME OVER CLICK TO START' end def move (@pos_x -= 2) < -300 and @pos_x = 640 end end #--------------------------------------------------------------- # # ステージ開始 # class Sign < Label def initialize(s, power) super(s, 0, 170, 1) @font.size = 18 * Pango::SCALE @stat.font_description = @font @stat.width = 640 * Pango::SCALE @stat.text = "PLAYER 1\n\n%d X POINTS\n\n\n\nDEFEND%sCITIES" % [power, ' ' * 24] end end #--------------------------------------------------------------- # # スコア(得点) # class Score < Label attr_reader :score def initialize(s, x, y, hi = nil) super(s, x, y, 2) @font.size = 18 * Pango::SCALE @stat.font_description = @font @stat.alignment = Pango::Layout::ALIGN_RIGHT @hi = hi and @hi.set(7500) # 初期ハイスコア reset end def set(score) @stat.text = '%d%s' % [@score = score, @hi ? '←' : ''] @hi and @score > @hi.score and @hi.set(@score) end def reset set(0) end def win(object) set(@score + object.points) end end #--------------------------------------------------------------- # # ステージクリアレポート # class Report < Label attr_reader :points def initialize(s, x, y, c, t = '', w = 400, a = Pango::Layout::ALIGN_LEFT) super(s, x, y, c) @font.size = 18 * Pango::SCALE @stat.font_description = @font @stat.width = w * Pango::SCALE @stat.alignment = a @stat.text = t @points = 0; @marks = '' end def win(object, mark = nil) @stat.text = '%04d ' % (@points += object.points) @stat.text += (@marks += mark) if(mark) end end #--------------------------------------------------------------- # # キャラクタ出現タイムテーブル # class Timetables attr_reader :state def initialize(s) @s = s @tables = [] @wait = 0 bind_stages demo end def bind(n) @tables[n] = Proc.new end def demo @stage = 0; @time = 0 @state = :DEMO end def change_stage(stage) @stage = stage; @time = 0 @state = :PLAY end def start_stage change_stage(1) end def next_stage change_stage(@stage + 1) end def report @time = 0 @state = :REPORT end def do @time += 1 (@wait -= 1) > 0 and return(true) @tables[@state != :REPORT ? @stage : 65535].call # 返り値を素通し end def bind_stages # 初期化&デモ画面定義 bind(0) { if(@time == 1) @s[:OBJECTS][:BATTERIES].each {|battery| battery.supply } @s[:OBJECTS][:CITIES].each {|city| city.construct } @s[:OBJECTS][:TITLE] = [Title.new(@s)] @s[:OBJECTS][:SCROLL_MESSAGE] = [Message.new(@s)] elsif(@time % 50 == 0) target = (it = @s[:OBJECTS][:TARGETS])[rand(it.size)] @s[:OBJECTS][:ENEMIES] << BallisticMissile.new(@s, target.pos_x, target.pos_y, rand(640), 0, 1) end true } # ステージ 01 定義 bind(1) { if(@time == 1) # 開始処理 @s[:OBJECTS][:TITLE].clear @s[:OBJECTS][:SCROLL_MESSAGE].clear @s[:OBJECTS][:ENEMIES].clear @s[:OBJECTS][:SCORE][1].reset @s[:OBJECTS][:BATTERIES].each {|battery| battery.supply } @s[:OBJECTS][:CITIES].each {|city| city.construct } @s[:OBJECTS][:SIGNS] = [Sign.new(@s, 1)] @delay = 100 end @time == 30 and @s[:OBJECTS][:SIGNS].clear if(@time > 30 and @time < 100 and @time % 5 == 0) target = (it = @s[:OBJECTS][:TARGETS])[rand(it.size)] @s[:OBJECTS][:ENEMIES] << BallisticMissile.new(@s, target.pos_x, target.pos_y, rand(640), 0, 1) end @time == 200 and @s[:OBJECTS][:ENEMIES] << Bomber.new(@s, 0, 200, 640, 200, 3) if(@time > 230 and @time < 300 and @time % 5 == 0) target = (it = @s[:OBJECTS][:TARGETS])[rand(it.size)] @s[:OBJECTS][:ENEMIES] << BallisticMissile.new(@s, target.pos_x, target.pos_y, rand(640), 0, 1) end @time < 300 or @s[:OBJECTS][:ENEMIES].size > 0 or (@delay -= 1) > 0 } # ステージ 02 定義 bind(2) { if(@time == 1) # 弾数復活 @s[:OBJECTS][:BATTERIES].each {|battery| battery.supply } @s[:OBJECTS][:SIGNS] = [Sign.new(@s, 1)] @delay = 100 end @time == 30 and @s[:OBJECTS][:SIGNS].clear if(@time > 30 and @time < 300 and @time % 2 == 0) target = (it = @s[:OBJECTS][:TARGETS])[rand(it.size)] @s[:OBJECTS][:ENEMIES] << BallisticMissile.new(@s, target.pos_x, target.pos_y, rand(640), 0, 4) end @time == 40 and @s[:OBJECTS][:ENEMIES] << Bomber.new(@s, 640, 200, 0, 200, 3) @time == 80 and @s[:OBJECTS][:ENEMIES] << KillerSatellite.new(@s, 0, 100, 640, 100, 3) @time < 300 or @s[:OBJECTS][:ENEMIES].size > 0 or (@delay -= 1) > 0 } # レポート/ゲームオーバ画面定義 bind(65535) { if(@time == 1) # 開始処理 @s[:OBJECTS][:ENEMIES].clear unless(@s[:OBJECTS][:CITIES].wrecked?) # ステージクリア @s[:OBJECTS][:REPORTS] = [Report.new(@s, 200, 140, 1, 'BONUS POINTS')] @wait = 20; @report = { :PHASE => :A } else # ゲームオーバ @s[:OBJECTS][:TITLE] = [Title.new(@s, 0, 160, 2, 'THE END')] @wait = 80; @report = { :PHASE => :O } end elsif(@report[:PHASE] == :A) # 残ミサイル集計 @report[:REPORT_A] or @s[:OBJECTS][:REPORTS] << (@report[:REPORT_A] = Report.new(@s, 200, 210, 2)) if(@s[:OBJECTS][:BATTERIES].result) @report[:REPORT_A].win(AntiballisticMissile, '^'); @wait = 2 else @wait = 20; @report[:PHASE] = :C end elsif(@report[:PHASE] == :C) # 残都市集計 @report[:REPORT_C] or @s[:OBJECTS][:REPORTS] << (@report[:REPORT_C] = Report.new(@s, 200, 280, 2)) @report[:CITY_X] ||= 250 if(city = @s[:OBJECTS][:CITIES].live?) city.replace(@report[:CITY_X] += 42, 295) @report[:REPORT_C].win(City); @wait = 10 else @wait = 80; @report[:PHASE] = :Q end # ボーナスシティ 未実装 :-) elsif(@report[:PHASE] == :Q) # 次のステージへ @s[:OBJECTS][:SCORE][1].win(@report[:REPORT_A]) @s[:OBJECTS][:SCORE][1].win(@report[:REPORT_C]) @s[:OBJECTS][:REPORTS].clear @s[:OBJECTS][:CITIES].restore false elsif(@report[:PHASE] == :O) # ゲームオーバ @s[:OBJECTS][:TITLE].clear false end } end end #--------------------------------------------------------------- # # 進行処理定義 # class Director def initialize(s) @timetables = Timetables.new(@s = s) @launch_keys = { :Z => 0, :X => 1, :C => 2 } end def do # 入力処理 while(input = @s[:INPUTS].shift) if(@timetables.state == :PLAY) it = @launch_keys[input[0]] and @s[:OBJECTS][:BATTERIES][it].launch(input[1][1], input[1][2]) # キー毎、左、中、右側から (input[1].button == 1 ? [1] : (input[1].x < 320 ? [0, 2] : [2, 0])).each {|w| # 左クリックなら中央、右クリックなら左右どちらかの側から @s[:OBJECTS][:BATTERIES][w].launch(input[1].x.to_i, input[1].y.to_i) and break } if(input[0] == :CLICK) elsif(@timetables.state == :DEMO) input[0] == :CLICK and @timetables.start_stage end end if(@timetables.state == :PLAY or @timetables.state == :DEMO) # 弾切れ、または、都市が全て破壊されたら3倍速で動かす (1..((@s[:OBJECTS][:BATTERIES].invalid? or @s[:OBJECTS][:CITIES].wrecked?) ? 3 : 1)).each { # 移動処理 @s[:OBJECTS].each {|type, objects| objects.delete_if {|object| object.move !object.active # 死んだオブジェクトは配列から削除 } } # 登場処理(ステージ毎のキャラクタ出現) @timetables.do or @timetables.report # 衝突判定処理 @s[:OBJECTS].each {|type, objects| objects.delete_if {|object| object.check_hit !object.active # 死んだオブジェクトは配列から削除 } } } elsif(@timetables.state == :REPORT) # レポート/ゲームオーバ表示処理 @timetables.do or (@s[:OBJECTS][:CITIES].wrecked? ? @timetables.demo : @timetables.next_stage) end end end #--------------------------------------------------------------- # # カラー定義 # $color = [ black = Gdk::Color.new( 0, 0, 0), blue = Gdk::Color.new( 0, 0, 65535), red = Gdk::Color.new( 65535, 0, 0), magenta = Gdk::Color.new( 65535, 0, 65535), green = Gdk::Color.new( 0, 65535, 0), cyan = Gdk::Color.new( 0, 65535, 65535), yellow = Gdk::Color.new( 65535, 65535, 0), white = Gdk::Color.new( 65535, 65535, 65535), ] $colormap = Gdk::Colormap.system $color.each {|c| $colormap.alloc_color(c, false, true) } #--------------------------------------------------------------- # # 描画画面定義 # draw0 = Gtk::DrawingArea.new draw0.set_size_request(640, 480) draw0.modify_bg(Gtk::STATE_NORMAL, $color[0]) draw0.signal_connect('realize') {|draw| @gc = Gdk::GC.new(@window = draw.window) @window.cursor = Gdk::Cursor.new(Gdk::Cursor::CROSSHAIR) # 出来合いのクロスカーソルを利用 } draw0.signal_connect('expose_event') { # 再描画イベント @objects.each {|type, objects| objects.each {|object| object.draw } } } @draw = Gtk::EventBox.new.add(draw0) # クリック、ドラッグなどを受ける枠組み @draw.signal_connect('button_press_event') {|w, e| # マウスボタン押下ハンドラ(Gtk::EventBox, Gdk::EventButton) @inputs << [:CLICK, e] } main_box = Gtk::VBox.new main_box.pack_start(@draw, false, false, 0) #--------------------------------------------------------------- # # ウィンドウ定義 # window = Gtk::Window.new window.title = 'MISSILE DEFEAT' window.signal_connect('destroy') { Gtk.main_quit } # 終了ハンドラ window.signal_connect('key_press_event') {|w, e| # キー押下ハンドラ(Gtk::Window, Gdk::EventKey) e.keyval == Gdk::Keyval::GDK_z and @inputs << [:Z, @window.pointer] e.keyval == Gdk::Keyval::GDK_x and @inputs << [:X, @window.pointer] e.keyval == Gdk::Keyval::GDK_c and @inputs << [:C, @window.pointer] } window.add(main_box) window.show_all #--------------------------------------------------------------- # # 垂直帰線割り込みのシミュレート # @spf = (1.0 / 30) # フレーム速度(1/FPS) Gtk.idle_add { $jiffies += 1 @start = Time.now.to_f; @next = @start + @spf @draw.queue_draw # 描画処理(expose_event を発生) @director.do # 各種処理 @finish = Time.now.to_f # puts 'now: %13.2f, load: %6.2f%%' % [@start, (@finish - @start) * 100 / @spf] (it = @next - @finish) > 0 and sleep(it) # 余った時間分 sleep する true } #--------------------------------------------------------------- # # メイン処理 # $jiffies = 0 s = { :DRAW => @draw, # Gtk::EventBox :WINDOW => @window, # Gdk::Window :GC => @gc, # Gdk::GC :INPUTS => @inputs = [], :OBJECTS => @objects = {} } @objects[:GROUND] = [Ground.new(s)] @objects[:BATTERIES] = b = Batteries.new(s) @objects[:CITIES] = c = Cities.new(s) @objects[:TARGETS] = b + c @objects[:ENEMIES] = [] @objects[:ANTI_BALLISTIC_MISSILES] = [] @objects[:EXPLOSIONS] = [] @objects[:SCORE] = [hi = Score.new(s, 360, 0), Score.new(s, 200, 0, hi)] @director = Director.new(s) Gtk.main __END__