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,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