-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcomponents.py
1499 lines (1312 loc) · 60.1 KB
/
components.py
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
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
from collections import OrderedDict
from json import dumps, loads
from math import sqrt
from random import randint, choice
from bear_hug.bear_utilities import BearECSException, rectangles_collide, \
BearException
from bear_hug.ecs import Component, PositionComponent, BearEvent, \
Entity, EntityTracker, CollisionComponent, \
DestructorComponent, AnimationWidgetComponent
class SpeakerWidgetComponent(AnimationWidgetComponent):
"""
A specialized WidgetComponent for the boombox in settings window.
Triggers its animation upon receiving 'brut_change_config' event with
('sound', False) and ('sound', True) values
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.dispatcher.register_listener(self, 'brut_change_config')
def on_event(self, event):
if event.event_type == 'brut_change_config' and event.event_value[0] == 'sound':
print(event.event_value)
if event.event_value[1]:
self.widget.start()
else:
self.widget.stop()
else:
return super().on_event(event)
class WalkerComponent(PositionComponent):
"""
A simple PositionComponent that switches widgets appropriately
"""
def __init__(self, *args, direction='r', initial_phase='1',
jump_duration=0.4, jump_timer=0,
jump_direction=0,
jump_vx=60, jump_vy=-40, **kwargs):
super().__init__(*args, **kwargs)
self.dispatcher.register_listener(self, ['tick', 'ecs_collision'])
self.direction = direction
self.phase = initial_phase
self.moved_this_tick = False
self.jump_vx = jump_vx
self.jump_vy = jump_vy
self.jump_direction = jump_direction
self.jump_timer = jump_timer
self.jump_duration = jump_duration
def walk(self, move):
"""
Move the owner, switching widgets for step animation and setting
direction
If this is undesirable (ie character was pushed or something), use
regular ``position_component.move()``, which is not overridden.
:param move: tuple of ints - relative move
:return:
"""
if self.moved_this_tick:
return
self.relative_move(*move)
self.moved_this_tick = True
if move[0] > 0:
self.direction = 'r'
elif move[0] < 0:
self.direction = 'l'
# If move[0] == 0, the direction stays whatever it was, move is vertical
# TODO: Supp more than two phases of movement
if self.phase == '1':
self.phase = '2'
else:
self.phase = '1'
self.dispatcher.add_event(BearEvent('play_sound', 'step'))
self.owner.widget.switch_to_image(f'{self.direction}_{self.phase}')
def jump(self):
"""
Jump in currently set direction
:return:
"""
# TODO: Call correct jump image
# Jump direction is set to 1 while raising and to -1 while falling
# Zero when not in jump state
if self.jump_direction:
# No double jumps
return
self.affect_z = False
self.jump_direction = 1
self.jump_timer = 0
self.vx = self.jump_vx if self.direction == 'r' else -1 * self.jump_vx
self.vy = self.jump_vy
def turn(self, direction):
"""
Set direction and set correct widget
:param direction:
:return:
"""
if direction not in ('l', 'r'):
raise ValueError('WalkerComponent can only turn l or r')
self.owner.position.direction = direction
self.owner.widget.switch_to_image(f'{self.direction}_{self.phase}')
def on_event(self, event):
if event.event_type == 'tick':
self.moved_this_tick = False
if self.jump_direction:
self.jump_timer += event.event_value
if self.jump_timer >= self.jump_duration:
# Ending jump
self.vx = 0
self.vy = 0
self.jump_direction = 0
self.affect_z = True
elif self.jump_direction == 1 and \
self.jump_timer >= self.jump_duration/2:
self.jump_direction = -1
self.vy = -1 * self.vy
elif event.event_type == 'ecs_collision' \
and event.event_value[0] == self.owner.id \
and self.jump_direction:
should_fall = False
# TODO: Fix bugs in jump
if event.event_value[1]:
# This exception covers some weird bug when after changing level
# mid-jump it attempts to process ecs_collision event with an
# already destroyed highlight entity, and crashes with KeyError
try:
other = EntityTracker().entities[event.event_value[1]]
if other.collision.passable:
should_fall = False
else:
should_fall = True
except KeyError:
should_fall = True
if should_fall:
self.vx = 0
if self.jump_direction == 1:
# Currently raising, need to drop
self.jump_direction = -1
self.jump_timer = self.jump_duration - self.jump_timer
self.vy = -1 * self.vy
return super().on_event(event)
def __repr__(self):
d = loads(super().__repr__())
d.update({'direction': self.direction,
'initial_phase': self.phase,
'jump_vx': self.jump_vx,
'jump_vy': self.jump_vy,
'jump_direction': self.jump_direction,
'jump_timer': self.jump_timer,
'jump_duration': self.jump_duration})
return dumps(d)
class AttachedPositionComponent(PositionComponent):
"""
A PositionComponent that maintains its position relative to other entity.
This component can have its own vx and vy, but it also listens to the
other's ``ecs_move`` events and repeats them.
:param tracked_entity: entity ID
"""
def __init__(self, *args, tracked_entity=None, **kwargs):
super().__init__(*args, **kwargs)
# Could be None, in which case it acts like a regular PositionComponent
self.tracked_entity = tracked_entity
self.dispatcher.register_listener(self, 'ecs_move')
def on_event(self, event):
if event.event_type == 'ecs_move' and self.tracked_entity and \
event.event_value[0] == self.tracked_entity:
if not hasattr(self.owner, 'hiding')\
or self.owner.hiding.is_working:
rel_move = EntityTracker().entities[self.tracked_entity].\
position.last_move
self.relative_move(*rel_move)
return super().on_event(event)
def __repr__(self):
d = loads(super().__repr__())
d['tracked_entity'] = self.tracked_entity
return dumps(d)
class GravityPositionComponent(PositionComponent):
"""
A PositionComponent that maintains a constant downward acceleration.
:param acceleration: A number. Acceleration in chars per second squared.
Defaults to 10.0
"""
def __init__(self, *args, acceleration=10.0, have_waited=0, **kwargs):
super().__init__(*args, **kwargs)
self.acceleration = acceleration
self.update_freq = 1/acceleration
self.have_waited = have_waited
self.dispatcher.register_listener(self, 'tick')
def on_event(self, event):
if event.event_type == 'tick':
self.have_waited += event.event_value
if self.have_waited >= self.update_freq:
self.vy += round(self.have_waited/self.update_freq)
self.have_waited = 0
return super().on_event(event)
def __repr__(self):
d = loads(super().__repr__())
{}.update({'acceleration': self.acceleration,
'have_waited': self.have_waited})
return dumps(d)
class ProjectileCollisionComponent(CollisionComponent):
"""
A collision component that damages whatever its owner is collided into
"""
def __init__(self, *args, damage=1, **kwargs):
super().__init__(*args, **kwargs)
self.damage = damage
def collided_into(self, entity):
if not entity:
self.owner.destructor.destroy()
elif hasattr(EntityTracker().entities[entity], 'collision'):
self.dispatcher.add_event(BearEvent('play_sound', 'punch'))
self.dispatcher.add_event(BearEvent(event_type='brut_damage',
event_value=(entity,
self.damage)))
self.owner.destructor.destroy()
def __repr__(self):
d = loads(super().__repr__())
d['damage'] = self.damage
return dumps(d)
class PowerProjectileCollisionComponent(ProjectileCollisionComponent):
"""
A collision projectile for a power spark.
Acts like a regular ProjectileCollisionComponent unless its target has
PowerInteractionComponent, in which case it is powered
"""
def collided_into(self, other):
if not other:
self.owner.destructor.destroy()
else:
try:
entity = EntityTracker().entities[other]
except KeyError:
# Another case of collision into nonexistent item
return
if hasattr(entity, 'collision') and not entity.collision.passable:
if hasattr(entity, 'powered'):
entity.powered.get_power()
else:
self.dispatcher.add_event(BearEvent('brut_damage',
(other, self.damage)))
self.owner.destructor.destroy()
class HealingProjectileCollisionComponent(CollisionComponent):
"""
Like a bullet, except that it heals whoever it hits instead of damaging them
"""
def __init__(self, *args, healing=2, **kwargs):
super().__init__(*args, **kwargs)
self.healing=healing
def collided_into(self, other):
if not other:
self.owner.destructor.destroy()
else:
entity = EntityTracker().entities[other]
if not entity.collision.passable:
if hasattr(entity, 'health'):
self.dispatcher.add_event(BearEvent('brut_heal',
(other, self.healing)))
self.owner.destructor.destroy()
def collided_by(self, other):
# Unlike most other projectiles, can also be collided into.
# This is guaranteed to destroy the bubble
self.dispatcher.add_event(BearEvent('brut_heal',
(other, self.healing)))
self.owner.destructor.destroy()
class HazardCollisionComponent(CollisionComponent):
"""
A collision component that damages whoever collides into it.
Intended for use with flames, traps and such.
"""
def __init__(self, *args, damage=1, damage_cooldown=0.3,
on_cooldown=False, have_waited=0, **kwargs):
super().__init__(*args, **kwargs)
self.damage = damage
self.damage_cooldown = damage_cooldown
self.on_cooldown = on_cooldown
self.have_waited = have_waited
self.dispatcher.register_listener(self, 'tick')
def collided_by(self, entity):
# TODO: do damage to entities who stand in fire and don't move
if not self.on_cooldown:
try:
# Covers collisions from nonexistent entities
other = EntityTracker().entities[entity]
except KeyError:
return
if hasattr(other, 'health'):
self.dispatcher.add_event(BearEvent(event_type='brut_damage',
event_value=(entity,
self.damage)))
self.on_cooldown = True
def on_event(self, event):
if event.event_type == 'tick' and self.on_cooldown:
self.have_waited += event.event_value
if self.have_waited >= self.damage_cooldown:
self.on_cooldown = False
self.have_waited = 0
return super().on_event(event)
def __repr__(self):
d = loads(super().__repr__())
d.update({'damage': self.damage,
'damage_cooldown': self.damage_cooldown,
'have_waited': self.have_waited,
'on_cooldown': self.on_cooldown})
return dumps(d)
class GrenadeCollisionComponent(CollisionComponent):
"""
Destroys the entity if collided into None (ie screen edge)
Meant for the grenades and such, which cause crashes by attempting to fly
beyond screen edges.
Expects owner to have DestructorComponent
"""
def collided_into(self, entity):
if entity is None:
# If vy is negative (ie on the rise), it's possible that the grenade
# collided into screen top. In that case, just bounce off it
if self.owner.position.vy < 0:
self.owner.position.vy = 0
self.owner.position.have_waited = self.owner.position.update_freq
# If not, it has definitely collided into left or right edge
elif self.owner.position.vy > 5:
self.owner.destructor.destroy()
class AmmoPickupCollisionComponent(CollisionComponent):
"""
A collision component for ammo pickup.
If collided into by an entity with hands, check whether this entity holds
an item that requires ammo. If it does, and if that item is not on full
ammo, recharges the item, emits the appropriate event and self-destructs.
"""
def collided_by(self, other):
# No sense instantiating entity tracker three times for as many entities
tracker = EntityTracker()
entity = tracker.entities[other]
if hasattr(entity, 'hands'):
left_item = tracker.entities[entity.hands.left_item]
right_item = tracker.entities[entity.hands.right_item]
used = False
if left_item.item_behaviour.max_ammo and left_item.item_behaviour.ammo < left_item.item_behaviour.max_ammo:
left_item.item_behaviour.ammo = left_item.item_behaviour.max_ammo
used = True
if right_item.item_behaviour.max_ammo and right_item.item_behaviour.ammo < right_item.item_behaviour.max_ammo:
right_item.item_behaviour.ammo = right_item.item_behaviour.max_ammo
used = True
if used:
self.dispatcher.add_event(BearEvent('play_sound', 'reload'))
self.owner.destructor.destroy()
class ScorePickupCollisionComponent(CollisionComponent):
"""
A collision component for score pickup
"""
def __init__(self, *args, score=10, player_entity='cop_1', **kwargs):
super().__init__(*args, **kwargs)
if not isinstance(score, int):
raise TypeError(f'Score kwarg for ScorePickupCollisionComponent is {type(score)} instead of int')
self.score = score
self.player_entity = player_entity
def collided_by(self, other):
# Only work when collided into by the cop
if other == self.player_entity:
self.dispatcher.add_event(BearEvent('brut_score', self.score))
self.dispatcher.add_event(BearEvent('play_sound', 'coin'))
self.owner.destructor.destroy()
class GrenadeComponent(Component):
"""
The grenade behaviour.
When entity with this component reaches a certain y, it self-destructs and
creates a predetermined entity with its Spawner. This is supposed to be used
with things like grenades and Molotov cocktails that fly in the arc and
explode upon hitting the ground.
"""
def __init__(self, *args, spawned_item='flame', target_y=None,
explosion_sound=None, **kwargs):
super().__init__(*args, **kwargs)
self.spawned_item = spawned_item
self.target_y = target_y
self.explosion_sound = explosion_sound
self.dispatcher.register_listener(self, 'ecs_move')
def on_event(self, event):
if event.event_type == 'ecs_move' and event.event_value[0] == self.owner.id:
if not self.target_y:
self.target_y = self.owner.position.y + randint(2, 12)
if self.owner.position.y >= self.target_y:
if self.explosion_sound:
self.dispatcher.add_event(BearEvent('play_sound',
self.explosion_sound))
# TODO: remove flame sound if I add other grenades
self.dispatcher.add_event(BearEvent('play_sound',
'molotov_fire'))
self.owner.spawner.spawn(self.spawned_item,
(round(self.owner.widget.width/2),
round(self.owner.widget.height/2)))
self.owner.destructor.destroy()
def __repr__(self):
d = loads(super().__repr__())
d.update({'spawned_item': self.spawned_item,
'target_y': self.target_y})
return dumps(d)
class SoundDestructorComponent(DestructorComponent):
def __init__(self, *args, bg_sound='supercop_bg', **kwargs):
super().__init__(*args, **kwargs)
self.bg_sound = bg_sound
def destroy(self):
self.dispatcher.add_event(BearEvent('set_bg_sound', self.bg_sound))
super().destroy()
class HealthComponent(Component):
"""
A component that monitors owner's health and processes its changes.
"""
def __init__(self, *args, hitpoints=3, **kwargs):
super().__init__(*args, name='health', **kwargs)
self.dispatcher.register_listener(self, ('brut_damage', 'brut_heal'))
self.max_hitpoints = hitpoints
self._hitpoints = hitpoints
def on_event(self, event):
if event.event_type == 'brut_damage' and \
event.event_value[0] == self.owner.id:
self.hitpoints -= event.event_value[1]
elif event.event_type == 'brut_heal' and \
event.event_value[0] == self.owner.id:
self.hitpoints += event.event_value[1]
@property
def hitpoints(self):
return self._hitpoints
@hitpoints.setter
def hitpoints(self, value):
if not isinstance(value, int):
raise BearECSException(f'Attempting to set hitpoints of {self.owner.id} to non-integer {value}')
self._hitpoints = value
if self._hitpoints < 0:
self._hitpoints = 0
if self._hitpoints > self.max_hitpoints:
self._hitpoints = self.max_hitpoints
self.process_hitpoint_update()
def process_hitpoint_update(self):
"""
:return:
"""
raise NotImplementedError('HP update processing should be overridden')
def __repr__(self):
return dumps({'class': self.__class__.__name__,
'hitpoints': self.hitpoints})
class SwitchHealthComponent(HealthComponent):
"""
A health component for various switches, triggers and so on.
When damaged (ie collided into by a combat projectile), switches its state
and orders the widget to change. Does not work as an actual damageable
item.
This logic is not implemented in CollisionComponent because the item needs
that one for correct interaction with walkers and such. OTOH, I don't see
any need for destructible switches, so they aren't likely to need a
regular HealthComponent.
Expects owner's widget component to be SwitchWidgetComponent
"""
def __init__(self, *args,
initial_state=False,
on_event_type='brut_change_config',
on_event_value=(None, None),
on_sound='switch_on',
on_widget='wall_switch_on',
off_event_type='brut_change_config',
off_event_value=(None, None),
off_sound='switch_off',
off_widget='wall_switch_off',
**kwargs):
super().__init__(*args, **kwargs)
if not isinstance(initial_state, bool):
raise BearException(f'{type(initial_state)} used as initial_state for SwitchHealthComponent instead of bool')
self.current_state = initial_state
self.on_event_type = on_event_type
self.on_event_value = on_event_value
self.on_sound = on_sound
self.on_widget = on_widget
self.off_event_type = off_event_type
self.off_event_value = off_event_value
self.off_sound = off_sound
self.off_widget = off_widget
self.dispatcher.register_listener(self, 'brut_change_config')
def on_event(self, event):
if event.event_type == 'brut_damage' and \
event.event_value[0] == self.owner.id:
return self.trigger()
elif event.event_type == 'brut_change_config' and \
event.event_value[0] == self.on_event_value[0]:
# Assumes the event_value is always (str, bool). It not necessarily
# is, but that's the kind of option switches are for.
if event.event_value[1]:
self.switch_on()
else:
self.switch_off()
def trigger(self):
if self.current_state:
return self.switch_off()
else:
return self.switch_on()
def switch_on(self):
self.current_state = True
self.owner.widget.switch_to_image(self.on_widget)
return [BearEvent(self.on_event_type, self.on_event_value),
BearEvent('play_sound', self.on_sound)]
def switch_off(self):
self.current_state = False
self.owner.widget.switch_to_image(self.off_widget)
return [BearEvent(self.off_event_type, self.off_event_value),
BearEvent('play_sound', self.off_sound)]
def __repr__(self):
d = loads(super().__repr__())
d['initial_state'] = self.current_state
d['on_event_type'] = self.on_event_type
d['on_event_value'] = self.on_event_value
d['on_sound'] = self.on_sound
d['on_widget'] = self.on_widget
d['off_event_type'] = self.off_event_type
d['off_event_value'] = self.off_event_value
d['off_sound'] = self.off_sound
d['off_widget'] = self.off_widget
return dumps(d)
class DestructorHealthComponent(HealthComponent):
"""
Destroys entity upon reaching zero HP
"""
def process_hitpoint_update(self):
if self.hitpoints == 0 and hasattr(self.owner, 'destructor'):
self.owner.destructor.destroy()
class SpawnerDestructorHealthComponent(HealthComponent):
"""
Destroys entity upon reaching zero HP and spawns an item
"""
def __init__(self, *args, spawned_item='pistol', relative_pos=(0, 0),
**kwargs):
super().__init__(*args, **kwargs)
self.spawned_item = spawned_item
self.relative_pos = relative_pos
def process_hitpoint_update(self):
if self.hitpoints == 0:
self.owner.spawner.spawn(self.spawned_item, self.relative_pos)
self.owner.destructor.destroy()
class CharacterHealthComponent(HealthComponent):
"""
Health component for characters (both playable and NPCs). Upon death,
creates a corpse and drops whatever the character had in his hands.
Expects owner to have SpawnerComponent, HandInterfaceComponent and
DestructorComponent
"""
def __init__(self, *args, corpse=None, heal_sounds=('bandage', ),
hit_sounds=None, death_sounds=None,
score=None, **kwargs):
super().__init__(*args, **kwargs)
self.corpse_type = corpse
self.hit_sounds = hit_sounds
self.heal_sounds = heal_sounds
self.death_sounds = death_sounds
self.last_hp = self.hitpoints
self.score = score
def process_hitpoint_update(self):
if 0 < self.hitpoints < self.last_hp and self.hit_sounds:
self.last_hp = self.hitpoints
self.dispatcher.add_event(BearEvent('play_sound',
choice(self.hit_sounds)))
elif 0 < self.last_hp < self.hitpoints and self.heal_sounds:
self.last_hp = self.hitpoints
self.dispatcher.add_event(BearEvent('play_sound',
choice(self.heal_sounds)))
if self.hitpoints == 0:
self.owner.spawner.spawn(self.corpse_type,
relative_pos=(0, self.owner.widget.height - 9))
# Drop non-fist items, if any
self.owner.hands.drop('right', restore_fist=True)
self.owner.hands.drop('left', restore_fist=True)
EntityTracker().entities[self.owner.hands.left_item].destructor.destroy()
EntityTracker().entities[self.owner.hands.right_item].destructor.destroy()
# Dump score ball, if any
if self.score:
self.dispatcher.add_event(BearEvent('play_sound', 'coin_drop'))
self.owner.spawner.spawn('score_pickup', (randint(-3, 6),
self.owner.widget.height-2),
score=self.score)
if self.death_sounds:
if self.owner.id == 'cop_1':
self.dispatcher.add_event(BearEvent('set_bg_sound', None))
self.dispatcher.add_event(BearEvent('play_sound',
choice(self.death_sounds)))
self.owner.destructor.destroy()
def __repr__(self):
d = loads(super().__repr__())
d['corpse'] = self.corpse_type
return dumps(d)
class VisualDamageHealthComponent(HealthComponent):
"""
A health component for non-active objects.
Tells the owner's widget to switch image upon reaching certain amounts of HP
This should be in `widgets_dict` parameter to __init__ which is a dict from
int HP to image ID. A corresponding image is shown while HP is not less than
a dict key, but less than the next one (in increasing order).
If HP reaches zero and object has a Destructor component, it is destroyed
"""
def __init__(self, *args, widgets_dict={}, hit_sounds=(), **kwargs):
super().__init__(*args, **kwargs)
self.widgets_dict = OrderedDict()
self.hit_sounds = hit_sounds
for x in sorted(widgets_dict.keys()):
# Int conversion useful when loading from JSON, where dict keys get
# converted to str due to some weird bug. Does nothing during
# normal Component creation
self.widgets_dict[int(x)] = widgets_dict[x]
def process_hitpoint_update(self):
if self.hit_sounds:
self.dispatcher.add_event(BearEvent('play_sound',
choice(self.hit_sounds)))
if self.hitpoints == 0 and hasattr(self.owner, 'destructor'):
self.owner.destructor.destroy()
for x in self.widgets_dict:
if self.hitpoints >= x:
self.owner.widget.switch_to_image(self.widgets_dict[x])
def __repr__(self):
d = loads(super().__repr__())
d['widgets_dict'] = self.widgets_dict
return dumps(d)
class PowerInteractionComponent(Component):
"""
Responsible for the interaction with energy projectiles.
The idea is that while these projectiles are harmful for the characters
(which turns eg pairs of spikes into reliable defensive tools), other
objects can react to them differently: spikes are ignoring them, healing
ray emitters are powered by them, etc. This is the component responsible
for those behaviours
"""
def __init__(self, *args, powered=False, action_cooldown=0.1, **kwargs):
super().__init__(*args, name='powered', **kwargs)
self.powered = powered
self.action_cooldown = action_cooldown
self.have_waited = 0
self.dispatcher.register_listener(self, 'tick')
def get_power(self):
if not self.powered:
# Should charge after being powered even if it had collected some
# charge before
self.have_waited = 0
self.powered = True
def take_action(self, *args, **kwargs):
raise NotImplementedError('Power interaction behaviours should be overridden')
def on_event(self, event):
if self.powered and event.event_type == 'tick':
self.have_waited += event.event_value
while self.have_waited >= self.action_cooldown:
self.take_action()
self.have_waited -= self.action_cooldown
def __repr__(self):
d = loads(super().__repr__())
d.update({'powered': self.powered,
'action_cooldown': self.action_cooldown})
return dumps(d)
class SciencePropPowerInteractionComponent(PowerInteractionComponent):
"""
A prop that does nothing useful. It just switches its widget between powered
and unpowered state.
Expects the owner's WidgetComponent to be a SwitchWidgetComponent
"""
def get_power(self):
super().get_power()
self.owner.widget.switch_to_image('powered')
def take_action(self):
self.powered = False
self.owner.widget.switch_to_image('unpowered')
self.dispatcher.add_event(BearEvent('play_sound', 'blue_machine'))
class SpikePowerInteractionComponent(PowerInteractionComponent):
"""
Power interaction component for spikes.
They spam sparks towards any entities with PowerInteractionComponent
within range
"""
def __init__(self, *args, range=40, **kwargs):
super().__init__(*args, **kwargs)
self.range = range
self.targets = {}
self.target_names = []
self.dispatcher.register_listener(self, ('ecs_add', 'ecs_destroy'))
def get_power(self):
super().get_power()
self.owner.widget.switch_to_image('powered')
def on_event(self, event):
r = super().on_event(event)
if event.event_type == 'ecs_add':
entity = EntityTracker().entities[event.event_value[0]]
if not hasattr(entity, 'powered') or entity.id in self.target_names:
return
if event.event_value[0] == self.owner.id:
# On deployment, look for nearby machines
powered = EntityTracker().filter_entities(lambda x: hasattr(x, 'powered'))
for machine in powered:
dx = self.owner.position.x - machine.position.x
dy = self.owner.position.y - machine.position.y
dist = sqrt(dx ** 2 + dy ** 2)
if dist <= self.range and machine.id != self.owner.id and \
machine.id not in self.target_names:
self.targets[machine.id] = (machine.position.pos[0],
machine.position.pos[1])
self.target_names.append(machine.id)
else:
dx = self.owner.position.x - entity.position.x
dy = self.owner.position.y - entity.position.y
dist = sqrt(dx ** 2 + dy ** 2)
if dist <= self.range and entity.id not in self.target_names:
self.targets[entity.id] = (entity.position.pos[0] + entity.widget.width // 2,
entity.position.pos[1] + entity.widget.height - 12)
self.target_names.append(entity.id)
elif event.event_type == 'ecs_destroy':
if event.event_value in self.targets:
self.target_names.remove(event.event_value)
del self.targets[event.event_value]
def take_action(self, *args, **kwargs):
if self.targets:
target = self.targets[choice(self.target_names)]
dx = target[0] - self.owner.position.x - 2
dy = target[1] - self.owner.position.y - 7
dx_sign = abs(dx) // dx if dx != 0 else 0
dy_sign = abs(dy) // dy if dy != 0 else 0
# Trivially proven from:
# 1) V**2 = vx**2 + vy ** 2
# 2) vx/vy = dx/dy
vy = sqrt(1600 / (1 + dx**2/dy**2)) if dy != 0 else 0
vx = sqrt(1600 / (1 + dy**2/dx**2)) if dx != 0 else 0
self.owner.spawner.spawn('tall_spark', (2 + 2*dx_sign,
7 + 2*dy_sign),
vx=vx * dx_sign,
vy=vy * dy_sign,
**kwargs)
self.dispatcher.add_event(BearEvent('play_sound',
'spark'))
def __repr__(self):
d = loads(super().__repr__())
d['range'] = self.range
return dumps(d)
class HealerPowerInteractionComponent(PowerInteractionComponent):
"""
Shoots healing projectiles in random directions.
Expects owner to have a SpawnerComponent.
Expects its widget to be a SwitchWidget with 'powered' and 'unpowered'
states.
"""
def get_power(self):
super().get_power()
self.owner.widget.switch_to_image('powered')
def take_action(self, *args, **kwargs):
vx = randint(-10, 10)
vy = randint(-10, 10)
self.dispatcher.add_event(BearEvent('play_sound', 'balloon'))
self.owner.spawner.spawn('healing_projectile', (5 * vx // abs(vx) if vx != 0 else choice((5, -5)),
5 * vy // abs(vy) if vy != 0 else choice((5, -5))),
vx=vx, vy=vy)
self.owner.widget.switch_to_image('unpowered')
self.powered = False
class SpawnerComponent(Component):
"""
A component responsible for spawning stuff near its owner
For projectiles and other such things
"""
def __init__(self, *args, factory=None, **kwargs):
super().__init__(*args, name='spawner', **kwargs)
self.factory = factory
self.dispatcher.register_listener(self, 'key_down')
def spawn(self, item, relative_pos, **kwargs):
"""
Spawn item at self.pos+self.relative_pos
:param item:
:param relative_pos:
:return:
"""
self.factory.create_entity(item, (self.owner.position.x + relative_pos[0],
self.owner.position.y + relative_pos[1]),
**kwargs)
# No __repr__ because its only kwarg is the factory instance that cannot
# be stored between runs.
class FactionComponent(Component):
"""
Stores the faction data to see who should attack whom.
:param faction: str. The faction name. Defaults to 'placeholder'
:param phrase_color: str. The color that should be used in speech
"""
colors = {'police': '#aaaaaa',
'scientists': '#aaaaaa',
'punks': '#ffffff',
'placeholder': '#ffffff'}
def __init__(self, *args, faction='placeholder',
phrase_color='white',
**kwargs):
super().__init__(*args, name='faction', **kwargs)
self.faction = faction
self.phrase_color = self.colors[faction]
def __repr__(self):
return dumps({'class': self.__class__.__name__,
'faction': self.faction})
class LevelSwitchComponent(Component):
"""
Stores the level ID for the level switch widgets
"""
# TODO: let LevelSwitchComponent track whether the level was won or lost
def __init__(self, *args, next_level='ghetto_test', **kwargs):
super().__init__(*args, name='level_switch', **kwargs)
self.next_level = next_level
def __repr__(self):
return dumps({'class': self.__class__.__name__,
'next_level': self.next_level})
class InputComponent(Component):
"""
A component that handles input.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, name='controller', **kwargs)
self.dispatcher.register_listener(self, ['key_down', 'tick'])
self.walk_delay = 0.1
self.current_walk_delay = 0
self.action_delay = 0.4
self.current_action_delay = 0
self.next_move = [0, 0]
self.accepts_input = True
def on_event(self, event):
#TODO: Support non-hardcoded actions and keys
x = super().on_event(event)
if isinstance(x, BearEvent):
r = [x]
elif isinstance(x, list):
r = x
else:
r = []
if event.event_type == 'tick':
if self.current_walk_delay > 0:
self.current_walk_delay -= event.event_value
else:
# Movement is processed from the commands collected during
# the previous tick. Of course, the input is ignored while
# entity is on the cooldown
if self.next_move[0] != 0 or self.next_move[1] != 0:
self.owner.position.walk(self.next_move)
self.next_move = [0, 0]
self.current_walk_delay = self.walk_delay
if self.current_action_delay > 0:
self.current_action_delay -= event.event_value
if event.event_type == 'key_down' and self.accepts_input:
if self.owner.health.hitpoints > 0 and \
self.current_action_delay <= 0:
# These actions are only available to a non-dead player char
if event.event_value == 'TK_Q':
# left-handed attack
self.current_action_delay = self.owner.hands.use_hand('left')
elif event.event_value == 'TK_E':
# Right-handed attack
self.current_action_delay = self.owner.hands.use_hand('right')
elif event.event_value == 'TK_Z':
# Left-handed pickup
self.owner.hands.pick_up(hand='left')
self.current_action_delay = self.action_delay
elif event.event_value == 'TK_C':
# Right-handed pickup
self.owner.hands.pick_up(hand='right')
self.current_action_delay = self.action_delay
elif event.event_value == 'TK_SPACE':
self.owner.position.jump()
self.current_action_delay = self.action_delay
# These actions are available whether or not the player is dead
if event.event_value in ('TK_D', 'TK_RIGHT') and self.current_walk_delay <= 0:
self.next_move[0] += 2
elif event.event_value in ('TK_A', 'TK_LEFT') and self.current_walk_delay <= 0:
self.next_move[0] -= 2
elif event.event_value in ('TK_S', 'TK_DOWN') and self.current_walk_delay <= 0:
self.next_move[1] += 2
elif event.event_value in ('TK_W', 'TK_UP') and self.current_walk_delay <= 0:
self.next_move[1] -= 2
elif event.event_value == 'TK_KP_6':
r.append(BearEvent(event_type='ecs_scroll_by',
event_value=(1, 0)))
elif event.event_value == 'TK_KP_4':
r.append(BearEvent(event_type='ecs_scroll_by',
event_value=(-1, 0)))
elif event.event_value == 'TK_KP_8':
r.append(BearEvent(event_type='ecs_scroll_by',
event_value=(0, -1)))