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.
- data/COPYING +3 -0
- data/README.md +31 -0
- data/bin/petermorphose +3 -0
- data/levels/flgr_Der_Alkohol_und_seine_Folgen.pml +447 -0
- data/levels/jr_Am_Tempel_des_Harlow-Karlow.pml +413 -0
- data/levels/jr_Auf_der_Flucht.pml +456 -0
- data/levels/jr_Die_zwei_Baeume.pml +447 -0
- data/levels/jr_Feuertauchen.pml +343 -0
- data/levels/jr_Gemuetlicher_Aufstieg.pml +329 -0
- data/levels/jr_Gruenhuegelshausen.pml +423 -0
- data/levels/jr_Gruselgrotte.pml +421 -0
- data/levels/jr_Hoch_hinaus.pml +265 -0
- data/levels/jr_Vom_Ozean_in_die_Traufe.pml +342 -0
- data/levels/jr_Weg_durchs_Feuer.pml +544 -0
- data/levels/sl_Heimweg_zu_Henk.pml +307 -0
- data/media/arg1.wav +0 -0
- data/media/arg2.wav +0 -0
- data/media/arrow_hit.wav +0 -0
- data/media/blocker_break.wav +0 -0
- data/media/bow.wav +0 -0
- data/media/break1.wav +0 -0
- data/media/break2.wav +0 -0
- data/media/buttons.png +0 -0
- data/media/collect_ammo.wav +0 -0
- data/media/collect_freeze.wav +0 -0
- data/media/collect_health.wav +0 -0
- data/media/collect_key.wav +0 -0
- data/media/collect_points.wav +0 -0
- data/media/collect_star.wav +0 -0
- data/media/danger.png +0 -0
- data/media/death.wav +0 -0
- data/media/dialogs.bmp +0 -0
- data/media/door1.wav +0 -0
- data/media/door2.wav +0 -0
- data/media/eat.wav +0 -0
- data/media/effects.bmp +0 -0
- data/media/enemies.bmp +0 -0
- data/media/explosion.wav +0 -0
- data/media/game.ogg +0 -0
- data/media/gui.bmp +0 -0
- data/media/help1.wav +0 -0
- data/media/help2.wav +0 -0
- data/media/jump.wav +0 -0
- data/media/lava.wav +0 -0
- data/media/lever.wav +0 -0
- data/media/menu.ogg +0 -0
- data/media/morph.wav +0 -0
- data/media/player.bmp +0 -0
- data/media/player_arg.wav +0 -0
- data/media/shshsh.wav +0 -0
- data/media/skies.png +0 -0
- data/media/slime1.wav +0 -0
- data/media/slime2.wav +0 -0
- data/media/slime3.wav +0 -0
- data/media/stairs.wav +0 -0
- data/media/stairs_steps.wav +0 -0
- data/media/stuff.bmp +0 -0
- data/media/sword_whoosh.wav +0 -0
- data/media/tiles.bmp +0 -0
- data/media/title.png +0 -0
- data/media/title_dark.png +0 -0
- data/media/turbo.wav +0 -0
- data/media/water1.wav +0 -0
- data/media/water2.wav +0 -0
- data/media/whoosh.wav +0 -0
- data/media/whoosh_back.wav +0 -0
- data/media/won.bmp +0 -0
- data/media/yippie.wav +0 -0
- data/src/const.rb +301 -0
- data/src/en.yml +62 -0
- data/src/gosu-preview.rb +116 -0
- data/src/helpers/audio.rb +9 -0
- data/src/helpers/graphics.rb +24 -0
- data/src/helpers/input.rb +28 -0
- data/src/ini_file.rb +25 -0
- data/src/level_info.rb +63 -0
- data/src/localization.rb +19 -0
- data/src/main.rb +86 -0
- data/src/map.rb +136 -0
- data/src/objects/collectible_object.rb +174 -0
- data/src/objects/effect_object.rb +120 -0
- data/src/objects/game_object.rb +363 -0
- data/src/objects/living_object.rb +657 -0
- data/src/objects/object_def.rb +45 -0
- data/src/script.rb +207 -0
- data/src/states/credits.rb +24 -0
- data/src/states/game.rb +463 -0
- data/src/states/help.rb +268 -0
- data/src/states/level_selection.rb +43 -0
- data/src/states/menu.rb +45 -0
- data/src/states/options.rb +81 -0
- data/src/states/state.rb +11 -0
- data/src/states/title.rb +17 -0
- data/src/states/victory.rb +20 -0
- 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
|
data/src/states/game.rb
ADDED
@@ -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
|