petermorphose 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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