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,657 @@
1
+ class LivingObject < GameObject
2
+ attr_accessor :life, :action, :direction
3
+
4
+ def initialize *args
5
+ super
6
+
7
+ @life = ObjectDef[pmid].life
8
+ @action = 0
9
+ @direction = rand(2)
10
+ end
11
+
12
+ def act
13
+ return if game.frame == -1
14
+
15
+ case pmid
16
+ when ID_PLAYER_FIGHTER then
17
+ return if action > ACT_LAND
18
+ sound(:sword_whoosh).play
19
+ self.action = ACT_ACTION_1
20
+ tile_x = (x + 10 * direction.dir_to_vx) / TILE_SIZE
21
+ tile_y = y / TILE_SIZE
22
+ if game.map[tile_x, tile_y].between? TILE_BLOCKER, TILE_BLOCKER_3 then
23
+ if game.map[tile_x, tile_y] != TILE_BLOCKER_3 then
24
+ game.map[tile_x, tile_y] = TILE_BLOCKER_BROKEN
25
+ else
26
+ game.map[tile_x, tile_y] = TILE_BLOCKER_3_BROKEN
27
+ end
28
+ game.cast_objects ID_FX_BLOCKER_PARTS, 10, 0, -2, 5,
29
+ ObjectDef::Rect.new(tile_x * TILE_SIZE, tile_y * TILE_SIZE, TILE_SIZE, TILE_SIZE)
30
+ sound(:blocker_break).play
31
+ end
32
+ when ID_PLAYER_GUN
33
+ self.action = ACT_ACTION_1 if action < ACT_LAND and game.ammo > 0
34
+ when ID_PLAYER_BOMBER
35
+ self.action = ACT_ACTION_1 if action < ACT_LAND and game.bombs > 0
36
+ end;
37
+ end
38
+
39
+ def busy?
40
+ not blocked? DIR_DOWN or not action.between? ACT_STAND, ACT_WALK_4
41
+ end
42
+
43
+ def draw
44
+ return if [ACT_INV_UP, ACT_INV_DOWN].include? action
45
+
46
+ case pmid
47
+ when ID_PLAYER..ID_PLAYER_BOMBER then
48
+ # Animated wings while flying
49
+ if game.fly_time_left > 0 then
50
+ color = alpha([game.fly_time_left * 2 + 16, 255].min)
51
+ case direction
52
+ when DIR_LEFT then
53
+ EffectObject.images[38 + (game.frame / 2) % 4].draw x - 18, y - 12 - game.view_pos, 0, 0.75, 1, color, :additive
54
+ EffectObject.images[42 + (game.frame / 2) % 4].draw x, y - 12 - game.view_pos, 0, 1.00, 1, color, :additive
55
+ when DIR_RIGHT then
56
+ EffectObject.images[38 + (game.frame / 2) % 4].draw x - 24, y - 12 - game.view_pos, 0, 1.00, 1, color, :additive
57
+ EffectObject.images[42 + (game.frame / 2) % 4].draw x, y - 12 - game.view_pos, 0, 0.75, 1, color, :additive
58
+ end
59
+ end
60
+ # Transparency while invincible
61
+ if game.inv_time_left == 0 or type = ID_PLAYER_BERSERKER then
62
+ color = 0xffffffff
63
+ else
64
+ color = 0xa0ffffff
65
+ end
66
+ @@player_images ||= Gosu::Image.load_tiles 'media/player.bmp', -ACT_NUM, -10
67
+ @@player_images[ACT_NUM * (direction + (pmid - ID_PLAYER) * 2) + action].draw x - 11, y - 11 - game.view_pos, 0, 1, 1, color
68
+ when ID_ENEMY..ID_ENEMY_MAX then
69
+ if pmid == ID_ENEMY_GUN then
70
+ dir = x > game.player.x ? DIR_LEFT : DIR_RIGHT
71
+ else
72
+ dir = direction
73
+ end
74
+ @@enemy_images ||= Gosu::Image.load_tiles 'media/enemies.bmp', -ACT_NUM, -10
75
+ @@enemy_images[ACT_NUM * (dir + (pmid - ID_ENEMY) * 2) + action].draw x - 11, y - 11 - game.view_pos, 0
76
+ end
77
+ end
78
+
79
+ def hurt from_explosion
80
+ return if action == ACT_DEAD or pmid == ID_PLAYER_BERSERKER
81
+ damage = 3
82
+ damage -= 1 if pmid == ID_PLAYER_FIGHTER
83
+ if pmid <= ID_PLAYER_MAX then
84
+ if game.inv_time_left > 0 then
85
+ damage = from_explosion ? 1 : 0
86
+ end
87
+ if from_explosion or game.inv_time_left == 0 then
88
+ game.inv_time_left = [25, game.inv_time_left].max
89
+ end
90
+ end
91
+ self.life -= damage
92
+
93
+ if life < 1 then
94
+ self.action = ACT_DEAD
95
+ self.life = 0
96
+ else
97
+ self.action = ACT_PAIN_1 + rand(2)
98
+ end
99
+ case pmid
100
+ when ID_PLAYER..ID_PLAYER_MAX then
101
+ sound(:player_arg).play
102
+ when ID_ENEMY..ID_ENEMY_MAX then
103
+ emit_sound "arg#{rand(2) + 1}"
104
+ # TODO or Death sound
105
+ end
106
+
107
+ # if Data.OptBlood = 1 then CastObjects(ID_FXBlood, ToDoDamage * 8, 0, 2, 2, Data.OptEffects, GetRect(0, 0), Data.ObjEffects);
108
+
109
+ if pmid == ID_ENEMY_BOMBER and action == ACT_DEAD then
110
+ kill
111
+ game.cast_fx 10, 30, 10, x, y, 10, 10, 0, -10, 5
112
+ end
113
+ end
114
+
115
+ def hit
116
+ return if action == ACT_DEAD or pmid == ID_PLAYER_BERSERKER
117
+ return if pmid == ID_PLAYER_FIGHTER and rand(2) == 0
118
+
119
+ if pmid <= ID_PLAYER_MAX then
120
+ return if game.inv_time_left > 0
121
+ game.inv_time_left = [25, game.inv_time_left].max
122
+ end
123
+
124
+ self.life -= 1
125
+
126
+ if life < 1 then
127
+ self.action = ACT_DEAD
128
+ self.life = 0
129
+ else
130
+ self.action = ACT_PAIN_1 + rand(2)
131
+ end
132
+ case pmid
133
+ when ID_PLAYER..ID_PLAYER_MAX then
134
+ sound(:player_arg).play
135
+ when ID_ENEMY..ID_ENEMY_MAX then
136
+ emit_sound "arg#{rand(2) + 1}"
137
+ # TODO or Death sound
138
+ end
139
+
140
+ # if Data.OptBlood = 1 then CastObjects(ID_FXBlood, 8, 0, 2, 2, Data.OptEffects, GetRect(0, 0), Data.ObjEffects);
141
+
142
+ if pmid == ID_ENEMY_BOMBER and action == ACT_DEAD then
143
+ kill
144
+ game.cast_fx 10, 30, 10, x, y, 10, 10, 0, -10, 5
145
+ end
146
+ end
147
+
148
+ def update
149
+ # Runterfallen
150
+ fall if action < ACT_INV_UP
151
+
152
+ # Roasted by lava
153
+ if y + ObjectDef[pmid].rect.bottom > game.map.lava_pos then
154
+ game.cast_fx 8, 8, 0, x, y, 16, 16, 0, -4, 1
155
+ kill
156
+ emit_sound :shshsh
157
+ if action != ACT_DEAD then
158
+ emit_sound :player_arg if pmid <= ID_PLAYER_MAX
159
+ emit_sound "arg#{rand(2) + 1}" if pmid.between? ID_ENEMY, ID_ENEMY_MAX
160
+ # TODO bloody: emit_sound :death if pmid.between? ID_ENEMY, ID_ENEMY_MAX
161
+ end
162
+ return
163
+ end
164
+
165
+ # Ascending staircase
166
+ except_open_doors = (0..TILE_STAIRS_UP_LOCKED).to_a + [TILE_STAIRS_DOWN_LOCKED]
167
+ if action == ACT_INV_UP then
168
+ emit_sound :stairs_steps if rand(8) == 0
169
+ 2.times do
170
+ tile_below = game.map[x / TILE_SIZE, (y + 12) / TILE_SIZE]
171
+ tile_above = game.map[x / TILE_SIZE, (y - 9) / TILE_SIZE]
172
+ if except_open_doors.include? tile_below or except_open_doors.include? tile_above then
173
+ self.y -= 2
174
+ else
175
+ self.x = self.x / TILE_SIZE * TILE_SIZE + TILE_SIZE / 2 - 1
176
+ end
177
+ end
178
+ tile_below = game.map[x / TILE_SIZE, (y + 12) / TILE_SIZE]
179
+ tile_above = game.map[x / TILE_SIZE, (y - 9) / TILE_SIZE]
180
+ if except_open_doors.include? tile_below or except_open_doors.include? tile_above then
181
+ self.y -= 2
182
+ return
183
+ else
184
+ self.x = self.x / TILE_SIZE * TILE_SIZE + TILE_SIZE / 2
185
+ end
186
+ elsif action == ACT_INV_DOWN then
187
+ emit_sound :stairs_steps if rand(7) == 0
188
+ 3.times do
189
+ tile_below = game.map[x / TILE_SIZE, (y + 12) / TILE_SIZE]
190
+ tile_above = game.map[x / TILE_SIZE, (y - 9) / TILE_SIZE]
191
+ if except_open_doors.include? tile_below or except_open_doors.include? tile_above then
192
+ self.y += 2
193
+ else
194
+ self.x = self.x / TILE_SIZE * TILE_SIZE + TILE_SIZE / 2 - 1
195
+ end
196
+ end
197
+ tile_below = game.map[x / TILE_SIZE, (y + 12) / TILE_SIZE]
198
+ tile_above = game.map[x / TILE_SIZE, (y - 9) / TILE_SIZE]
199
+ if except_open_doors.include? tile_below or except_open_doors.include? tile_above then
200
+ self.y += 2
201
+ return
202
+ else
203
+ self.x = self.x / TILE_SIZE * TILE_SIZE + TILE_SIZE / 2
204
+ end
205
+ end
206
+
207
+ check_tile if action != ACT_DEAD
208
+
209
+ # Break tiles where due
210
+ rect = self.rect(0, 2)
211
+ break_floor rect.left, rect.bottom
212
+ break_floor rect.right, rect.bottom
213
+
214
+ return if action == ACT_DEAD
215
+
216
+ if in_water? then
217
+ game.create_object ID_FX_WATER_BUBBLE, x, y - 7, nil if rand(30) == 0
218
+ if pmid == ID_PLAYER_BERSERKER then
219
+ self.pmid = ID_PLAYER
220
+ game.cast_fx 8, 0, 0, x, y, 24, 24, 0, -1, 4
221
+ elsif pmid == ID_ENEMY_BERSERKER then
222
+ self.pmid = ID_ENEMY
223
+ game.cast_fx 8, 0, 0, x, y, 24, 24, 0, -1, 4
224
+ end
225
+ end
226
+
227
+ # Open doors
228
+ if pmid <= ID_PLAYER_MAX and game.keys > 0 then
229
+ # Left
230
+ tile_x, tile_y = (x + ObjectDef[pmid].rect.left - 1) / TILE_SIZE, y / TILE_SIZE
231
+ if game.map[tile_x, tile_y].between? TILE_CLOSED_DOOR, TILE_CLOSED_DOOR_3 then
232
+ game.map[tile_x, tile_y] -= (TILE_CLOSED_DOOR - TILE_OPEN_DOOR)
233
+ game.keys -= 1
234
+ sound("door#{rand(2) + 1}").play
235
+ end
236
+
237
+ # Right
238
+ tile_x, tile_y = (x + ObjectDef[pmid].rect.right + 1) / TILE_SIZE, y / TILE_SIZE
239
+ if game.map[tile_x, tile_y].between? TILE_CLOSED_DOOR, TILE_CLOSED_DOOR_3 then
240
+ game.map[tile_x, tile_y] -= (TILE_CLOSED_DOOR - TILE_OPEN_DOOR)
241
+ game.keys -= 1
242
+ sound("door#{rand(2) + 1}").play
243
+ end
244
+
245
+ # Up
246
+ tile_x, tile_y = x / TILE_SIZE, (y + ObjectDef[pmid].rect.top - 1) / TILE_SIZE
247
+ if game.map[tile_x, tile_y].between? TILE_CLOSED_DOOR, TILE_CLOSED_DOOR_3 then
248
+ game.map[tile_x, tile_y] -= (TILE_CLOSED_DOOR - TILE_OPEN_DOOR)
249
+ game.keys -= 1
250
+ sound("door#{rand(2) + 1}").play
251
+ end
252
+
253
+ # Down
254
+ tile_x, tile_y = x / TILE_SIZE, (y + ObjectDef[pmid].rect.bottom + 1) / TILE_SIZE
255
+ if game.map[tile_x, tile_y].between? TILE_CLOSED_DOOR, TILE_CLOSED_DOOR_3 then
256
+ game.map[tile_x, tile_y] -= (TILE_CLOSED_DOOR - TILE_OPEN_DOOR)
257
+ game.keys -= 1
258
+ sound("door#{rand(2) + 1}").play
259
+ end
260
+ end
261
+
262
+ # Special actions
263
+
264
+ # Plain Peter: Use levers
265
+ if pmid == ID_PLAYER and action == ACT_ACTION_3 and game.frame % 2 == 0 then
266
+ flip_lever
267
+ end
268
+
269
+ # Lord Peter: Stab
270
+ if pmid == ID_PLAYER_FIGHTER and action.between? ACT_ACTION_1, ACT_ACTION_5 then
271
+ rect = ObjectDef::Rect.new(x - 11 + direction.dir_to_vx * 6, y - 16, 22, 32)
272
+ if target = game.find_living(ID_ENEMY, ID_ENEMY_MAX, 0, ACT_PAIN_1 - 1, rect) then
273
+ target.hit
274
+ target.fling 5 * direction.dir_to_vx, -4, 1, true, true
275
+ if target.action == ACT_DEAD then
276
+ game.score += score = ObjectDef[target.pmid].life * 3
277
+ target.emit_text "*#{score}*"
278
+ end
279
+ end
280
+ end
281
+
282
+ # Archer Peter: Shoot at ACT_ACTION_5
283
+ # TODO or better 4, because the "action progress" code translation below is FUBAR
284
+ if pmid == ID_PLAYER_GUN and action == ACT_ACTION_4 and game.frame % 2 == 0 then
285
+ game.ammo -= 1
286
+ if target = game.launch_projectile(x, y + 2, direction, ID_ENEMY, ID_ENEMY_MAX) then
287
+ target.hurt(true)
288
+ target.fling 3 * direction.dir_to_vx * 3, -3, 1, true, true
289
+ if target.action == ACT_DEAD then
290
+ game.score += score = ObjectDef[target.pmid].life * 3
291
+ target.emit_text "*#{score}*"
292
+ end
293
+ end
294
+ end
295
+
296
+ if pmid == ID_PLAYER_BERSERKER then
297
+ game.cast_fx rand(2), 2 + rand(2), 0, x, y, 18, 24, 0, -3, 2
298
+ rect = self.rect(2, 2)
299
+ game.objects.each do |obj|
300
+ if obj.pmid.between? ID_ENEMY, ID_ENEMY_MAX and obj.action < ACT_DEAD and obj.collide_with? rect then
301
+ obj.hurt(false)
302
+ if obj.action = ACT_DEAD then
303
+ game.score += score = ObjectDef[obj.pmid].life * 3
304
+ obj.emit_text "*#{score}*"
305
+ end
306
+ end
307
+ end
308
+ end
309
+
310
+ if pmid == ID_ENEMY_BERSERKER then
311
+ game.cast_fx 0, rand(3), 0, x, y, 18, 24, 0, -2, 3
312
+ if game.player.action < ACT_DEAD and game.player.collide_with? rect(5, 2) then
313
+ game.player.hit
314
+ game.player.fling 8 * direction.dir_to_vx * 8, -3, 0, true, true
315
+ self.fling -8 * direction.dir_to_vx * 8, -4, 0, true, false
316
+ return
317
+ end
318
+ end
319
+
320
+ if pmid == ID_PLAYER_BOMBER and action == ACT_ACTION_4 and game.frame % 3 == 0 then
321
+ game.bombs -= 1
322
+ bomb = game.create_object(ID_FUSING_BOMB, x + direction.dir_to_vx * 5, y + 2, '0')
323
+ bomb.vx = direction.dir_to_vx * 8
324
+ bomb.vy = -3
325
+ end
326
+
327
+ if pmid <= ID_PLAYER_MAX and not busy? then
328
+ self.direction = DIR_LEFT if vx < 0
329
+ self.direction = DIR_RIGHT if vx > 0
330
+ end;
331
+
332
+ if pmid == ID_ENEMY_GUN and action == ACT_ACTION_4 then
333
+ self.action = ACT_ACTION_5 # TODO this was better before?!
334
+ shoot_dir = game.player.x < x ? DIR_LEFT : DIR_RIGHT
335
+ if target = game.launch_projectile(x, y + 2, shoot_dir, ID_PLAYER, ID_PLAYER_MAX) then
336
+ target.hit
337
+ target.fling 3 * shoot_dir.dir_to_vx, -3, 1, true, true
338
+ end
339
+ end
340
+
341
+ # "AI"
342
+ if pmid >= ID_ENEMY and not busy? then
343
+ self.direction = direction.other_dir if blocked? direction
344
+
345
+ # Run around
346
+ if pmid == ID_ENEMY_FIGHTER and game.frame % 100 < 70 or
347
+ pmid == ID_ENEMY_GUN and game.frame % 100 > 15 or
348
+ [ID_ENEMY, ID_ENEMY_BERSERKER, ID_ENEMY_BOMBER].include? pmid then
349
+ if game.map.solid?(x + direction.dir_to_vx * 7, y + ObjectDef[pmid].rect.bottom + 1)
350
+ self.vx += ObjectDef[pmid].speed * direction.dir_to_vx
351
+ else
352
+ if xdata and xdata[0, 1] == '1' then
353
+ jump
354
+ else
355
+ self.direction = direction.other_dir
356
+ end
357
+ end
358
+ end
359
+
360
+ # Occasionally use a floor tile
361
+ return use_tile if rand(100) == 0 and xdata and xdata[2, 1] == '1'
362
+
363
+ # Ambush player
364
+ if pmid == ID_ENEMY_FIGHTER and
365
+ game.player.action < ACT_DEAD and
366
+ game.player.collide_with? ObjectDef::Rect.new(x - 120 + direction * 120, y - 24, 120, 48) and
367
+ game.map.solid?(x + direction.dir_to_vx * 7, y + ObjectDef[pmid].rect.bottom + 1) then
368
+ self.vx = ObjectDef[pmid].speed * 2 * direction.dir_to_vx
369
+ end
370
+
371
+ # Shoot player if line of sight is free
372
+ if pmid == ID_ENEMY_GUN and game.player.action < ACT_DEAD and
373
+ rect(320, 1).include? game.player then
374
+ tiles_to_check = (game.player.x - x).abs / TILE_SIZE
375
+ if (0..tiles_to_check).none? { |i| game.map.solid?(i * TILE_SIZE + [game.player.x, x].min, y) } then
376
+ self.action = ACT_ACTION_1
377
+ end
378
+ end
379
+
380
+ # Collide with player while running
381
+ if game.player.action < ACT_PAIN_1 and collide_with? game.player.rect(1, -1) then
382
+ if pmid == ID_ENEMY_BOMBER then
383
+ game.player.hurt(true)
384
+ game.cast_fx 10, 30, 10, x, y, 10, 20, 0, -10, 5
385
+ kill
386
+ else
387
+ game.player.hit
388
+ fling -6 * direction.dir_to_vx, -2, 0, true, false
389
+ end
390
+ game.player.fling 8 * direction.dir_to_vx, -3, 0, true, true
391
+ return
392
+ end
393
+ end
394
+
395
+ # Collide with player while airborne
396
+ if pmid >= ID_ENEMY and [ACT_JUMP, ACT_LAND].include? action and
397
+ collide_with? game.player.rect(0, -1) then
398
+ if pmid == ID_ENEMY_BOMBER then
399
+ game.player.hurt(true)
400
+ game.cast_fx 10, 30, 10, x, y, 10, 0, -10, 5
401
+ kill
402
+ else
403
+ game.player.hit
404
+ fling -6 * direction.dir_to_vx, -2, 0, true, false
405
+ end
406
+ game.player.fling 8 * direction.dir_to_vx, -3, 0, true, true
407
+ return
408
+ end
409
+
410
+ # Fly
411
+ if pmid <= ID_PLAYER_MAX and game.fly_time_left > 0 and not (ACT_ACTION_1..ACT_ACTION_5).include? action then
412
+ self.action = vy < 0 ? ACT_JUMP : ACT_LAND
413
+ return
414
+ end
415
+
416
+ # Hurt player cannot recover until on the ground
417
+ return if [ACT_PAIN_1, ACT_PAIN_2].include? action and not blocked? DIR_DOWN and not in_water?
418
+
419
+ # Hingefallen, weiter aufstehen
420
+ if action.between? ACT_IMPACT_1, ACT_IMPACT_5 then
421
+ self.action -= 1 unless game.frame % 2 == 0
422
+ return unless action == ACT_IMPACT_1 and game.frame % 2 == 1
423
+ end
424
+
425
+ # Continue with special action
426
+ if action.between? ACT_ACTION_1, ACT_ACTION_5 then
427
+ case pmid
428
+ when ID_PLAYER, ID_PLAYER_GUN, ID_ENEMY_FIGHTER then slowness = 2
429
+ when ID_ENEMY_GUN then slowness = 8
430
+ when ID_PLAYER_BOMBER then slowness = 3
431
+ else
432
+ slowness = 1
433
+ end
434
+
435
+ return if game.frame % slowness != 0
436
+ self.action += 1
437
+ return unless action == ACT_ACTION_5
438
+ end
439
+
440
+ if not blocked? DIR_DOWN then
441
+ self.action = vy < 0 ? ACT_JUMP : ACT_LAND
442
+ return
443
+ end
444
+
445
+ if pmid <= ID_ENEMY_MAX and
446
+ game.map[x / TILE_SIZE, (y + ObjectDef[pmid].rect.bottom + 1) / TILE_SIZE].between? TILE_SLIME, TILE_SLIME_3 then
447
+ # Only when walking...
448
+ if (pmid <= ID_PLAYER_MAX and (left_pressed? or right_pressed?)) or
449
+ (pmid == ID_ENEMY_FIGHTER and game.frame % 100 < 70 or pmid == ID_ENEMY_GUN and game.frame % 100 > 15) then
450
+ self.action = ACT_WALK_1 + game.frame % 12 / 3
451
+ if pmid <= ID_PLAYER_MAX then
452
+ self.direction = DIR_LEFT if left_pressed?
453
+ self.direction = DIR_RIGHT if right_pressed?
454
+ end
455
+ if (y - game.player.y).abs < HEIGHT and rand(5) == 0 then
456
+ blob = game.create_object(ID_FX_FLYING_BLOB, x, y + ObjectDef[pmid].rect.bottom, nil)
457
+ blob.vx = rand(3) - 1
458
+ blob.vy = rand(3)
459
+ emit_sound "slime#{rand(3) + 1}"
460
+ end
461
+ return
462
+ end
463
+ end
464
+
465
+ # Walking animation
466
+ if blocked? DIR_DOWN and vx.abs > 0 then
467
+ self.action = ACT_WALK_1 + game.frame % 8 / 2
468
+ return
469
+ end
470
+
471
+ # "idlezeiten von ueber 4 wochen sind absolut eleet"
472
+ self.action = ACT_STAND
473
+ end
474
+
475
+ def jump
476
+ # Cannot jump when dead
477
+ return if action >= ACT_DEAD
478
+
479
+ if in_water? then
480
+ # Cannot jump when in deep water
481
+ return if ALL_WATER_TILES.include? game.map[x / TILE_SIZE, (y - 3) / TILE_SIZE]
482
+ else
483
+ # Cannot jump when busy
484
+ return if busy?
485
+ end
486
+
487
+ dir = DIR_UP
488
+ if pmid <= ID_PLAYER_MAX then
489
+ dir = self.direction = DIR_LEFT if left_pressed?
490
+ dir = self.direction = DIR_RIGHT if right_pressed?
491
+ elsif pmid.between? ID_ENEMY, ID_ENEMY_MAX then
492
+ dir = direction
493
+ end
494
+
495
+ if pmid <= ID_PLAYER_MAX and game.jump_time_left > 0 then
496
+ self.vy = (ObjectDef[pmid].jump_y * 1.7).round - 1
497
+ game.cast_objects ID_FX_SMOKE, 2, 0, 3, 2, rect(1, 0)
498
+ sound(:turbo).play
499
+ dir = DIR_UP
500
+ else
501
+ self.vy = ObjectDef[pmid].jump_y - 1
502
+ end
503
+
504
+ if dir == DIR_UP then
505
+ self.vx = 0
506
+ else
507
+ self.vx = dir.dir_to_vx * [ObjectDef[pmid].jump_x, vx.abs / 2].max
508
+
509
+ if game.map[x / TILE_SIZE, (y + ObjectDef[pmid].rect.bottom + 1) / TILE_SIZE].between? TILE_SLIME, TILE_SLIME_3 then
510
+ self.vx / 3
511
+ self.vy -= -2
512
+ end
513
+ end
514
+
515
+ if in_water? then
516
+ self.vx /= 3
517
+ self.vy += 1
518
+ emit_sound "water#{rand(2) + 1}"
519
+ end
520
+
521
+ self.action = ACT_JUMP
522
+ sound(:jump).play if pmid <= ID_PLAYER_MAX
523
+ end
524
+
525
+ def can_reach_lever?
526
+ game.find_object ID_LEVER, ID_LEVER_RIGHT, rect(10, 3)
527
+ end
528
+
529
+ def flip_lever
530
+ if target = can_reach_lever? then
531
+ sound(:lever).play
532
+ target.pmid =
533
+ case target.pmid
534
+ when ID_LEVER then ID_LEVER_DOWN
535
+ when ID_LEVER_LEFT then ID_LEVER_RIGHT
536
+ when ID_LEVER_RIGHT then ID_LEVER_LEFT
537
+ end
538
+
539
+ if not target.xdata.nil? and not target.xdata.empty? then
540
+ if target.xdata =~ /^[0-9A-F]/ then
541
+ target.xdata[0, 1].to_i(16).times do |i|
542
+ tile_x = target.xdata[2 + i * 10, 2].to_i(16)
543
+ tile_y = target.xdata[5 + i * 10, 3].to_i(16)
544
+ new_tile = target.xdata[9 + i * 10, 2].to_i(16)
545
+ old_tile = game.map[tile_x, tile_y]
546
+ game.map[tile_x, tile_y] = new_tile
547
+ target.xdata[9 + i * 10, 2] = '%02X' % old_tile
548
+ game.cast_fx 8, 0, 0, tile_x * TILE_SIZE + 10, tile_y * TILE_SIZE + 12, 24, 24, 0, 0, 2
549
+ end
550
+ elsif not target.xdata.nil? then
551
+ game.execute_script target.xdata[2..-1], 'do'
552
+ end
553
+ end
554
+ end
555
+ end
556
+
557
+ def use_tile
558
+ return if busy?
559
+
560
+ # Lever behind player
561
+ if pmid <= ID_PLAYER_MAX and can_reach_lever? then
562
+ if pmid == ID_PLAYER then
563
+ # only the normal player has an animation for that
564
+ self.action = ACT_ACTION_1
565
+ else
566
+ flip_lever
567
+ end
568
+ self.vx = 0
569
+ return
570
+ end
571
+
572
+ # Tile below player (magic floor tiles)
573
+ case map_tile = game.map[x / TILE_SIZE, (y + ObjectDef[pmid].rect.bottom + 1) / TILE_SIZE]
574
+ when TILE_ROCKET_UP, TILE_ROCKET_UP_2, TILE_ROCKET_UP_3 then
575
+ sound(:jump).play if pmid <= ID_PLAYER_MAX
576
+ emit_sound :turbo
577
+ self.vx = 0
578
+ self.vy = -21
579
+ self.y -= 1 unless blocked? DIR_UP
580
+ self.action = ACT_JUMP
581
+ game.cast_fx 0, 0, 10, x, y, 24, 24, 0, -10, 1
582
+ return
583
+ when TILE_ROCKET_UP_LEFT, TILE_ROCKET_UP_LEFT_2, TILE_ROCKET_UP_LEFT_3 then
584
+ sound(:jump).play if pmid <= ID_PLAYER_MAX
585
+ emit_sound :turbo
586
+ self.vx = -15
587
+ self.vy = -15
588
+ self.y -= 1 unless blocked? DIR_UP
589
+ self.action = ACT_JUMP
590
+ self.direction = DIR_LEFT
591
+ game.cast_fx 0, 0, 10, x, y, 24, 24, -8, -8, 1
592
+ return
593
+ when TILE_ROCKET_UP_RIGHT, TILE_ROCKET_UP_RIGHT_2, TILE_ROCKET_UP_RIGHT_3 then
594
+ sound(:jump).play if pmid <= ID_PLAYER_MAX
595
+ emit_sound :turbo
596
+ self.vx = +15
597
+ self.vy = -15
598
+ self.y -= 1 unless blocked? DIR_UP
599
+ self.action = ACT_JUMP
600
+ self.direction = DIR_RIGHT
601
+ game.cast_fx 0, 0, 10, x, y, 24, 24, +8, -8, 1
602
+ return
603
+ when TILE_MORPH_FIGHTER..TILE_MORPH_MAX
604
+ if pmid <= ID_PLAYER_MAX then
605
+ game.map[x / TILE_SIZE, (y + ObjectDef[pmid].rect.bottom + 1) / TILE_SIZE] = TILE_MORPH_EMPTY
606
+ sound(:morph).play
607
+ self.pmid = ID_PLAYER_FIGHTER + map_tile - TILE_MORPH_FIGHTER
608
+ game.time_left = ObjectDef[pmid].life unless pmid == ID_PLAYER
609
+ game.cast_fx 8, 0, 0, x, y, 24, 24, 0, -1, 4
610
+ emit_text "#{ObjectDef[pmid].name}!"
611
+ return
612
+ end
613
+ end
614
+
615
+ # Tile right behind player (doors etc.)
616
+ case game.map[x / TILE_SIZE, y / TILE_SIZE]
617
+ when TILE_STAIRS_UP_LOCKED then
618
+ return if pmid > ID_PLAYER_MAX or game.keys == 0
619
+ game.map[x / TILE_SIZE, y / TILE_SIZE] = TILE_STAIRS_UP
620
+ game.keys -= 1
621
+ sound("door#{rand(2) + 1}").play
622
+ use_tile
623
+ when TILE_STAIRS_UP..TILE_STAIRS_UP_2 then
624
+ return if not game.map.stairs_passable? x / TILE_SIZE, y / TILE_SIZE and pmid > ID_PLAYER_MAX
625
+ self.y = y / TILE_SIZE * TILE_SIZE
626
+ self.action = ACT_INV_UP
627
+ self.vx = self.vy = 0
628
+ emit_sound :stairs
629
+ when TILE_STAIRS_DOWN_LOCKED then
630
+ return if pmid > ID_PLAYER_MAX or game.keys == 0
631
+ game.map[x / TILE_SIZE, y / TILE_SIZE] = TILE_STAIRS_DOWN
632
+ game.keys -= 1
633
+ sound("door#{rand(2) + 1}").play
634
+ use_tile
635
+ when TILE_STAIRS_DOWN..TILE_STAIRS_DOWN_2 then
636
+ return if not game.map.stairs_passable? x / TILE_SIZE, y / TILE_SIZE
637
+ self.y = y / TILE_SIZE * TILE_SIZE + 13
638
+ self.action = ACT_INV_DOWN
639
+ self.vx = self.vy = 0
640
+ emit_sound :stairs
641
+ end
642
+ end
643
+
644
+ private
645
+
646
+ def break_floor x, y
647
+ debug binding if pmid == ID_PLAYER and $window.button_down? Gosu::KbD
648
+ if (TILE_BRIDGE..TILE_BRIDGE_4).include? game.map[x / TILE_SIZE, y / TILE_SIZE] and
649
+ not game.find_object(ID_FX_BREAK, ID_FX_BREAK_2,
650
+ ObjectDef::Rect.new(x / TILE_SIZE * TILE_SIZE, y / TILE_SIZE * TILE_SIZE, 24, 24)) then
651
+ game.create_object ID_FX_BREAK + rand(2),
652
+ x / TILE_SIZE * TILE_SIZE + 11,
653
+ y / TILE_SIZE * TILE_SIZE + 11, nil
654
+ emit_sound "break#{rand(2) + 1}"
655
+ end
656
+ end
657
+ end