petermorphose 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (95) hide show
  1. data/COPYING +3 -0
  2. data/README.md +31 -0
  3. data/bin/petermorphose +3 -0
  4. data/levels/flgr_Der_Alkohol_und_seine_Folgen.pml +447 -0
  5. data/levels/jr_Am_Tempel_des_Harlow-Karlow.pml +413 -0
  6. data/levels/jr_Auf_der_Flucht.pml +456 -0
  7. data/levels/jr_Die_zwei_Baeume.pml +447 -0
  8. data/levels/jr_Feuertauchen.pml +343 -0
  9. data/levels/jr_Gemuetlicher_Aufstieg.pml +329 -0
  10. data/levels/jr_Gruenhuegelshausen.pml +423 -0
  11. data/levels/jr_Gruselgrotte.pml +421 -0
  12. data/levels/jr_Hoch_hinaus.pml +265 -0
  13. data/levels/jr_Vom_Ozean_in_die_Traufe.pml +342 -0
  14. data/levels/jr_Weg_durchs_Feuer.pml +544 -0
  15. data/levels/sl_Heimweg_zu_Henk.pml +307 -0
  16. data/media/arg1.wav +0 -0
  17. data/media/arg2.wav +0 -0
  18. data/media/arrow_hit.wav +0 -0
  19. data/media/blocker_break.wav +0 -0
  20. data/media/bow.wav +0 -0
  21. data/media/break1.wav +0 -0
  22. data/media/break2.wav +0 -0
  23. data/media/buttons.png +0 -0
  24. data/media/collect_ammo.wav +0 -0
  25. data/media/collect_freeze.wav +0 -0
  26. data/media/collect_health.wav +0 -0
  27. data/media/collect_key.wav +0 -0
  28. data/media/collect_points.wav +0 -0
  29. data/media/collect_star.wav +0 -0
  30. data/media/danger.png +0 -0
  31. data/media/death.wav +0 -0
  32. data/media/dialogs.bmp +0 -0
  33. data/media/door1.wav +0 -0
  34. data/media/door2.wav +0 -0
  35. data/media/eat.wav +0 -0
  36. data/media/effects.bmp +0 -0
  37. data/media/enemies.bmp +0 -0
  38. data/media/explosion.wav +0 -0
  39. data/media/game.ogg +0 -0
  40. data/media/gui.bmp +0 -0
  41. data/media/help1.wav +0 -0
  42. data/media/help2.wav +0 -0
  43. data/media/jump.wav +0 -0
  44. data/media/lava.wav +0 -0
  45. data/media/lever.wav +0 -0
  46. data/media/menu.ogg +0 -0
  47. data/media/morph.wav +0 -0
  48. data/media/player.bmp +0 -0
  49. data/media/player_arg.wav +0 -0
  50. data/media/shshsh.wav +0 -0
  51. data/media/skies.png +0 -0
  52. data/media/slime1.wav +0 -0
  53. data/media/slime2.wav +0 -0
  54. data/media/slime3.wav +0 -0
  55. data/media/stairs.wav +0 -0
  56. data/media/stairs_steps.wav +0 -0
  57. data/media/stuff.bmp +0 -0
  58. data/media/sword_whoosh.wav +0 -0
  59. data/media/tiles.bmp +0 -0
  60. data/media/title.png +0 -0
  61. data/media/title_dark.png +0 -0
  62. data/media/turbo.wav +0 -0
  63. data/media/water1.wav +0 -0
  64. data/media/water2.wav +0 -0
  65. data/media/whoosh.wav +0 -0
  66. data/media/whoosh_back.wav +0 -0
  67. data/media/won.bmp +0 -0
  68. data/media/yippie.wav +0 -0
  69. data/src/const.rb +301 -0
  70. data/src/en.yml +62 -0
  71. data/src/gosu-preview.rb +116 -0
  72. data/src/helpers/audio.rb +9 -0
  73. data/src/helpers/graphics.rb +24 -0
  74. data/src/helpers/input.rb +28 -0
  75. data/src/ini_file.rb +25 -0
  76. data/src/level_info.rb +63 -0
  77. data/src/localization.rb +19 -0
  78. data/src/main.rb +86 -0
  79. data/src/map.rb +136 -0
  80. data/src/objects/collectible_object.rb +174 -0
  81. data/src/objects/effect_object.rb +120 -0
  82. data/src/objects/game_object.rb +363 -0
  83. data/src/objects/living_object.rb +657 -0
  84. data/src/objects/object_def.rb +45 -0
  85. data/src/script.rb +207 -0
  86. data/src/states/credits.rb +24 -0
  87. data/src/states/game.rb +463 -0
  88. data/src/states/help.rb +268 -0
  89. data/src/states/level_selection.rb +43 -0
  90. data/src/states/menu.rb +45 -0
  91. data/src/states/options.rb +81 -0
  92. data/src/states/state.rb +11 -0
  93. data/src/states/title.rb +17 -0
  94. data/src/states/victory.rb +20 -0
  95. metadata +188 -0
