Transform Boyd based on resistances
[tabakrolletjie.git] / tabakrolletjie / enemies.py
1 # Boyd, the friendly, misunderstood turnip loving, light hating space mould
2
3 import random
4
5 import pymunk
6 import pymunk.pygame_util
7 import pygame.draw
8 import pygame.surface
9 import pygame.display
10
11 from .constants import (SCREEN_SIZE, MOULD_CATEGORY, OBSTACLE_CATEGORY,
12                         TURNIP_CATEGORY, COLOURS)
13 from .loader import loader
14 from .sound import sound
15 from .transforms import Multiply
16
17 MOULD_FILTER = pymunk.ShapeFilter(
18     mask=MOULD_CATEGORY | OBSTACLE_CATEGORY,
19     categories=MOULD_CATEGORY)
20
21 EAT_TURNIP_FILTER = pymunk.ShapeFilter(mask=TURNIP_CATEGORY)
22
23
24 # Boyd parameters
25 SPAWN_RATE = 10
26 MAX_AGE = 60
27 MAX_ELEMENTS = 400
28 MAX_HEALTH = 100
29
30 # Increase in health per day
31 DAY_HEALTH = 10
32
33 HEAL_FACTOR = 1
34
35 MOULD_STAGES = [15, 25]
36
37
38 def calc_colour_transform(resistances):
39     fr = fg = fb = 0
40     for colour, value in resistances.items():
41         if value:
42             new_value = 63 + 64 * value
43             r, g, b = COLOURS[colour]
44             if r:
45                 fr += new_value
46             if g:
47                 fg += new_value
48             if b:
49                 fb += new_value
50     # Scale if we've exceeded 255
51     # Should only be required when we have lights that aren't soley red,
52     # green or blue
53     max_value = max(fr, fg, fb)
54     if max_value > 255:
55         fr = int(255 * fr / float(max_value))
56         fg = int(255 * fg / float(max_value))
57         fb = int(255 * fb / float(max_value))
58     return Multiply(colour=(fr, fg, fb))
59
60
61 class Mould(pymunk.Body):
62     """A segment of Boyd"""
63
64     def __init__(self, gamestate, space, pos, resistances, transform):
65         super(Mould, self).__init__(0, 0, pymunk.Body.STATIC)
66         self.position = pos
67         self._shape = pymunk.Circle(self, 16)
68         space.add(self, self._shape)
69         self._shape.filter = MOULD_FILTER
70         self._age = 0
71         self._img = None
72         self._health = 500
73         self.has_eyeball = False
74         self._eyeball = None
75         self._resistances = resistances
76         self._transform = transform
77
78     def pygame_pos(self, surface):
79         """Convert to pygame coordinates and offset position so
80            our position is the centre of the image."""
81         # The odd sign combination is because of the pymunk / pygame
82         # transform, but we do it this way to exploit Vec2d math magic
83         return pymunk.pygame_util.to_pygame(self.position + (-16, 16), surface)
84
85     def get_image(self):
86         if not self._img:
87             name = random.choice(
88                 ('mouldA.png', 'mouldB.png', 'mouldC.png'))
89             size = "16" if self._age < MOULD_STAGES[0] else "32" if self._age < MOULD_STAGES[1] else "64"
90             self._img = loader.load_image(size, name,
91                                           transform=self._transform)
92         return self._img
93
94     def get_eyeball(self):
95         if not self._eyeball:
96             name = random.choice(
97                 ('eyeballA.png', 'eyeballB.png', 'eyeballC.png'))
98             self._eyeball = loader.load_image("32", name)
99         return self._eyeball
100
101     def set_health(self, new_health):
102         self._health = new_health
103
104     def tick(self, gamestate, space, moulds):
105         """Grow and / or Die"""
106
107         self._age += 1
108
109         # we regain a health every tick, so we heal in the dark
110         if self._health < MAX_HEALTH:
111             self._health += HEAL_FACTOR
112
113         refresh = False
114
115         if (self._age % SPAWN_RATE) == 0 and len(moulds) < MAX_ELEMENTS:
116             # Spawn a new child, if we can
117             spawn = True
118             choice = random.randint(0, 3)
119             if choice == 0:
120                 pos = self.position + (0, 24)
121             elif choice == 1:
122                 pos = self.position + (24, 0)
123             elif choice == 2:
124                 pos = self.position + (-24, 0)
125             else:
126                 pos = self.position + (0, -24)
127             # check for bounds
128             if pos[0] < 0 or pos[0] >= SCREEN_SIZE[0]:
129                 spawn = False
130             if pos[1] < 0 or pos[1] >= SCREEN_SIZE[1]:
131                 spawn = False
132             # Check for free space
133             # We allow some overlap, hence not checking full radius
134             query = space.point_query(pos, 8, MOULD_FILTER)
135             if query:
136                 spawn = False
137             if spawn:
138                 child = Mould(gamestate, space, pos, self._resistances,
139                               self._transform)
140                 child._health = self._health
141                 moulds.append(child)
142                 refresh = True
143                 if random.randint(0, 10) < 2:
144                     sound.play_sound("mouth_pop_2a.ogg")
145
146         if self._age in MOULD_STAGES:
147             # We grow in size
148             refresh = True
149             self._img = None  # invalidate cached image
150
151         if self._age > MOULD_STAGES[1] and random.randint(0, 500) < 1:
152             # Maybe we grow an eyeball
153             self.has_eyeball = True
154
155         if self._age > MAX_AGE:
156             # We die of old age
157             space.remove(self, self._shape)
158             moulds.remove(self)
159             refresh = True
160         else:
161             # Check for turnips we can eat
162             # Note that we can only eat a tick after we spawn
163             query = space.point_query(self.position, 16, EAT_TURNIP_FILTER)
164             if query:
165                 query[0].shape.body.turnip.eaten = True
166         return refresh
167
168     def damage(self, light, space, moulds):
169         """Take damage for light, adjusted for resistances."""
170         damage = light.base_damage()
171         colour = light.colour
172         damage = int(damage * (3 - self._resistances.get(colour, 0)) / 3.0)
173         self._health -= damage
174         if self._health <= 0 and self._age <= MAX_AGE:
175             # We die of damage
176             space.remove(self, self._shape)
177             moulds.remove(self)
178             return True
179         return False
180
181
182 class Boyd(object):
183
184     def __init__(self, gamestate, space):
185         self._moulds = []
186         self._seen_colours = set()
187         self._mould_transform = calc_colour_transform(gamestate.resistances)
188         for position in gamestate.get_spawn_positions():
189             seed = Mould(gamestate, space, position,
190                          gamestate.resistances, self._mould_transform)
191             seed.set_health(MAX_HEALTH + gamestate.days * DAY_HEALTH)
192             self._moulds.append(seed)
193         self._image = pygame.surface.Surface(SCREEN_SIZE)
194         self._image = self._image.convert_alpha(pygame.display.get_surface())
195         self._draw_moulds()
196
197     def _draw_moulds(self):
198         self._image.fill((0, 0, 0, 0))
199         for m in self._moulds:
200             self._image.blit(m.get_image(),
201                              m.pygame_pos(self._image), None,
202                              0)
203         for m in self._moulds:
204             if m.has_eyeball:
205                 self._image.blit(m.get_eyeball(), m.pygame_pos(self._image),
206                                  None, 0)
207
208     def tick(self, gamestate, space, lights):
209         redraw = False
210         # Handle spawn events
211         for mould in self._moulds[:]:
212             # Handle updates
213             if mould.tick(gamestate, space, self._moulds):
214                 redraw = True
215             # Check for damage
216             lit_by = lights.light_query(mould._shape)
217             for light in lit_by:
218                 self._seen_colours.add(light.colour)
219                 if mould.damage(light, space, self._moulds):
220                     redraw = True
221                     break  # we only die once
222         if redraw:
223             self._draw_moulds()
224
225     def render(self, surface):
226         """Draw ourselves"""
227         surface.blit(self._image, (0, 0), None, 0)
228
229     def alive(self):
230         return len(self._moulds) > 0
231
232     def update_resistances(self, gamestate):
233         for colour in self._seen_colours:
234             cur_reistance = gamestate.resistances.get(colour, 0)
235             gamestate.resistances[colour] = cur_reistance + 2
236         for colour in gamestate.resistances:
237             gamestate.resistances[colour] -= 1
238             if gamestate.resistances[colour] > 3:
239                 gamestate.resistances[colour] = 3
240             if gamestate.resistances[colour] < 0:
241                 gamestate.resistances[colour] = 0