forked from frogatto/frogatto
/
hittable.cfg
637 lines (505 loc) · 38 KB
/
hittable.cfg
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
{
id: "hittable",
prototype: ["standard_values"],
is_strict: true,
collide_dimensions: ["player","~enemy","~hazard"],
mass: 5,
properties: {
#-------------------------- vars --------------------------#
time_last_hit: { type: "int", default: 0, persistent: false },
attributes: { type: "{string->string}", default: {}, persistent: false },
is_a_boss: { type: "bool", default: false, persistent: true },
#-------------------------- temporary vars --------------------------#
_in_solidity_fail: { type: "bool", default: false, persistent: false },
_last_played_pain_sfx: { type: "int", default: 0, persistent: false },
_last_entered_water: { type: "int", default: 0, persistent: false },
_attaches: { type: "[obj hittable_attache]", default: [] },
#-------------------------- constants --------------------------#
team: "string :: 'evil'",
teams_which_wont_get_hurt_by_me: "[string] :: []", //a special exemption; used on e.g. the shockwave airplane boss bomb, to keep it from killing the bridge
swallowable: "bool :: false",
is_a_flier: "bool :: false",
flinch_threshold: "int :: 3",
hurt_velocity_y: "int :: -400",
hurt_velocity_x: "int :: -200",
attack_knockback: "int :: 0",
attack_damage: "int :: 0",
attack_damage_to_player: "int :: 0",
damage_type: "string :: 'neutral'", //options include fire, acid, energy
armor: "int :: 0",
posthit_invicibility_period: "int :: 0",
damage_cooldown: "int :: 0",
sourceless_damage_cooldown: "int :: 20",
points_value: "int :: 0",
material_sound: "string :: 'default'",
physical_size: "int :: 16",
is_player_body_part: "bool :: false",
taxonomy: "string :: 'neutral'", //e.g. plant, bug, fish, etc. Used for damage type reductions.
basic_type: "string :: me.type",
frogourmet_tag: "string :: me.basic_type",
thrown_type: "string :: me.basic_type",
#-- bestiary description --#
//These properties can be overridden with a ~translatable string~ in an individual object.
//Title of object is currently generated, but this probably won't translate. One fix would be to do a script to apply this automatically to all the relevant enemies .cfg files.
//title: "string :: fold(map(reverse(split(type, '_')), (cap_map[value[0]] or (string<-value[0])) + value[1:]), a + ' ' + b)
// where cap_map = {a:'A',b:'B',c:'C','d':'D',e:'E',f:'F',g:'G',h:'H',i:'I',j:'J',k:'K',l:'L',m:'M',n:'N',o:'O',p:'P',q:'Q',r:'R',s:'S',t:'T',u:'U',v:'V',w:'W',x:'X',y:'Y',z:'Z'}",
title: "string :: 'missing title'",
description: "string :: dump('no desc for: ${type}.cfg ', 'Error, no description for ${type}.\nPlease define a description property.\nReported from hittable.cfg:description.')",
taste: "string :: ''", //Optional, as not everything can be tasted.
#-- shot includes --#
deflectable_via_attacks: "bool :: if(team = 'player', false, true)",
dies_upon_dealing_damage: "bool :: true",
is_a_shot: "bool :: false",
goes_through_enemy_shots: "bool :: false",
#-- special flags for specific objects --#
affects_ethereal_block_triggers: "bool :: true",
#-------------------------- core behavior handlers --------------------------#
get_hit_by: "def(obj hittable collide_with) -> commands execute(me, [
handle_special_pre_damage_reaction(collide_with),
if((collide_with.attack_damage > 0) and collide_with.hitpoints > 0 and collide_with.damage_cooldown < (level.cycle - time_last_hit),
[if((not is_invincible) and collide_with.attack_damage > armor,
[display_posthit_invincibility_flash_sequence(),
handle_flinch(collide_with),
handle_damage(collide_with),
set(time_last_hit, level.cycle),
])]),
handle_knockback(collide_with)])",
get_hit_sourceless: "def(string damage_type, int damage_amount) -> commands execute(me, /*take damage and invoke the usual behavior, but without having a source object available to refer to.
This almost universally should be used for e.g. being stomped on, or other self-determined damage amounts (falling?).
It's also used for thrown damage because the throwee is dead and would (potentially) be a null reference by the time the latter collision was processed.*/
if((not is_invincible) and (damage_amount >= armor) and (sourceless_damage_cooldown < (level.cycle - time_last_hit)),
[display_posthit_invincibility_flash_sequence(),
handle_damage_sourceless(damage_type, damage_amount),
/* specifically skip flinching, because kitties don't do it. TODO: Might be something to later reconsider. */
set(time_last_hit, level.cycle),
]))",
/*
Notes RE: handle_body_collision:
The business about "all_collisions" and "collision_index" is meant to ensure we don't take multple instances of damage from a damage type that's a stream/flurry in a single frame. If we get hit, we check for other collisions with the same kind of shot, and only take damage from the first one.
We double-check it's the same collide-with area, because we DO want multiple collisions from an object for each different area; we use this on e.g. the milgram-pod to allow it to be shot out of the air by the player (needs a body area and a thrown area during the thrown anim, rather than the usual "only the thrown area" for player-spat objects). This mechanism may be the ideal place to check for 'armored regions' on an otherwise vulnerable creature; check if we're getting a collision on both the body and the armor.
TODO: this may be unwanted on shots without a cooldown, where a "shotgun" effect of multiple hits is desired.
Notes RE: process_collision:
There are two special exceptions here besides the 'no friendly-fire' rule - evil_harmless is a special team for thrown enemies wherein they can't hurt anyone, regardless of the target's team, but also - stuff from team 'evil' won't friendly-fire them. They can and will be hurt by any player actions, though, and any traps/neutral damage sources.
*/
handle_body_collision: "def(custom_obj collide_with, string collide_with_area, [builtin user_collision_callable]|null all_collisions=null, int|null collision_index=null) -> commands
if(collide_with is obj hittable, if(all_collisions != null and collision_index != null, if(not find( filter(all_collisions, value.collide_with.type = collide_with.type and value.collide_with_area = collide_with_area), value.collision_index < collision_index), process_collision), process_collision)
where process_collision = if(collide_with.team != team and (not team in collide_with.teams_which_wont_get_hurt_by_me) and collide_with.team != 'evil_harmless' and (not (collide_with.team = 'evil' and team = 'evil_harmless')), if(collide_with_area in ['attack','thrown'], get_hit_by(collide_with))))",
#-------------------------- attache logic --------------------------#
me_and_attaches: "[obj hittable|obj hittable_attache] :: [me] + _attaches",
handle_grabbed_cleanup: "commands :: map(_attaches, remove_object(value))",
#-------------------------- generic helper functions --------------------------#
turn_towards_player: "commands :: if(not facing_towards_player, turn_around)",
turn_around: "commands :: set(facing,-facing)", //this is *basically* virtual in all usage, but sensible behavior here doesn't hurt
direction_towards_player: "if(midpoint_x - level.player.midpoint_x > 0, -1, 1)",
facing_towards_player: "bool :: facing = if(level.player.midpoint_x < self.midpoint_x, -1, 1)",
attempt_animation: "def(string anim_name, string fallback_anim) -> commands execute(me, if(anim_name in available_animations, set(animation, anim_name), set(animation, fallback_anim)))",
/*
Notes RE: replacement_object_with_preserved_attributes:
Our current idiom for changing objects from one type to another is to delete the object of type A, and spawn a new object of type B "in situ"; with only the properties we explicitly want to preserve, preserved. Our previous system preserved all stored-data properties, and simply changed the type name (thus replacing only the evaluatable events/properties). We ran into a *lot* of issues where preserved state had lots of unexpected side-effects (for an easy example, some objects would set fall_through_platforms or solid/collide dimensions, and not have them get unset upon swapping to a different type (since the new type had none of the cleanup code)). This new system isn't perfect, but it's a heck of a lot less stateful, which kills a lot of bugs.
*/
replacement_object_with_preserved_attributes: "def(obj hittable source_object, string dest_obj_type, int|null new_hitpoints=null) -> obj hittable
(obj hittable <- object(dest_obj_type, source_object.mid_x, source_object.mid_y, {facing: source_object.facing, hitpoints: if(new_hitpoints != null, new_hitpoints, source_object.hitpoints), attributes:source_object.attributes, variations: source_object.variations, event_handlers:source_object.event_handlers}))",
#-------------------------- behavior handlers --------------------------#
# anims/movement
handle_special_pre_damage_reaction: "def(obj hittable|null collide_with) -> commands null #virtual#", //meant for unique actions that happen upon getting hit, but take place BEFORE taking damage, and can potentially abort the whole damage chain - right now this isn't quite in the right position to do this (it can only do so by replacing the object before damage is dealt at the end of processing), but we'll reposition it when need arises to actually use this, and make the rest of the damage chain an executable parameter. Good uses of this include becoming invincible in response to taking damage without actually taking damage, or setting off a timed explosion, etc.
handle_special_damage_response: "def(string damage_type, int final_damage_amount, bool will_die) -> commands null #virtual#", //meant for unique actions upon taking damage, like losing wings. Also for special type-based behavior that happens regardless of damage amount (such as maybe a feathered creature having its feathers burned off and effectively turning into a different, flightless enemy type). Because this has the potential to be highly conditional on whether the creature dies or not, we pass in a bool for death (some responses like losing feathers should always happen; some responses like extruding spikes and hurting the player should only happen if the creature doesn't die).
handle_flinch: "def(obj hittable collide_with) -> commands
execute(me, if(final_damage_amount(collide_with, collide_with.attack_damage) >= flinch_threshold, if(me.is_a_flier, cause_flinch(collide_with), if(is_standing, cause_flinch(collide_with))) ))",
cause_flinch: "def(obj hittable|null collide_with) -> commands execute(me, [
add(me.velocity_x,me.hurt_velocity_x * sign(me.midpoint_x - if(collide_with, collide_with.midpoint_x, 0))),
add(me.velocity_y,me.hurt_velocity_y),
cause_hurt_anim(collide_with)
])",
cause_hurt_anim: "def(obj hittable|null collide_with) -> commands execute(me, if('hurt' in available_animations,set(me.animation, 'hurt')))",
player_damage_response: "def(string damage_type, int amount) -> commands null", //player objects will always do special responses to damage (like making the screen flash), besides the built-in stuff
enter_water_only_once_per_frame: "execute(me, bind_command(def() [if(level.cycle > _last_entered_water, [set(_last_entered_water, level.cycle), bind_command(def() enter_water)] )]))",
enter_water: "commands :: null",
elastic_collision: "def( obj hittable the_other_object, {multiplier: decimal, constraint: {min: decimal, max: decimal}|null} params ) -> commands
([set(velocity_x, radius * cos(angle)), set(velocity_y, radius * sin(angle))]
where angle = lib.math.angle(the_other_object, me)
where radius = if(params.constraint, lib.math.constrain(params.constraint.min, params.multiplier * velocity_magnitude , params.constraint.max), params.multiplier * velocity_magnitude) )
where velocity_magnitude = hypot(velocity_x,velocity_y)",
/*there's a sharp loss of precision as this goes through the calculations, even though the resulting angles from the calcs are right. I've beaten my head against this and can't, for the life of me, figure out what's going wrong; I don't know if it's due to data type precision, or what. So what we're doing instead is figuring out how much we've deviated from the correct "conservation of momentum" velocity magnitude, and we use that as a fudge-factor to multiply the values back up into the right ballpark area. It's a horrible hack ... but that's the price of getting things done. */
two_party_elastic_collision: "def( obj movable_enemy|obj throwable other_object, decimal multiplier=1.0) -> commands
internal_two_party_elastic_collision( (obj hittable <- other_object), multiplier)",
internal_two_party_elastic_collision: "def( obj hittable other_object, decimal multiplier=1.0 ) -> commands
if(cycle - _last_elastic_collided > 0 and cycle - other_object._last_elastic_collided > 0,
[
set(velocity_x, (my_new_velocity.x) * fudge_factor * multiplier),
set(velocity_y, (my_new_velocity.y) * fudge_factor * multiplier),
set(other_object.velocity_x, (others_new_velocity.x) * fudge_factor * multiplier),
set(other_object.velocity_y, (others_new_velocity.y) * fudge_factor * multiplier),
set(_last_elastic_collided, cycle),
set(other_object._last_elastic_collided, cycle)
])
where fudge_factor = 1.002 * ((abs(hypot(velocity_x,velocity_y))+abs(hypot(other_object.velocity_x,other_object.velocity_y))) /
((abs(hypot(my_new_velocity.x,my_new_velocity.y)) + abs(hypot(others_new_velocity.x,others_new_velocity.y))))),
where my_new_velocity = { x: cos(angle_of_incidence) * my_new_velocity_in_rotated_coords.x + cos(opp_angle_of_incidence) * my_new_velocity_in_rotated_coords.y,
y: sin(angle_of_incidence) * my_new_velocity_in_rotated_coords.x + cos(opp_angle_of_incidence) * my_new_velocity_in_rotated_coords.y },
where others_new_velocity = { x: cos(angle_of_incidence) * others_new_velocity_in_rotated_coords.x + cos(opp_angle_of_incidence) * others_new_velocity_in_rotated_coords.y,
y: sin(angle_of_incidence) * others_new_velocity_in_rotated_coords.x + cos(opp_angle_of_incidence) * others_new_velocity_in_rotated_coords.y },
where my_new_velocity_in_rotated_coords = { x: (my_velocity_in_rotated_coords.x * (my_mass - others_mass) + others_velocity_in_rotated_coords.x * (my_mass + others_mass)) / (my_mass + others_mass),
y: my_velocity_in_rotated_coords.y },
where others_new_velocity_in_rotated_coords = { x: (my_velocity_in_rotated_coords.x * (my_mass + others_mass) + others_velocity_in_rotated_coords.x * (others_mass - my_mass)) / (my_mass + others_mass),
y: others_velocity_in_rotated_coords.y },
where my_velocity_in_rotated_coords = { x: my_vel_mag * cos(my_angle_of_motion - angle_of_incidence), y: my_vel_mag * sin(my_angle_of_motion - angle_of_incidence)}
where my_vel_mag = hypot(velocity_x,velocity_y)
where my_angle_of_motion = atan2( velocity_y, velocity_x )
where others_velocity_in_rotated_coords = { x: others_vel_mag * cos(others_angle_of_motion - angle_of_incidence), y: others_vel_mag * sin(others_angle_of_motion - angle_of_incidence)}
where others_vel_mag = hypot(other_object.velocity_x,other_object.velocity_y)
where others_angle_of_motion = atan2( other_object.velocity_y, other_object.velocity_x )
where my_mass = 1.0 where others_mass = 1.0
where angle_of_incidence = lib.math.angle(other_object, me)
where opp_angle_of_incidence = lib.math.angle(me, other_object)",
_last_elastic_collided: { type:"int", default: 0 },
# damage
//meant for applying any kind of arithmetic to the raw damage amount, based on type.
//this is what you overload if you want to have an exception to the damage tables (i.e. for an unusual weakness; as opposed to a usual one for a given type)
handle_custom_damage_type_modifications: "def(string damage_type, int amount) -> int|null null #virtual#",
//only overload this if you want to completely annul the damage tables for this object (i.e. for complete immunity to all but one damage type)
handle_damage_type_modifications: "def(string damage_type, int amount) -> int
if(handle_custom_damage_type_modifications(damage_type,amount) != null, int <- handle_custom_damage_type_modifications(damage_type,amount),
if(uses_custom_damage_table, int(custom_damage_table[damage_type] * amount),
if(me.taxonomy in keys(damage_tables), int(damage_tables[me.taxonomy][damage_type] * amount), amount
)))", //the neutral case automatically falls through
uses_custom_damage_table: "bool :: false", //cheap trick: if you just want something to take 100% damage from all damage types, you can use this without redefining the 'custom_damage_table', which happens to just have 100% as the value for everything.
//meant for flat, type-agnostic reductions to ALL damage.
handle_base_damage_reductions: "def(int amount) -> int amount #virtual#",
final_damage_amount: "def(interface {damage_type: string} collide_with, int damage_amount) -> int if((damage_amount < armor) or me.is_invincible, 0, handle_damage_type_modifications(collide_with.damage_type, handle_base_damage_reductions( damage_amount )))",
will_be_dead: "def(int damage_amount) -> bool ((me.hitpoints - damage_amount) <= 0)",
handle_damage_sourceless: "def(string damage_type, int amount) -> commands execute(me,[handle_special_damage_response(damage_type, dmg, will_be_dead(dmg)), if(will_be_dead(dmg), handle_death({damage_type:damage_type})), add(me.hitpoints, -dmg), display_hurt_visuals({damage_type:damage_type},dmg), player_damage_response(damage_type,dmg)])
where dmg = final_damage_amount({damage_type:damage_type}, amount)",
handle_damage: "def(obj hittable collide_with) -> commands execute(me,[handle_special_damage_response(collide_with.damage_type, dmg, will_be_dead(dmg)), if(will_be_dead(dmg), handle_death(collide_with)), add(me.hitpoints, - dmg), display_hurt_visuals(collide_with,dmg), player_damage_response(collide_with.damage_type,dmg)])
where dmg = final_damage_amount(collide_with, attack_damage_amount)
where attack_damage_amount = if((me is obj player_controlled) and collide_with.attack_damage_to_player, collide_with.attack_damage_to_player, collide_with.attack_damage)",
handle_knockback: "def(obj hittable collide_with) -> commands execute(me,add(velocity_x, behind_or_in_front * collide_with.attack_knockback) where behind_or_in_front = sign(me.mid_x - collide_with.mid_x) )",
custom_damage_table: "{neutral:decimal, fire:decimal, energy:decimal, arcane:decimal, acid:decimal, impact:decimal, lacerate:decimal} ::
{ neutral: 1.0,
fire: 1.0,
energy: 1.0,
arcane: 1.0,
acid: 1.0,
impact: 1.0,
lacerate: 1.0
}",
/*
some rationales for these types:
neutral: is the grab-bag for unaffiliated attacks that always do full damage, including self-damage, etc.
fire, energy, acid, arcane: are all justified by allowing each of frogatto's attacks to have different barriers to break (which lock off major areas of the game). They're also used to e.g. have some enemies completely immune to the basic thrown attacks, which are of the "impact" type, forcing the player to experiment with different damage types. Generally all enemies have a particular weakness.
impact, lacerate: exist to give basic physical contacts different behaviors; we have e.g. hard-shelled bugs which aren't harmed much by scratches, but are crushed by impact.
*/
damage_tables: "{ string -> {neutral:decimal, fire:decimal, energy:decimal, arcane:decimal, acid:decimal, impact:decimal, lacerate:decimal} } ::
{
'bug' : { neutral: 1.0,
fire: 0.75,
energy: 1.0,
arcane: 1.0,
acid: 1.25,
impact: 2.0,
lacerate: 0.25
},
'plant' : { neutral: 1.0,
fire: 1.0,
energy: 0.5,
arcane: 1.0,
acid: 0.25,
impact: 0.0,
lacerate: 0.5
},
'mushroom' : { neutral: 1.0,
fire: 1.0,
arcane: 2.0,
energy: 1.5,
acid: 0.0,
impact: 0.0,
lacerate: 1.0
},
'stone' : { neutral: 1.0,
fire: 0.0,
energy: 0.1,
arcane: 0.1,
acid: 2.0,
impact: 1.0,
lacerate: 0.0
},
'milgramen' : { neutral: 1.0,
fire: 0.75,
energy: 1.0,
arcane: 1.0,
acid: 0.75,
impact: 1.0,
lacerate: 1.0
},
'fish' : { neutral: 1.0,
fire: 0.0,
energy: 1.5,
arcane: 1.0,
acid: 0.5,
impact: 0.5,
lacerate: 1.0
},
'mechanical' : {neutral: 1.0,
fire: 0.0,
energy: 1.0,
arcane: 1.0,
acid: 1.5,
impact: 0.5,
lacerate: 0.0
},
'spectral' : { neutral: 1.0,
fire: 0.0,
energy: 1.0,
arcane: 1.0,
acid: 0.0,
impact: 0.0,
lacerate: 0.0
},
'mammal' : { neutral: 1.0,
fire: 1.0,
energy: 0.5,
arcane: 0.5,
acid: 1.5,
impact: 1.0,
lacerate: 1.0
},
'bird' : { neutral: 1.0,
fire: 0.5,
energy: 0.5,
arcane: 0.5,
acid: 0.5,
impact: 1.0,
lacerate: 1.5
},
}",
# death
should_track_death: "bool :: false", //tracking is for achievements
handle_death: "def(interface {damage_type: string} collide_with) -> commands [
add(level.player.score,points_value),
if(not acquirable_item_drop_value = null,drop_acquirable_items()),
if(should_track_death, level.player.register_kill(me)),
if(me.corpse_object_type, spawn_corpse),
map(me._attaches, remove_object(value)),
death_effects( if(collide_with.damage_type in death_fx_damage_types_that_have_animations and (not death_fx_ignore_damage_type), collide_with.damage_type, death_fx_type) )
] asserting level.player is obj player_controlled",
spawn_corpse: "commands :: if(corpse_object_type is string, spawn(corpse_object_type, mid_x, mid_y, facing))",
corpse_object_type: "string|null :: null",
death_fx_type: "string :: if(me.taxonomy in keys(death_fx_table), death_fx_table[me.taxonomy], 'medium')",
death_fx_ignore_damage_type: "bool :: false", //certain bosses always have the same death effects, rather than having "special" ones according to what they got damaged by.
death_fx_damage_types_that_have_animations: "['fire', 'acid', 'energy']", //we don't and won't have special animations for all damage types, only for "flashy" ones like fire and such.
death_fx_table: "{string->string} :: {
'bug': 'bug',
'plant': 'plant',
'mushroom': 'mushroom',
'mammal': 'animal',
'bird': 'animal',
'fish': 'animal',
'milgramen': 'milgramen',
'stone': 'medium',
'mechanical': 'medium',
'spectral': 'medium',
}",
# invincibility
is_invincible_posthit: "bool :: if(time_last_hit and (abs(time_last_hit - level.cycle) < posthit_invicibility_period), true, false)",
is_invincible: "bool :: if(invincible or level.in_dialog or is_invincible_posthit, true, false)",
#-------------------------- item drop logic --------------------------#
drop_item_list: "{string -> int} :: sans_undroppables(_drop_item_list)",
drop_item_weights: "{string -> int} :: sans_undroppables(_drop_item_weights)",
_drop_item_list: "{string -> int} :: {'heart_object' : 10, 'mana_cube' : 7}",
_drop_item_weights: "{string -> int} :: {'heart_object' : 30, 'mana_cube' : 70}",
drop_item_validity: "{string -> bool} :: {'heart_object' : (not level.player.hitpoints = level.player.max_hitpoints), 'mana_cube' : true }",
acquirable_item_drop_value: "int :: 0", //how many of the dropped items the dying monster/thing is worth.
sans_undroppables: "def({string -> int} source) -> {string -> int} filter(source, drop_item_validity[key])",
choose_drop_item_nonweighted: "string :: choose(keys(drop_item_list))", //unused but useful reference
choose_drop_item_weighted: "string :: search_drop_list(drop_item_weights, 0, 0, 1d(sum(values(drop_item_weights)))) ",
search_drop_list: "def({string -> int} thelist, int i, int tally, int target_val) -> string if(tally >= target_val, keys(thelist)[i-1], search_drop_list( thelist, i+1, tally + values(thelist)[i], target_val))",
calculate_drop_items: "def(int value_left, [string] toBeDropped) -> [string]
if( anything_can_still_be_dropped, calculate_drop_items(value_left - drop_item_list[picked], toBeDropped + [picked]), toBeDropped)
where picked = choose_drop_item_weighted
where anything_can_still_be_dropped = (find(values(drop_item_list), value <= value_left) != null)",
drop_acquirable_items: "def() -> commands if((not higher_difficulty) or 1d4=4,
map(calculate_drop_items(me.acquirable_item_drop_value,[]), spawn(value, me.mid_x, me.y, me.facing,[set(child.velocity_x, velocity_x/6 +1d600-300), set(child.velocity_y, velocity_y/6)])))",
#-------------------------- cosmetic functions --------------------------#
play_grabbed_cosmetics: "commands :: [play_hurt_sounds('neutral',1), play_object_specific_grabbed_cosmetics]",
play_object_specific_grabbed_cosmetics: "commands :: null",
display_hurt_visuals: "def(interface {damage_type: string} collide_with, int amount) -> commands execute(me,
[
play_hurt_sounds(collide_with.damage_type, amount),
display_damage_type_particles(collide_with.damage_type, amount),
if(amount > 0, hurt_flash_sequence, invincible_flash_sequence)
]
)",
invincible_flash_sequence: "commands :: [ flash_blue,
schedule(5, flash_off),
schedule(6, flash_blue),
schedule(8, flash_off)]",
hurt_flash_sequence: "commands :: [ flash_bright,
schedule(3, flash_red),
schedule(6, flash_bright),
schedule(9, flash_red),
schedule(12, flash_bright),
schedule(15, flash_off)]",
display_damage_type_particles: "def(string damage_type, decimal damage_amount) -> commands
if(damage_amount > 0,
switch(damage_type,
'acid', spawn('acid_burn_particles',mid_x,mid_y,1,set(child.parent,me)),
'neutral', null
),
)",
//these should be the material-interaction sounds of an object being damaged; wood crunching, flesh squishing, glass breaking, etc. A certain material will make different noise depending on what hurts it, and that's what this handles - wood burning is a very different sound from wood being crushed. By default we provide a set of sounds for fleshy objects.
play_hurt_sounds: "def(string damage_type, decimal damage_amount) -> commands [if(play_object_specific_hurt_sounds(damage_type, damage_amount) != null, play_object_specific_hurt_sounds(damage_type, damage_amount),
if(damage_amount > 0,
switch(damage_type,
'bite', sound('hurt-bite.wav'),
'stab', sound('hurt-stab'+1d2+'.wav'),
'slash', sound('hurt-slash'+1d3+'.wav'),
'organic-bludgeon', sound('hurt-organic-bludgeon.wav'),
'neutral', null,
'impale', sound ('Death-Spikes.wav'),
'electricity', sound('hurt-electricity.wav', 0.5)
),
switch(me.taxonomy,
'plant', sound('resist-plant.wav'),
'neutral', null
),
)),
if(should_play_pain_sfx,[play_object_specific_pain_vocalization(damage_type, damage_amount), set(_last_played_pain_sfx,level.cycle)])]
where should_play_pain_sfx = (level.cycle >= (_last_played_pain_sfx + 28))",
//override this to allow an object to have its own specific material sounds
play_object_specific_hurt_sounds: "def(string damage_type, damage_amount) -> commands switch(damage_type,
'neutral', null,
null)",
//generally speaking, these sounds should not differ between damage types. If a creature yelps in pain, it should always sound the same. We might want it to be a matter of magnitude, though.
play_object_specific_pain_vocalization: "def(string damage_type, damage_amount) -> commands null",
display_posthit_invincibility_flash_sequence: "def() -> commands if(posthit_invicibility_period,
map(range(me.posthit_invicibility_period/2), 'step' ,schedule(step*2, if(step%2=0, map(me_and_attaches, set(value.alpha,50)) , map(me_and_attaches, set(value.alpha,255))) ) ) )",
flash_bright: "commands :: map(me_and_attaches, [set(value.brightness, 1023)])",
flash_blue: "commands :: map(me_and_attaches, [set(value.red, 50),set(value.green, 50), set(value.blue, 175)])",
flash_red: "commands :: map(me_and_attaches, [set(value.red, 255), set(value.green, 100), set(value.blue, 100)])",
flash_off: "commands :: map(me_and_attaches, [set(value.brightness, 255), set(value.red, 255), set(value.green, 255), set(value.blue, 255)])",
death_effects: "def(string type) -> commands
if(me.underwater, splash_effect(),
(switch(type,
'none', null, //for creatures that specifically want to die without showing anything
/*these are a set of effects based on the type of creature dying*/
'bug', [vfx('die_cloud_puffy'), sound('death-crunch'+1d2+'.wav')],
'mushroom', [vfx('die_cloud_evaporative'), sound('death-crunch'+1d2+'.wav')],
'plant', [vfx('die_cloud_evaporative'), sound('Death-Plant-Short'+1d5+'.wav')],
'animal', [vfx('die_cloud'), sound('death-crunch'+1d2+'.wav'), spawn_gibs('bone_straight',1d2), spawn_gibs('bone_skull',1)],
'milgramen', if(victim_is_big, repeat_fx('die_cloud',10,5,50,70, sound('Milgramen-Explode'+1d5+'.wav')),
[vfx('die_cloud'), sound('Milgramen-Explode'+1d5+'.wav')]),
/*a set of effects based on the damage source; elsewhere in hittable these actually overrule the creature-type*/
'fire', if(victim_is_big, [sound('death-fire-large.wav'), repeat_fx('die_cloud_fire',5,4,30,30, null)],
[vfx('die_cloud_fire'), sound('death-acid'+1d5+'.wav')]),
'acid', if(victim_is_big, [repeat_fx('die_cloud_acid_small',40,1,50,50,null), sound('death-acid-bubbly1.wav')],
[repeat_fx('die_cloud_acid_small',10,6,30,30,null), sound('death-acid-bubbly1.wav')]),
'energy', [repeat_fx('die_cloud_electric_medium',1d2+5,4,20,30, null),repeat_fx('electric_spark1',3+1d3,1,60,60,null), sound('death-acid'+1d5+'.wav')],
/*generic recurring effects for miscellaneous things - often not actually enemies*/
'small', repeat_fx('die_cloud_small',5,4,10,10, null),
'medium', repeat_fx('die_cloud_medium',5,4,20,30, null),
'large', repeat_fx('die_cloud',5,10,50,70, sound('splat.ogg')),
/*special custom effects for bosses*/
'mushroom-boss', repeat_fx('die_cloud_evaporative',40,10,70,120, sound('death-crunch'+1d2+'.wav')),
'moth-boss', repeat_fx('die_cloud_puffy',40,5,70,120, sound('death-crunch'+1d2+'.wav')),
)
where repeat_fx = def(string obj_type, int count, int delay, int spread_x, int spread_y, commands do_sound) -> commands spawn('particle_system_holder', me.mid_x, me.mid_y, if(1d2=2,-1,1),
execute(child, map(range(count),schedule(value*delay, [child.set_time_to_live(count*(delay+1)), vfx_spread(obj_type,spread_x,spread_y), do_sound ]))))
) where vfx = def(string obj_type) -> commands spawn(obj_type, me.mid_x, me.mid_y, if(1d2=2,-1,1)))
where vfx_spread = def(string obj_type, int spread_x, int spread_y) -> commands spawn(obj_type, me.mid_x + 1d(spread_x) - 1d(spread_x), me.mid_y + 1d(spread_y) - 1d(spread_y), if(1d2=2,-1,1))
where victim_is_big = me.physical_size >= 48
",
spawn_gibs: "def(string variation_type, int count) -> commands map([0]*count, spawn('bouncing_debris_chunk',x+1d10, y+1d10, if(1d2=2,1,-1), [add(child.variations, [variation_type]),child.init_vel('burst')]))",
splash_effect: "def() -> commands if(me.underwater and me.water_bounds,
[if(abs(me.water_bounds[1] - me.midpoint_y) > 40,
spawn('water_splash_underwater_big', me.mid_x, me.mid_y, if(1d2=2,me.facing,-me.facing)),
spawn('water_splash', me.midpoint_x, me.water_bounds[1]+10, if(1d2=2,me.facing,-me.facing))),
sound('water-enter.ogg'), ])",
#-------------------------- sfx for material-interactions --------------------------#
tile_tags: "{string -> string} :: //Twist around the data structure, so that each tile mnemonic is the key to a list of keys to the lists it's in.
fold(
map(flatten(values(tags)), 'tla',
{(tla): string <- find(keys(tags), 'key', tla in tags[key])} ),
a+b)
where tags = {
'wood': ['fbr','acn','act','ast','isb','fnt','int',],
'foliage': ['ngs',],
'dirt': ['nrk','frg',],
'stone': ['crk','dbk','cbk',],
'wood_solid': ['ins','tnk',],
'metal': ['ppl','dsb',],
}",
object_tags: "[string] :: if(material_tag is [string], material_tag, ['default']) where material_tag = filter([standing_on and standing_on['material_sound']], value)",
tags_on: "[string] :: object_tags or unique(filter(map(tiles_at(midpoint_x, y+img_h+1), tile_tags[value.id]), value))",
tagged_sfx: "def(string action) -> [{keys : [string], sound : commands}] switch(action,
'slide', [
{keys: ['wood'], sound: sound('slide-wood'+1d13+'.wav')},
{keys: ['foliage'], sound: sound('slide-foliage'+1d10+'.wav')},
{keys: ['dirt'], sound: sound('slide-dirt'+1d4+'.wav')},
{keys: ['stone'], sound: sound('slide-stone'+1d10+'.wav')},
{keys: ['wood_solid'], sound: sound('slide-wood-solid'+1d5+'.wav')},
{keys: ['metal'], sound: sound('slide-metal'+1d9+'.wav')},
{keys: ['padding'], sound: sound('footstep-slide-padding'+1d4+'.wav')},
{keys: ['plastic'], sound: sound('footstep-slide-watercooler'+1d6+'.wav')},
{keys: ['default'], sound: sound('slide-dirt'+1d4+'.wav')},
],
'jump', [
{keys: ['wood'], sound: sound('jump-wood'+1d10+'.wav',0.7)},
{keys: ['foliage'], sound: sound('jump-foliage'+1d8+'.wav')},
{keys: ['padding'], sound: sound('footstep-jump-padding'+1d7+'.wav')},
{keys: ['plastic'], sound: sound('footstep-jump-watercooler'+1d6+'.wav')},
{keys: ['dirt'], sound: sound('jump-dirt'+1d9+'.wav')},
{keys: ['stone'], sound: sound('footstep-run-stone'+1d8+'.wav')},
{keys: ['wood_solid'], sound: sound('jump-wood-solid'+1d3+'.wav',0.6)},
{keys: ['metal'], sound: sound('footstep-run-metal'+1d5+'.wav',0.6)},
{keys: ['default'], sound: sound('JumpSoft.ogg')},
],
'footfall', [
{keys: ['wood'], sound: sound('footstep-'+run+'wood'+if(running,1d10,1d7)+'.wav',if(running,0.7,1.0))},
{keys: ['foliage'], sound: sound('footstep-'+run+'foliage'+if(running,1d9,1d6)+'.wav')},
{keys: ['plastic'], sound: sound('footstep-run-watercooler'+1d5+'.wav')},
{keys: ['dirt'], sound: sound('footstep-'+run+'dirt'+if(running,1d9,1d10)+'.wav')},
{keys: ['stone'], sound: sound('footstep-'+run+'stone'+if(running,1d8,1d10)+'.wav')},
{keys: ['padding'], sound: sound('footstep-'+run+'padding'+if(running,1d7,1d7)+'.wav')},
{keys: ['wood_solid'], sound: sound('footstep-'+run+'wood-solid'+if(running,1d7,1d8)+'.wav',0.6)},
{keys: ['metal'], sound: sound('footstep-'+run+'metal'+if(running,1d5,1d5)+'.wav',0.8)},
{keys: ['default'], sound: sound('footstep'+1d4+'.wav')},
] where run = if(animation in ['run'],'run-','') where running = (animation in ['run'])
)",
choose_sfx: "def(string action) -> commands
if(snd, snd.sound) where snd = {keys: [string], sound: commands} <- find(sfx, 'effect', find(tags + ['default'], 'tag', tag in effect.keys)) where sfx = tagged_sfx(action), tags = tags_on",
impact_cloud_silent: "def(int new_x, int new_y, string size) -> commands if(size = 'small', spawn('impact_cloud_small',new_x,new_y,1), spawn('impact_cloud',new_x,new_y,1))",
impact_cloud: "def(int new_x, int new_y, string size) -> commands [impact_cloud_silent(new_x,new_y,size),play_impact_sound]",
custom_impact_sound: "string|null :: null",
play_impact_sound: "commands :: if(custom_impact_sound != null, sound_falloff(custom_impact_sound),
switch(material_sound,
'metal', sound_falloff('collide-metal-heavy'+1d7+'.wav'),
'coconut', sound_falloff('hopper-block1.wav'),
sound_falloff('bump-2.wav')))",
//this is meant to be a client function only used from individual monster classes and such.
handle_damage_from_immersion: "def(object the_water_object) -> commands //water_object is accessible within any object event, if one exists
[
if(underwater and not is_invincible_posthit,
if( the_liquid.liquid_damage,
get_hit_sourceless(the_liquid.damage_type, the_liquid.liquid_damage)
)
),
] where the_liquid = (obj liquid <- water_object)",
},
#-------------------------- collision event handling --------------------------#
on_start_level: "if(level.cycle < time_last_hit, set(time_last_hit, 0))",
on_outside_level: "[if(y > level.dimensions[3] and unless_we_shouldnt, add(hitpoints,-1))] where unless_we_shouldnt = if(level.player is obj player_controlled_platformer_character, not (level.player.exempt_from_dying_whilst_falling_rules_for_a_cutscene or level.player.exempt_from_dying_whilst_falling_due_to_level_portals), true)",
on_being_removed: "map(filter(spawned_children, value is obj shadow), remove_object(value))",
on_collide_object_body: "handle_body_collision(arg.collide_with, arg.collide_with_area, arg.all_collisions, arg.collision_index)",
on_enter_water: "enter_water_only_once_per_frame",
#-------------------------- error condition handling --------------------------#
on_change_solid_dimensions_fail: "fire_event('solidity_fail')",
on_change_animation_failure: "fire_event('solidity_fail')",
# if the level starts, and we're embedded in solid stuff, try moving upwards to get out of it.
# this should catch any errors introduced by changes to solid area or handling thereof
on_solidity_fail: "if(_in_solidity_fail, die(),
[set(_in_solidity_fail, true),
resolve_solid(me),
set(_in_solidity_fail, false)
])",
on_add_object_fail: "if(collide_with is obj hittable, [if(collide_with.team != team and collide_with.get_hit_by, collide_with.get_hit_by(me)), die()], die())
where collide_with = arg.collide_with",
}