@@ -0,0 +1,45 @@
1
+ class ObjectDef < Struct.new(:name, :life, :rect, :speed, :jump_x, :jump_y)
2
+ # TODO This has become a general purpose class, should be moved out of this file
3
+ class Rect < Struct.new(:left, :top, :width, :height)
4
+ def right
5
+ left + width
6
+ end
7
+
8
+ def bottom
9
+ top + height
10
+ end
11
+
12
+ def collide_with? other
13
+ left < other.right and right > other.left and
14
+ top < other.bottom and bottom > other.top
15
+ end
16
+
17
+ def include? point
18
+ point.x >= left and point.y >= top and
19
+ point.x < right and point.y < bottom
20
+ end
21
+ end
22
+
23
+ def self.[](pmid)
24
+ @all ||= begin
25
+ ini = INIFile.new('objects.ini')
26
+ (0..ID_MAX).map do |id|
27
+ ObjectDef.new.tap do |obj_def|
28
+ hex_id = "%02X" % id
29
+ obj_def.name = ini['ObjName', hex_id] || '<no name>'
30
+ obj_def.life = (ini['ObjLife', hex_id] || 3).to_i
31
+ rect_string = ini['ObjRect', hex_id] || '10102020'
32
+ obj_def.rect = Rect.new
33
+ obj_def.rect.left = -rect_string[0, 2].to_i(16)
34
+ obj_def.rect.top = -rect_string[2, 2].to_i(16)
35
+ obj_def.rect.width = +rect_string[4, 2].to_i(16)
36
+ obj_def.rect.height = +rect_string[6, 2].to_i(16)
37
+ obj_def.speed = (ini['ObjSpeed', hex_id] || 3).to_i
38
+ obj_def.jump_x = (ini['ObjJump', "#{hex_id}X"] || 0).to_i
39
+ obj_def.jump_y = (ini['ObjJump', "#{hex_id}Y"] || 0).to_i
40
+ end
41
+ end
42
+ end
43
+ @all[pmid]
44
+ end
45
+ end
data/src/script.rb ADDED
@@ -0,0 +1,207 @@
1
+ module PMScript
2
+ def letter_to_object letter
3
+ letter == 'P' ? player : obj_vars[letter.to_i(16)]
4
+ end
5
+
6
+ def convert_sound_name name
7
+ case name.downcase
8
+ when /^(.*)Collect/ then
9
+ "collect_#{$1}"
10
+ when "jeepee" then
11
+ "yippie"
12
+ when "stairsrnd" then
13
+ "stairs_steps"
14
+ when "swordwoosh" then
15
+ "sword_whoosh"
16
+ when "blockerbreak" then
17
+ "blocker_break"
18
+ when "arrowhit" then
19
+ "arrow_hit"
20
+ else
21
+ if (sound("#{name.downcase}2") rescue nil) then
22
+ "#{name.downcase}1"
23
+ else
24
+ name.downcase
25
+ end
26
+ end
27
+ end
28
+
29
+ def shortcut_to_message shortcut
30
+ case shortcut
31
+ when 'ex' then :existence_as_int
32
+ when 'px' then :x
33
+ when 'py' then :y
34
+ when 'vx' then :vx
35
+ when 'vy' then :vy
36
+ when 'id' then :pmid
37
+ when 'lf' then :life
38
+ when 'ac' then :action
39
+ when 'dr' then :direction
40
+ end
41
+ end
42
+
43
+ def get_var var
44
+ case var
45
+ when /^var(.)$/ then
46
+ map.vars[$1.to_i(16)]
47
+ when /\?(...)/ then
48
+ rand($1.to_i(16) + 1)
49
+ when /^\$(.)(..)/ then
50
+ receiver = letter_to_object($1)
51
+ message = shortcut_to_message($2)
52
+ receiver.send(message)
53
+ when 'keys' then keys
54
+ when 'ammo' then ammo
55
+ when 'bomb' then bombs
56
+ when 'star' then stars
57
+ when 'scor' then score
58
+
59
+ when 'time' then time_left
60
+ when 'tspd' then speed_time_left
61
+ when 'tjmp' then jump_time_left
62
+ when 'tfly' then fly_time_left
63
+
64
+ when 'lpos' then map.lava_pos
65
+ when 'lspd' then map.lava_speed
66
+ when 'lmod' then map.lava_mode
67
+ else
68
+ throw "Getting unknown variable #{var}"
69
+ end
70
+ end
71
+
72
+ def set_var var, value
73
+ case var
74
+ when /^var(.)$/ then
75
+ map.vars[$1.to_i(16)] = value
76
+ when /^\$(.)(..)/ then
77
+ receiver = letter_to_object($1)
78
+ message = shortcut_to_message($2)
79
+ receiver.send "#{message}=", value
80
+ when 'keys' then self.keys = value
81
+ when 'ammo' then self.ammo = value
82
+ when 'bomb' then self.bombs = value
83
+ when 'star' then self.stars = value
84
+ when 'scor' then self.score = value
85
+
86
+ when 'time' then self.time_left = value
87
+ when 'tspd' then self.speed_time_left = value
88
+ when 'tjmp' then self.jump_time_left = value
89
+ when 'tfly' then self.fly_time_left = value
90
+
91
+ when 'lpos' then map.lava_pos = value
92
+ when 'lspd' then map.lava_speed = value
93
+ when 'lmod' then map.lava_mode = value
94
+ else
95
+ throw "Setting unknown variable #{var}"
96
+ end
97
+ end
98
+
99
+ def evaluate_param param
100
+ if param.length == 5 then
101
+ evaluate_param(param[1..-1]) * (param[0, 1] == '-' ? -1 : +1)
102
+ else
103
+ if param =~ /[0-9A-Fa-f]{4}/ then
104
+ param.to_i(16)
105
+ else
106
+ get_var param
107
+ end
108
+ end
109
+ end
110
+
111
+ def evaluate_condition condition
112
+ return true if condition == 'always'
113
+ left_var = evaluate_param(condition[0, 4])
114
+ right_var = evaluate_param(condition[5, 4])
115
+
116
+ case condition[4, 1]
117
+ when '=' then return left_var == right_var
118
+ when '!' then return left_var != right_var
119
+ when '<' then return left_var < right_var
120
+ when '>' then return left_var > right_var
121
+ when '"' then return (left_var - right_var).abs <= 16
122
+ when "'" then return (left_var - right_var).abs > 16
123
+ when '{' then return left_var <= right_var
124
+ when '}' then return left_var >= right_var
125
+ else
126
+ "Unknown comparison: #{condition[4, 1]}"
127
+ end
128
+ end
129
+
130
+ def execute_command command, caller
131
+ return if command.empty?
132
+
133
+ if command[0, 1] == '_' then
134
+ # Magic condition repeat - return if last condition was false
135
+ return if not @last_cond
136
+ action = command[1..-1]
137
+ elsif not command =~ /^#{caller}\(/ then
138
+ # Wrong caller - counts as false condition
139
+ @last_cond = false
140
+ return
141
+ else
142
+ # Evaluate conditions
143
+ @last_cond = true
144
+
145
+ conditions = command[/\([^)]*\)/]
146
+ conditions = conditions[1..-2].split('&')
147
+ @last_cond = conditions.all? &method(:evaluate_condition)
148
+ return if not @last_cond
149
+ action = command[(command.index('):') + 2)..-1]
150
+ end
151
+
152
+ case action
153
+ when /^set (....) (.....)$/ then
154
+ set_var $1, evaluate_param($2)
155
+ when /^add (....) (.....)$/ then
156
+ set_var $1, get_var($1) + evaluate_param($2)
157
+ when /^mul (....) (.....)$/ then
158
+ set_var $1, get_var($1) * evaluate_param($2)
159
+ when /^dev (....) (.....)$/ then
160
+ set_var $1, get_var($1) / evaluate_param($2)
161
+ when /^kill \$(.)$/ then
162
+ letter_to_object($1).kill rescue nil
163
+ when /^mapsolid (....) (....) (....)$/ then
164
+ set_var $1, (map.solid?(evaluate_param($2), evaluate_param($3)) ? 1 : 0)
165
+ when /^hit \$(.)$/ then
166
+ letter_to_object($1).hit rescue nil
167
+ when /^hurt \$(.)$/ then
168
+ letter_to_object($1).hurt rescue nil
169
+ when /^createobject (....) (....) (....) (..)$/ then
170
+ obj = create_object(evaluate_param($1), evaluate_param($2), evaluate_param($3), '')
171
+ obj_vars[$4[1, 1].to_i(16)] = obj unless $4 == 'no'
172
+ when /^setxd \$(.) (.*)$/ then
173
+ letter_to_object($1).xdata = $2
174
+ when /^message (.*)$/ then
175
+ @message_text = t($1)
176
+ @message_opacity = 255
177
+ when /^message2 (.*)$/ then
178
+ @message_text = t($1).gsub(/\^..../) { |term| evaluate_param(term[1..-1]) }
179
+ @message_opacity = 255
180
+ when /^sound (.*)$/ then
181
+ sound(convert_sound_name($1)).play
182
+ when /^casteffects (....) (....) (....) (....) (....)$/ then
183
+ cast_fx evaluate_param($1), evaluate_param($2), evaluate_param($3),
184
+ evaluate_param($4), evaluate_param($5), TILE_SIZE, TILE_SIZE, 0, 0, 5
185
+ when /^casteffects2 (....) (....) (....) (....) (....) (....) (.....) (.....) (....)$/ then
186
+ cast_fx evaluate_param($1), evaluate_param($2), evaluate_param($3),
187
+ evaluate_param($4), evaluate_param($5), evaluate_param($6), evaluate_param($7),
188
+ evaluate_param($8), evaluate_param($9), evaluate_param($10)
189
+ when /^changetile (....) (....) (....)$/ then
190
+ map[evaluate_param($1), evaluate_param($2)] = evaluate_param($3)
191
+ when /^explosion (....) (....) (....)$/ then
192
+ explosion evaluate_param($1), evaluate_param($2), evaluate_param($3)
193
+ when /^find \$(.) (....) (....) (....) (....) (....) (....)$/
194
+ obj_vars[$1.to_i(16)] = find_object evaluate_param($2), evaluate_param($3),
195
+ ObjectDef::Rect.new(evaluate_param($4), evaluate_param($5), evaluate_param($6), evaluate_param($7))
196
+ else
197
+ throw "Don't know how to #{action}"
198
+ end
199
+ end
200
+
201
+ def execute_script script, caller
202
+ return if script.nil? or script.empty?
203
+
204
+ @cond = true
205
+ script.split('\\').each { |command| execute_command command, caller }
206
+ end
207
+ end
@@ -0,0 +1,24 @@
1
+ # TODO translate to Ruby
2
+
3
+ =begin
4
+ State_Credits: begin
5
+ if not (DXDraw.CanDraw and Application.Active) then Exit;
6
+ DXImageList.Items[Image_TitleDark].Draw(DXDraw.Surface, 0, 0, 0);
7
+
8
+ DrawBMPText('Spielidee, Programmierung, Grafiken, Sounds', 100, 40, 192, DXImageList.Items[Image_Font], DXDraw.Surface, 1);
9
+ DrawBMPText('Julian Raschke, julian@raschke.de', 200, 80, 255, DXImageList.Items[Image_Font], DXDraw.Surface, 1);
10
+ DrawBMPText('http://www.petermorphose.de/', 200, 100, 255, DXImageList.Items[Image_Font], DXDraw.Surface, 1);
11
+ DrawBMPText('Zus‰tzliche Sounds und Himmel', 100, 140, 192, DXImageList.Items[Image_Font], DXDraw.Surface, 1);
12
+ DrawBMPText('Sandro Mascia, xxsgrrs-@gmx.de', 200, 160, 255, DXImageList.Items[Image_Font], DXDraw.Surface, 1);
13
+ DrawBMPText('Sebastian Ludwig, usludwig@t-online.de', 200, 180, 255, DXImageList.Items[Image_Font], DXDraw.Surface, 1);
14
+ DrawBMPText('Sˆren Bevier, soeren.bevier@dtp-isw.de', 200, 200, 255, DXImageList.Items[Image_Font], DXDraw.Surface, 1);
15
+ DrawBMPText('Sebastian Burkhart, buggi@buggi.com', 200, 220, 255, DXImageList.Items[Image_Font], DXDraw.Surface, 1);
16
+ DrawBMPText('Florian Grofl, flgr@gmx.de', 200, 240, 255, DXImageList.Items[Image_Font], DXDraw.Surface, 1);
17
+ DrawBMPText('Holger Biermann, kaiserbiddy@web.de', 200, 260, 255, DXImageList.Items[Image_Font], DXDraw.Surface, 1);
18
+ DrawBMPText('Musik', 100, 300, 192, DXImageList.Items[Image_Font], DXDraw.Surface, 1);
19
+ DrawBMPText('Steffen Wenz, s.wenz@t-online.de', 200, 340, 255, DXImageList.Items[Image_Font], DXDraw.Surface, 1);
20
+
21
+ DrawBMPText('Willst du zur¸ck zum Hauptmen¸, dr¸cke Escape.', 113, 435, 0, DXImageList.Items[Image_Font], DXDraw.Surface, 0);
22
+ DXDraw.Flip;
23
+ end;
24
+ =end
@@ -0,0 +1,463 @@
1
+ class Game < State
2
+ include PMScript
3
+
4
+ # @result can be: nil (running), :lost, :won
5
+ # @paused can be: true, false
6
+
7
+ attr_reader :player, :map, :objects
8
+ attr_reader :view_pos, :frame
9
+ attr_accessor :time_left, :inv_time_left, :speed_time_left, :jump_time_left, :fly_time_left
10
+ attr_accessor :score, :keys, :stars, :ammo, :bombs
11
+ attr_reader :stars_goal
12
+ attr_reader :obj_vars
13
+
14
+ def inspect
15
+ "#<Game>"
16
+ end
17
+
18
+ def lose reason
19
+ @result = :lost
20
+ @reason = reason
21
+ end
22
+
23
+ def explosion x, y, radius, do_score
24
+ emit_sound :explosion, y
25
+
26
+ (10 + radius / 2).times do
27
+ angle = Gosu::random(0, 360)
28
+ slowdown = Gosu::random(5, 11)
29
+ fx = create_object(ID_FX_SMOKE + rand(2), x, y, nil)
30
+ fx.vx = Gosu::offset_x(angle, radius / slowdown).to_i
31
+ fx.vy = Gosu::offset_y(angle, radius / slowdown).to_i
32
+ end
33
+
34
+ # Damage nearby livings
35
+ objects.each do |obj|
36
+ next if not obj.is_a? LivingObject or obj.action == ACT_DEAD
37
+
38
+ dist = Gosu::distance(x, y, obj.x, obj.y)
39
+ next if dist >= radius
40
+
41
+ if dist < radius / 3 then
42
+ obj.hurt(true)
43
+ else
44
+ obj.hit
45
+ end
46
+
47
+ if obj.action == ACT_DEAD and do_score and obj.pmid.between? ID_ENEMY, ID_ENEMY_MAX then
48
+ @score += score = ObjectDef[obj.pmid].life * 3
49
+ obj.emit_text "*#{score}*"
50
+ end
51
+ end
52
+
53
+ # Damage creates
54
+ ((x - radius) / TILE_SIZE).upto((x + radius) / TILE_SIZE) do |tile_x|
55
+ ((y - radius) / TILE_SIZE).upto((y + radius) / TILE_SIZE) do |tile_y|
56
+ next if Gosu::distance(x, y, tile_x * TILE_SIZE + 12, tile_y * TILE_SIZE + 12) > radius
57
+
58
+ case map[tile_x, tile_y]
59
+ when TILE_BIG_BLOCKER, TILE_BIG_BLOCKER_2 then
60
+ if not find_object ID_FX_FIRE, ID_FX_FIRE, ObjectDef::Rect.new(tile_x * TILE_SIZE - 1, tile_y * TILE_SIZE - 1, 2, 2) then
61
+ create_object ID_FX_FIRE, tile_x * TILE_SIZE, tile_y * TILE_SIZE, nil
62
+ end
63
+ when TILE_BIG_BLOCKER_3 then
64
+ map[tile_x, tile_y] = 0
65
+ emit_sound "break#{rand(2) + 1}", y
66
+ cast_objects ID_FX_BREAKING_PARTS, 20, 0, 3, 3, ObjectDef::Rect.new(tile_x * TILE_SIZE, tile_y * TILE_SIZE, TILE_SIZE, TILE_SIZE)
67
+ end
68
+ end
69
+ end
70
+ end
71
+
72
+ def initialize level_info
73
+ song(:game).play
74
+
75
+ @view_pos = TILES_Y * TILE_SIZE - HEIGHT # TODO
76
+
77
+ @player_top_pos = 1024
78
+ @lava_top_pos = 1024
79
+ @message_text = ''
80
+ @message_opacity = 0
81
+ @frame = -1
82
+ @frame_fading_box = 16
83
+ @time_left = @inv_time_left = @speed_time_left = @jump_time_left = @fly_time_left = 0
84
+ @score = @keys = @stars = @ammo = @bombs = 0
85
+
86
+ @lava_frame = 0
87
+ @lava_time_left = 0
88
+
89
+ @objects = []
90
+ @obj_vars = [nil] * 16
91
+
92
+ @map = Map.new(self, level_info.ini_file)
93
+ @stars_goal = (level_info.ini_file['Map', 'StarsGoal'] || 100).to_i
94
+
95
+ player_id = (level_info.ini_file['Objects', 'PlayerID'] || 0).to_i
96
+ player_x = (level_info.ini_file['Objects', 'PlayerX'] || 288).to_i
97
+ player_y = (level_info.ini_file['Objects', 'PlayerY'] || 24515).to_i
98
+ @player = LivingObject.new(self, player_id, player_x, player_y, nil)
99
+ player.vx = (level_info.ini_file['Objects', 'PlayerVX'] || 0).to_i
100
+ player.vy = (level_info.ini_file['Objects', 'PlayerVY'] || 0).to_i
101
+ player.life = (level_info.ini_file['Objects', 'PlayerLife'] || ObjectDef[ID_PLAYER].life).to_i
102
+ player.direction = (level_info.ini_file['Objects', 'PlayerDirection'] || rand(2)).to_i
103
+ player.action = ACT_STAND
104
+ @objects << player
105
+
106
+ # If the player starts as a Special Peter, give him some time
107
+ @time_left = player.pmid == ID_PLAYER ? 0 : ObjectDef[player.pmid].life
108
+
109
+ i = 0
110
+ while obj_string = level_info.ini_file['Objects', i] do
111
+ pmid, x, y = obj_string.split('|').map { |str| str.to_i(16) }
112
+ create_object pmid, x, y, level_info.ini_file['Objects', "#{i}Y"]
113
+ i += 1
114
+ end
115
+ end
116
+
117
+ MAX_SOUND_DISTANCE = 500.0
118
+
119
+ def emit_sound name, y
120
+ distance = (y - player.y).abs
121
+ return if distance > MAX_SOUND_DISTANCE
122
+ sound(name).play 1 - distance / MAX_SOUND_DISTANCE
123
+ end
124
+
125
+ def update
126
+ debug binding if $window.button_down? Gosu::KbD
127
+
128
+ if @result.nil? and (player.action == ACT_DEAD or player.marked?) then
129
+ lose t("Du bist gestorben.")
130
+ elsif @result.nil? and player.y < map.level_top then
131
+ if stars < stars_goal then
132
+ lose t("Du hast verloren, weil du nicht genug Sterne gesammelt hast.")
133
+ elsif find_object(ID_CAROLIN, ID_CAROLIN, ObjectDef::Rect.new(0, 0, 576, 24576)) then
134
+ lose t("Du hast verloren, weil du nicht alle Gefangenen befreit hast.")
135
+ else
136
+ @result = :won
137
+ end
138
+ end
139
+
140
+ return if not @result.nil?
141
+
142
+ @frame = (@frame + 1) % 2400
143
+ @message_opacity -= 3 if @message_opacity > 0
144
+
145
+ if map.lava_pos / TILE_SIZE < @lava_top_pos then
146
+ @lava_top_pos = map.lava_pos / TILE_SIZE
147
+ execute_script map.scripts[@lava_top_pos], 'lava'
148
+ end
149
+
150
+ if player.y / TILE_SIZE < @player_top_pos then
151
+ @player_top_pos = player.y / TILE_SIZE
152
+ execute_script map.scripts[@lava_top_pos], 'player'
153
+ end
154
+
155
+ execute_script map.timers[frame % 10], 'do'
156
+ execute_script map.timers[10], 'do'
157
+
158
+ # Rising lava
159
+ if map.lava_time_left == 0 then
160
+ if map.lava_speed != 0 then
161
+ map.lava_pos -= 1 if map.lava_mode == 0 and frame % map.lava_speed == 0
162
+ map.lava_pos -= map.lava_speed if map.lava_mode == 1
163
+ map.lava_frame = (map.lava_frame + 1) % 120
164
+ emit_sound :lava, map.lava_pos if frame % 10 == 0 and rand(10) == 0
165
+ end
166
+ else
167
+ map.lava_time_left -= 1
168
+ end
169
+
170
+ if player.pmid != ID_PLAYER then
171
+ @time_left -= 1
172
+ if @time_left == 0 then
173
+ player.pmid = ID_PLAYER
174
+ cast_fx 8, 0, 0, player.x, player.y, 24, 24, 0, -1, 4
175
+ end
176
+ end
177
+ @inv_time_left -= 1 if @inv_time_left > 0
178
+ @view_pos = [[map.lava_pos - 432, player.y - 240, 24096].min, map.level_top].max
179
+
180
+ if fly_time_left == 0 and not player.in_water? then
181
+ if left_pressed? then
182
+ player.instance_eval do
183
+ if not busy? and vx > -ObjectDef[pmid].speed * 1.75 then
184
+ self.vx -= ObjectDef[pmid].speed + (game.speed_time_left > 0 ? 6 : 0)
185
+ end
186
+ if [ACT_JUMP, ACT_LAND, ACT_PAIN_1, ACT_PAIN_2].include? action then
187
+ (ObjectDef[pmid].jump_x * 2).times { self.x -= 1 unless blocked? DIR_LEFT }
188
+ end
189
+ end
190
+ end
191
+ if right_pressed? then
192
+ player.instance_eval do
193
+ if not busy? and vx < +ObjectDef[pmid].speed * 1.75 then
194
+ self.vx += ObjectDef[pmid].speed + (game.speed_time_left > 0 ? 6 : 0)
195
+ end
196
+ if [ACT_JUMP, ACT_LAND, ACT_PAIN_1, ACT_PAIN_2].include? action then
197
+ (ObjectDef[pmid].jump_x * 2).times { self.x += 1 unless blocked? DIR_RIGHT }
198
+ end
199
+ end
200
+ end
201
+ else
202
+ player.instance_eval do
203
+ 4.times { self.vy -= 1 if up_pressed? and vy > -4 }
204
+ 2.times { self.vy += 1 if down_pressed? and vy < +4 }
205
+ 4.times { self.vx -= 1 if left_pressed? and vx > -6 }
206
+ 4.times { self.vx += 1 if right_pressed? and vx < +6 }
207
+ if not in_water? then
208
+ self.vx -= 1 if vx > 0
209
+ self.vx += 1 if vx < 0
210
+ self.vy -= 1 if vy > 0
211
+ self.vy += 1 if vy < 0
212
+ elsif vx + vy > 1 and game.frame % 3 == 0 and rand(5) == 0 then
213
+ sound("water#{rand(2) + 1}").play
214
+ end
215
+ self.direction = DIR_LEFT if vx < 0
216
+ self.direction = DIR_RIGHT if vx > 0
217
+ end
218
+ end
219
+
220
+ if @speed_time_left > 0 then
221
+ @speed_time_left -= 1
222
+ cast_objects ID_FX_SPARK, rand(2), 0, 0, 1, player.rect(1, 1)
223
+ end
224
+ @jump_time_left -= 1 if @jump_time_left > 0
225
+ @fly_time_left -= 1 if @fly_time_left > 0
226
+
227
+ if frame > 2 then
228
+ player.jump if jump_pressed?
229
+ #player.use_tile if use_pressed?
230
+ player.act if action_pressed?
231
+ end
232
+
233
+ @objects.each &:update
234
+ @objects.reject! &:marked?
235
+
236
+ if map.lava_time_left == 0 then
237
+ cast_fx rand(2) + 1, rand(2) + 1, 0, 288, map.lava_pos, 576, 8, 1, -3, 1
238
+ if rand(15) == 0 then
239
+ create_object(ID_FX_BUBBLE, rand(576), map.lava_pos - 12, nil).vx = 1 - rand(3)
240
+ end
241
+ end
242
+ end
243
+
244
+ def draw
245
+ @map.draw
246
+ @objects.each &:draw
247
+
248
+ # Lava
249
+ @@danger ||= Gosu::Image.load_tiles 'media/danger.png', -2, -2
250
+ offset = if map.lava_time_left == 0 then frame / 2 % 2 else 0 end
251
+ -1.upto(4) do |x|
252
+ @@danger[map.lava_time_left == 0 ? 0 : 1].draw x * 120 + map.lava_frame + offset, map.lava_pos - view_pos, Z_LAVA
253
+ end
254
+ if map.lava_pos < map.level_top + 432 then
255
+ -1.upto(4) do |x|
256
+ 0.upto((map.level_top + 432 - map.lava_pos) / 48 + 1) do |y|
257
+ @@danger[map.lava_time_left == 0 ? 2 : 3].draw x * 120 + map.lava_frame + offset, map.lava_pos - view_pos + 48 + y * 48, Z_LAVA
258
+ end
259
+ end
260
+ end
261
+
262
+ draw_status_bar
263
+
264
+ # Optional progress indicator
265
+ # if Data.OptShowStatus = 1 then begin
266
+ # DXDraw.Surface.FillRectAlpha(Bounds(2, 38, 8, 404), clGray, 64);
267
+ # if Data.ObjPlayers.Next.ID > -1 then DXDraw.Surface.FillRectAlpha(Bounds(0, 38 + Round(((Data.ObjPlayers.Next.PosY / 24 - Data.Map.LevelTop / 24) / (LevelBottom - Data.Map.LevelTop / 24)) * 400), 12, 4), clNavy, 192);
268
+ # if (Data.Map.LavaPos <= 24576) and (Data.Map.LavaTimeLeft = 0) then DXDraw.Surface.FillRectAlpha(Bounds(0, 38 + Round(((Data.Map.LavaPos / 24 - Data.Map.LevelTop / 24) / (LevelBottom - Data.Map.LevelTop / 24)) * 400), 12, 4), clYellow, 128);
269
+ # if (Data.Map.LavaPos <= 24576) and (Data.Map.LavaTimeLeft > 0) then DXDraw.Surface.FillRectAlpha(Bounds(0, 38 + Round(((Data.Map.LavaPos / 24 - Data.Map.LevelTop / 24) / (LevelBottom - Data.Map.LevelTop / 24)) * 400), 12, 4), clAqua, 128);
270
+ # end;
271
+
272
+ if @result.nil? and @message_opacity > 0 then
273
+ draw_centered_string @message_text, WIDTH / 2, 230, @message_opacity
274
+ end
275
+
276
+ @@dialogs ||= Gosu::Image.load_tiles 'media/dialogs.bmp', -1, -3
277
+ if not @result.nil? then
278
+ @frame_fading_box += 1
279
+ @frame_fading_box = 1 if @frame_fading_box == 33
280
+
281
+ if @result == :lost then
282
+ draw_centered_string @reason, WIDTH / 2, 220, (16 - @frame_fading_box).abs * 15
283
+ else
284
+ @@dialogs[1].draw 200, 160, Z_UI, 1, 1, alpha((16 - @frame_fading_box).abs * 15), :additive
285
+ end
286
+ elsif @paused then
287
+ @@dialogs[2].draw 200, 120, Z_UI, 1, 1, alpha(255), :additive
288
+ end
289
+
290
+ draw_centered_string "#{t 'Punkte'}: #{score}", WIDTH / 2, 5
291
+ end
292
+
293
+ def button_down id
294
+ case @result
295
+ when nil then
296
+ if menu_cancel? id then
297
+ State.current = LevelSelection.new
298
+ sound(:whoosh).play
299
+ elsif @paused then
300
+ @paused = false if id == Gosu::KbP
301
+ else
302
+ @paused = true if id == Gosu::KbP
303
+ player.jump if jump? id and fly_time_left == 0
304
+ player.use_tile if use? id and fly_time_left == 0
305
+ player.act if action? id
306
+ end
307
+ when :lost then
308
+ if menu_cancel? id then
309
+ State.current = LevelSelection.new
310
+ sound(:whoosh).play
311
+ end
312
+ when :won then
313
+ if menu_confirm? id or menu_cancel? id then
314
+ State.current = LevelSelection.new
315
+ sound(:whoosh).play
316
+ end
317
+ end
318
+ end
319
+
320
+ def launch_projectile x, y, direction, min_id, max_id
321
+ emit_sound :bow, y
322
+ orig_x = x
323
+ while x > 2 and x < 573 do
324
+ target = find_living(min_id, max_id, 0, ACT_DEAD - 1, ObjectDef::Rect.new(x - 4, y - 2, 8, 4))
325
+
326
+ if target or map.solid?(x, y) then
327
+ create_object ID_FX_RICOCHET, x, y - 1 + rand(3), direction.to_s
328
+ emit_sound :arrow_hit, y
329
+ return target
330
+ end
331
+
332
+ x += direction.dir_to_vx * 4
333
+ end
334
+ ensure
335
+ create_object ID_FX_LINE, [x, orig_x].min, y, (x - orig_x).abs.to_s
336
+ end
337
+
338
+ include PMScript
339
+
340
+ def cast_objects pmid, num, vx, vy, randomness, rect
341
+ num.times do
342
+ obj = create_object pmid, rect.left + rand(rect.width), rect.top + rand(rect.height), nil
343
+ obj.vx = vx - randomness + rand(randomness * 2 + 1)
344
+ obj.vy = vy - randomness + rand(randomness * 2 + 1)
345
+ end
346
+ end
347
+
348
+ def cast_fx smoke, flames, sparks, x, y, *args
349
+ return if (view_pos + HEIGHT / 2 - y).abs > HEIGHT
350
+
351
+ cast_single_fx ID_FX_SMOKE, smoke, x, y, *args
352
+ cast_single_fx ID_FX_FLAME, flames, x, y, *args
353
+ cast_single_fx ID_FX_SPARK, sparks, x, y, *args
354
+ end
355
+
356
+ def create_object pmid, x, y, xdata
357
+ cls = case pmid
358
+ when 0..ID_LIVING_MAX then LivingObject
359
+ when ID_OTHER_OBJECTS_MIN..ID_OTHER_OBJECTS_MAX then GameObject
360
+ when ID_COLLECTIBLE_MIN..ID_COLLECTIBLE_MAX then CollectibleObject
361
+ when ID_FX_MIN..ID_FX_MAX then EffectObject
362
+ end
363
+
364
+ obj = cls.new(self, pmid, x, y, xdata)
365
+ @objects << obj
366
+ obj
367
+ end
368
+
369
+ def find_object min_id, max_id, rect
370
+ @objects.find { |o| o.pmid >= min_id and o.pmid <= max_id and rect.include? o }
371
+ end
372
+
373
+ def find_living min_id, max_id, min_act, max_act, rect
374
+ objects.find do |obj|
375
+ obj.pmid.between? min_id, max_id and obj.action.between? min_act, max_act and obj.collide_with? rect
376
+ end
377
+ end
378
+
379
+ private
380
+
381
+ def draw_status_bar
382
+ @@gui ||= Gosu::Image.load_tiles 'media/gui.bmp', -4, -11
383
+ Gosu::translate(576, 0) do
384
+ tile_w = @@gui.first.width
385
+ tile_h = @@gui.first.height
386
+
387
+ draw_digits = lambda do |num, row|
388
+ left_digit = [num, 99].min / 10 * 2 + 20
389
+ right_digit = [num, 99].min % 10 * 2 + 21
390
+ @@gui[left_digit].draw tile_w * 2, tile_h * row, Z_UI
391
+ @@gui[right_digit].draw tile_w * 3, tile_h * row, Z_UI
392
+ end
393
+
394
+ # Game logo and spacing
395
+ 0.upto(3) do |x|
396
+ @@gui[x + 0].draw tile_w * x, tile_h * 0, Z_UI
397
+ @@gui[x + 4].draw tile_w * x, tile_h * 1, Z_UI
398
+ end
399
+ # Health
400
+ if true then # TODO player.alive?
401
+ @@gui[8].draw tile_w * 0, tile_h * 2, Z_UI
402
+ @@gui[9].draw tile_w * 1, tile_h * 2, Z_UI
403
+ draw_digits.call player.life, 2
404
+ else
405
+ 0.upto(3) { |x| @@gui[x + 4].draw tile_w * x, tile_h * 2, Z_UI }
406
+ end
407
+ # Keys
408
+ @@gui[10].draw tile_w * 0, tile_h * 3, Z_UI
409
+ @@gui[11].draw tile_w * 1, tile_h * 3, Z_UI
410
+ draw_digits.call keys, 3
411
+ # Stars
412
+ @@gui[12].draw tile_w * 0, tile_h * 4, Z_UI
413
+ @@gui[13].draw tile_w * 1, tile_h * 4, Z_UI
414
+ draw_digits.call stars, 4
415
+ if stars > stars_goal then
416
+ # Enough stars - draw checkmark
417
+ @@gui[42].draw tile_w * 2, tile_h * 4, Z_UI
418
+ @@gui[43].draw tile_w * 3, tile_h * 4, Z_UI
419
+ end
420
+ if stars_goal == 0 then
421
+ # Or blank the line out of no stars are needed
422
+ 0.upto(3) { |x| @@gui[x + 4].draw tile_w * x, tile_h * 4, Z_UI }
423
+ end
424
+ # Remaining special peter time
425
+ if player.pmid == ID_PLAYER then
426
+ 0.upto(3) { |x| @@gui[x + 4].draw tile_w * x, tile_h * 5, Z_UI }
427
+ else
428
+ @@gui[14].draw tile_w * 0, tile_h * 5, Z_UI
429
+ @@gui[15].draw tile_w * 1, tile_h * 5, Z_UI
430
+ draw_digits.call time_left / TARGET_FPS, 5
431
+ end
432
+ # Ammo
433
+ @@gui[16].draw tile_w * 0, tile_h * 6, Z_UI
434
+ @@gui[17].draw tile_w * 1, tile_h * 6, Z_UI
435
+ draw_digits.call ammo, 6
436
+ # Bombs
437
+ @@gui[18].draw tile_w * 0, tile_h * 7, Z_UI
438
+ @@gui[19].draw tile_w * 1, tile_h * 7, Z_UI
439
+ draw_digits.call bombs, 7
440
+ # Remaining time for frozen lava
441
+ if map.lava_time_left == 0 then
442
+ 0.upto(3) { |x| @@gui[x + 4].draw tile_w * x, tile_h * 8, Z_UI }
443
+ else
444
+ @@gui[40].draw tile_w * 0, tile_h * 8, Z_UI
445
+ @@gui[41].draw tile_w * 1, tile_h * 8, Z_UI
446
+ draw_digits.call map.lava_time_left / TARGET_FPS, 8
447
+ end
448
+ # Spacing
449
+ 0.upto(3) { |x| @@gui[x + 4].draw tile_w * x, tile_h * 9, Z_UI }
450
+ end
451
+ end
452
+
453
+ # TODO Merge into cast_objects
454
+ def cast_single_fx pmid, num, x, y, w, h, vx, vy, randomness
455
+ num.times do
456
+ x = [[x - w / 2 + rand(w + 1), 0].max, 575].min
457
+ y = y - h / 2 + rand(h + 1)
458
+ fx = create_object(pmid, x, y, nil)
459
+ fx.vx = vx - randomness + rand(randomness * 2 + 1)
460
+ fx.vy = vy - randomness + rand(randomness * 2 + 1)
461
+ end
462
+ end
463
+ end