source: gamelib/gameboard.py @ 272:cade64404997

Last change on this file since 272:cade64404997 was 272:cade64404997, checked in by Simon Cross <hodgestar@…>, 11 years ago

Use (wrecking) ball for smashing down trees.

File size: 33.7 KB
Line 
1import random
2
3import pygame
4from pygame.locals import MOUSEBUTTONDOWN, MOUSEMOTION, KEYDOWN, K_UP, K_DOWN, \
5        K_LEFT, K_RIGHT
6from pgu import gui
7
8import data
9import tiles
10import icons
11import constants
12import buildings
13import animal
14import equipment
15import sound
16import cursors
17import sprite_cursor
18import misc
19
20class OpaqueLabel(gui.Label):
21    def __init__(self, value, **params):
22        gui.Label.__init__(self, value, **params)
23        if 'width' in params:
24            self._width = params['width']
25        if 'height' in params:
26            self._height = params['height']
27        self._set_size()
28
29    def _set_size(self):
30        width, height = self.font.size(self.value)
31        width = getattr(self, '_width', width)
32        height = getattr(self, '_height', height)
33        self.style.width, self.style.height = width, height
34
35    def paint(self, s):
36        s.fill(self.style.background)
37        if self.style.align > 0:
38            r = s.get_rect()
39            w, _ = self.font.size(self.value)
40            s = s.subsurface(r.move((r.w-w, 0)).clip(r))
41        gui.Label.paint(self, s)
42
43    def update_value(self, value):
44        self.value = value
45        self._set_size()
46        self.repaint()
47
48def mklabel(text="", **params):
49    params.setdefault('color', constants.FG_COLOR)
50    params.setdefault('width', GameBoard.TOOLBAR_WIDTH/2)
51    return OpaqueLabel(text, **params)
52
53def mkcountupdate(counter):
54    def update_counter(self, value):
55        getattr(self, counter).update_value("%s  " % value)
56        self.repaint()
57    return update_counter
58
59class ToolBar(gui.Table):
60    def __init__(self, gameboard, **params):
61        gui.Table.__init__(self, **params)
62        self.group = gui.Group(name='toolbar', value=None)
63        self._next_tool_value = 0
64        self.gameboard = gameboard
65        self.cash_counter = mklabel(align=1)
66        self.chicken_counter = mklabel(align=1)
67        self.egg_counter = mklabel(align=1)
68        self.day_counter = mklabel(align=1)
69        self.killed_foxes = mklabel(align=1)
70
71        self.tr()
72        self.td(gui.Spacer(self.rect.w/2, 0))
73        self.td(gui.Spacer(self.rect.w/2, 0))
74        self.add_counter(mklabel("Day:"), self.day_counter)
75        self.add_counter(mklabel("Groats:"), self.cash_counter)
76        self.add_counter(mklabel("Eggs:"), self.egg_counter)
77        self.add_counter(icons.CHKN_ICON, self.chicken_counter)
78        self.add_counter(icons.KILLED_FOX, self.killed_foxes)
79        self.add_spacer(20)
80
81        self.add_tool_button("Move Hen", constants.TOOL_PLACE_ANIMALS,
82                None, cursors.cursors['select'])
83        self.add_tool_button("Cut Trees", constants.TOOL_LOGGING,
84                constants.LOGGING_PRICE, cursors.cursors['ball'])
85        self.add_spacer(20)
86
87        self.add_heading("Sell ...")
88        self.add_tool_button("Chicken", constants.TOOL_SELL_CHICKEN,
89                constants.SELL_PRICE_CHICKEN, cursors.cursors['sell'])
90        self.add_tool_button("Egg", constants.TOOL_SELL_EGG,
91                constants.SELL_PRICE_EGG, cursors.cursors['sell'])
92        self.add_tool_button("Building", constants.TOOL_SELL_BUILDING,
93                None, cursors.cursors['sell'])
94        self.add_tool_button("Equipment", constants.TOOL_SELL_EQUIPMENT,
95                None, cursors.cursors['sell'])
96        self.add_spacer(20)
97
98
99        self.add_heading("Buy ...")
100
101        self.add_tool_button("Fence", constants.TOOL_BUY_FENCE,
102                "%s/%s" % (constants.BUY_PRICE_FENCE,
103                           constants.REPAIR_PRICE_FENCE))
104
105        for building_cls in buildings.BUILDINGS:
106            self.add_tool_button(building_cls.NAME.title(), building_cls,
107                    None, cursors.cursors.get('build', None))
108
109        for equipment_cls in equipment.EQUIPMENT:
110            self.add_tool_button(equipment_cls.NAME.title(),
111                    equipment_cls,
112                    equipment_cls.BUY_PRICE,
113                    cursors.cursors.get('buy', None))
114
115        self.add_spacer(30)
116
117        self.add_tool("Finished Day", self.day_done)
118
119    def day_done(self):
120        import engine
121        pygame.event.post(engine.START_NIGHT)
122
123    update_cash_counter = mkcountupdate('cash_counter')
124    update_fox_counter = mkcountupdate('killed_foxes')
125    update_chicken_counter = mkcountupdate('chicken_counter')
126    update_egg_counter = mkcountupdate('egg_counter')
127    update_day_counter = mkcountupdate('day_counter')
128
129    def add_spacer(self, height):
130        self.tr()
131        self.td(gui.Spacer(0, height), colspan=2)
132
133    def add_heading(self, text):
134        self.tr()
135        self.td(mklabel(text), colspan=2)
136
137    def add_tool_button(self, text, tool, price=None, cursor=None):
138        if price is not None:
139            text = "%s  (%s)" % (text, price)
140        self.add_tool(text, lambda: self.gameboard.set_selected_tool(tool,
141            cursor))
142
143    def add_tool(self, text, func):
144        label = gui.basic.Label(text)
145        value = self._next_tool_value
146        self._next_tool_value += 1
147        tool = gui.Tool(self.group, label, value, width=self.rect.w, style={"padding_left": 0})
148        tool.connect(gui.CLICK, func)
149        self.tr()
150        self.td(tool, align=-1, colspan=2)
151
152    def clear_tool(self):
153        self.group.value = None
154
155    def add_counter(self, icon, label):
156        self.tr()
157        self.td(icon, width=self.rect.w/2)
158        self.td(label, width=self.rect.w/2)
159
160    def resize(self, width=None, height=None):
161        width, height = gui.Table.resize(self, width, height)
162        width = GameBoard.TOOLBAR_WIDTH
163        return width, height
164
165
166class VidWidget(gui.Widget):
167    def __init__(self, gameboard, vid, **params):
168        gui.Widget.__init__(self, **params)
169        self.gameboard = gameboard
170        self.vid = vid
171        self.vid.bounds = pygame.Rect((0, 0), vid.tile_to_view(vid.size))
172
173    def paint(self, surface):
174        self.vid.paint(surface)
175        # Blit animation frames on top of the drawing
176        x, y = self.vid.view.x, self.vid.view.y
177        for anim in self.gameboard.animations:
178            anim.fix_pos(self.vid)
179            anim.irect.x = anim.rect.x - anim.shape.x
180            anim.irect.y = anim.rect.y - anim.shape.y
181            surface.blit(anim.image, (anim.irect.x - x, anim.irect.y - y))
182            # We don't store anim._irect, since we only update anims if the
183            # image changes, which kills irect
184
185    def update(self, surface):
186        us = []
187        x, y = self.vid.view.x, self.vid.view.y
188        for anim in self.gameboard.animations[:]:
189            if anim.updated or anim.removed:
190                # We flag that we need to redraw stuff undeneath the animation
191                us.append(pygame.Rect(anim.irect.x - x, anim.irect.y - y,
192                    anim.irect.width, anim.irect.height))
193                self.vid.alayer[anim.pos.y][anim.pos.x]=1
194                self.vid.updates.append(anim.pos.to_tuple())
195            if anim.removed:
196                # Remove the animation from the draw loop
197                self.gameboard.animations.remove(anim)
198        us.extend(self.vid.update(surface))
199        for anim in self.gameboard.animations:
200            if anim.updated: 
201                anim.fix_pos(self.vid)
202                # setimage has happened, so redraw
203                anim.irect.x = anim.rect.x - anim.shape.x
204                anim.irect.y = anim.rect.y - anim.shape.y
205                surface.blit(anim.image, (anim.irect.x - x, anim.irect.y - y))
206                anim.updated = 0
207                us.append(pygame.Rect(anim.irect.x - x, anim.irect.y - y,
208                    anim.irect.width, anim.irect.height))
209                # This is enough, because sprite changes happen disjoint
210                # from the animation sequence, so we don't need to worry
211                # other changes forcing us to redraw the animation frame.
212        return us
213
214    def move_view(self, x, y):
215        self.vid.view.move_ip((x, y))
216
217    def event(self, e):
218        if e.type == MOUSEBUTTONDOWN:
219            self.gameboard.use_tool(e)
220        elif e.type == MOUSEMOTION and self.gameboard.sprite_cursor:
221            self.gameboard.update_sprite_cursor(e)
222
223
224class GameBoard(object):
225    TILE_DIMENSIONS = (20, 20)
226    TOOLBAR_WIDTH = 140
227
228    GRASSLAND = tiles.REVERSE_TILE_MAP['grassland']
229    FENCE = tiles.REVERSE_TILE_MAP['fence']
230    WOODLAND = tiles.REVERSE_TILE_MAP['woodland']
231    BROKEN_FENCE = tiles.REVERSE_TILE_MAP['broken fence']
232
233    FOX_WEIGHTINGS = (
234        (animal.Fox, 60),
235        (animal.GreedyFox, 30),
236        (animal.NinjaFox, 5),
237        (animal.DemoFox, 5),
238        )
239
240    def __init__(self, main_app):
241        self.disp = main_app
242        self.tv = tiles.FarmVid()
243        self.tv.png_folder_load_tiles(data.filepath('tiles'))
244        self.tv.tga_load_level(data.filepath('level1.tga'))
245        self.create_display()
246
247        self.selected_tool = None
248        self.animal_to_place = None
249        self.sprite_cursor = None
250        self.chickens = set()
251        self.foxes = set()
252        self.buildings = []
253        self.animations = []
254        self.cash = 0
255        self.eggs = 0
256        self.days = 0
257        self.killed_foxes = 0
258        self.add_cash(constants.STARTING_CASH)
259        self.day, self.night = True, False
260
261        self.fix_buildings()
262
263        self.add_some_chickens()
264
265    def get_top_widget(self):
266        return self.top_widget
267
268    def create_display(self):
269        width, height = self.disp.rect.w, self.disp.rect.h
270        tbl = gui.Table()
271        tbl.tr()
272        self.toolbar = ToolBar(self, width=self.TOOLBAR_WIDTH)
273        tbl.td(self.toolbar, valign=-1)
274        self.tvw = VidWidget(self, self.tv, width=width-self.TOOLBAR_WIDTH, height=height)
275        tbl.td(self.tvw)
276        self.top_widget = tbl
277
278    def update(self):
279        self.tvw.reupdate()
280
281    def loop(self):
282        self.tv.loop()
283
284    def set_selected_tool(self, tool, cursor):
285        if not self.day:
286            return
287        self.selected_tool = tool
288        if self.animal_to_place:
289            # Clear any highlights
290            self.animal_to_place.unequip_by_name("spotlight")
291        self.select_animal_to_place(None)
292        sprite_curs = None
293        if buildings.is_building(tool):
294            sprite_curs = sprite_cursor.SpriteCursor(tool.IMAGE, self.tv, tool.BUY_PRICE)
295        elif equipment.is_equipment(tool):
296            sprite_curs = sprite_cursor.SpriteCursor(tool.CHICKEN_IMAGE_FILE, self.tv)
297        elif tool == constants.TOOL_BUY_FENCE:
298            sprite_curs = sprite_cursor.SpriteCursor("tiles/fence.png", self.tv)
299        self.set_cursor(cursor, sprite_curs)
300
301    def set_cursor(self, cursor=None, sprite_curs=None):
302        if cursor:
303            pygame.mouse.set_cursor(*cursor)
304        else:
305            pygame.mouse.set_cursor(*cursors.cursors['arrow'])
306        self.sprite_cursor = sprite_curs
307        self.tv.sprites.set_cursor(sprite_curs)
308
309    def reset_states(self):
310        """Clear current states (highlights, etc.)"""
311        self.set_selected_tool(None, None)
312        self.toolbar.clear_tool()
313
314    def update_sprite_cursor(self, e):
315        tile_pos = self.tv.screen_to_tile(e.pos)
316        self.sprite_cursor.set_pos(tile_pos)
317
318    def start_night(self):
319        self.day, self.night = False, True
320        self.tv.sun(False)
321        self.reset_states()
322
323    def start_day(self):
324        self.day, self.night = True, False
325        self.tv.sun(True)
326        self.reset_states()
327
328    def in_bounds(self, pos):
329        """Check if a position is within the game boundaries"""
330        if pos.x < 0 or pos.y < 0:
331            return False
332        width, height = self.tv.size
333        if pos.x >= width or pos.y >= height:
334            return False
335        return True
336
337    def use_tool(self, e):
338        if not self.day:
339            return
340        if e.button == 3: # Right button
341            self.selected_tool = None
342            self.set_cursor()
343        elif e.button != 1: # Left button
344            return
345        if self.selected_tool == constants.TOOL_SELL_CHICKEN:
346            self.sell_chicken(self.tv.screen_to_tile(e.pos))
347        elif self.selected_tool == constants.TOOL_SELL_EGG:
348            self.sell_egg(self.tv.screen_to_tile(e.pos))
349        elif self.selected_tool == constants.TOOL_PLACE_ANIMALS:
350            self.place_animal(self.tv.screen_to_tile(e.pos))
351        elif self.selected_tool == constants.TOOL_BUY_FENCE:
352            self.buy_fence(self.tv.screen_to_tile(e.pos))
353        elif self.selected_tool == constants.TOOL_SELL_BUILDING:
354            self.sell_building(self.tv.screen_to_tile(e.pos))
355        elif self.selected_tool == constants.TOOL_SELL_EQUIPMENT:
356            self.sell_equipment(self.tv.screen_to_tile(e.pos))
357        elif self.selected_tool == constants.TOOL_LOGGING:
358            self.logging_forest(self.tv.screen_to_tile(e.pos))
359        elif buildings.is_building(self.selected_tool):
360            self.buy_building(self.tv.screen_to_tile(e.pos), self.selected_tool)
361        elif equipment.is_equipment(self.selected_tool):
362            self.buy_equipment(self.tv.screen_to_tile(e.pos), self.selected_tool)
363
364    def get_outside_chicken(self, tile_pos):
365        for chick in self.chickens:
366            if chick.covers(tile_pos) and chick.outside():
367                return chick
368        return None
369
370    def get_building(self, tile_pos):
371        for building in self.buildings:
372            if building.covers(tile_pos):
373                return building
374        return None
375
376    def sell_chicken(self, tile_pos):
377
378        def do_sell(chicken):
379            if not chicken:
380                return False # sanity check
381            if len(self.chickens) == 1:
382                print "You can't sell your last chicken!"
383                return False
384            for item in list(chicken.equipment):
385                self.add_cash(item.sell_price())
386                chicken.unequip(item)
387            self.add_cash(constants.SELL_PRICE_CHICKEN)
388            sound.play_sound("sell-chicken.ogg")
389            return True
390
391        chick = self.get_outside_chicken(tile_pos)
392        if chick is None:
393            building = self.get_building(tile_pos)
394            if building and building.NAME in buildings.HENHOUSES:
395                self.open_building_dialog(building, do_sell)
396            return
397
398        if do_sell(chick):
399            self.remove_chicken(chick)
400
401    def sell_one_egg(self, chicken):
402        if chicken.eggs:
403            self.add_cash(constants.SELL_PRICE_EGG)
404            chicken.remove_one_egg()
405            self.eggs -= 1
406            self.toolbar.update_egg_counter(self.eggs)
407            return True
408        return False
409
410    def sell_egg(self, tile_pos):
411        def do_sell(chicken):
412            # We try sell and egg
413            if self.sell_one_egg(chicken):
414                sound.play_sound("sell-chicken.ogg")
415                # Force toolbar update
416                self.toolbar.chsize()
417            return False
418
419        building = self.get_building(tile_pos)
420        if building and building.NAME in buildings.HENHOUSES:
421            self.open_building_dialog(building, do_sell)
422
423    def select_animal_to_place(self, animal):
424        if self.animal_to_place:
425            self.animal_to_place.unequip_by_name("spotlight")
426        self.animal_to_place = animal
427        if self.animal_to_place:
428            self.animal_to_place.equip(equipment.Spotlight())
429
430    def place_animal(self, tile_pos):
431        """Handle an TOOL_PLACE_ANIMALS click.
432
433           This will either select an animal or
434           place a selected animal in a building.
435           """
436        chicken = self.get_outside_chicken(tile_pos)
437        if chicken:
438            if chicken is self.animal_to_place:
439                self.select_animal_to_place(None)
440                pygame.mouse.set_cursor(*cursors.cursors['select'])
441            else:
442                self.select_animal_to_place(chicken)
443                pygame.mouse.set_cursor(*cursors.cursors['chicken'])
444            return
445        building = self.get_building(tile_pos)
446        if building:
447            if self.animal_to_place:
448                try:
449                    place = building.first_empty_place()
450                    self.relocate_animal(self.animal_to_place, place=place)
451                    self.animal_to_place.equip(equipment.Nest())
452                    self.select_animal_to_place(None)
453                    pygame.mouse.set_cursor(*cursors.cursors['select'])
454                except buildings.BuildingFullError:
455                    pass
456            else:
457                self.open_building_dialog(building)
458            return
459        if self.tv.get(tile_pos) == self.GRASSLAND:
460            if self.animal_to_place is not None:
461                self.animal_to_place.unequip_by_name("nest")
462                self.relocate_animal(self.animal_to_place, tile_pos=tile_pos)
463                self.eggs -= self.animal_to_place.get_num_eggs()
464                self.animal_to_place.remove_eggs()
465                self.toolbar.update_egg_counter(self.eggs)
466
467    def relocate_animal(self, chicken, tile_pos=None, place=None):
468        assert((tile_pos, place) != (None, None))
469        if chicken.abode is not None:
470            chicken.abode.clear_occupant()
471        if tile_pos:
472            chicken.set_pos(tile_pos)
473        else:
474            place.set_occupant(chicken)
475            chicken.set_pos(place.get_pos())
476        self.set_visibility(chicken)
477
478    def set_visibility(self, chicken):
479        if chicken.outside():
480            if chicken not in self.tv.sprites:
481                self.tv.sprites.append(chicken)
482        else:
483            if chicken in self.tv.sprites:
484                self.tv.sprites.remove(chicken)
485
486    def open_dialog(self, widget, close_callback=None):
487        """Open a dialog for the given widget. Add close button."""
488        tbl = gui.Table()
489
490        def close_dialog():
491            self.disp.close(tbl)
492            if close_callback is not None:
493                close_callback()
494
495        close_button = gui.Button("Close")
496        close_button.connect(gui.CLICK, close_dialog)
497
498        tbl = gui.Table()
499        tbl.tr()
500        tbl.td(widget, colspan=2)
501        tbl.tr()
502        tbl.td(gui.Spacer(100, 0))
503        tbl.td(close_button, align=1)
504
505        self.disp.open(tbl)
506        return tbl
507
508    def open_building_dialog(self, building, sell_callback=None):
509        """Create dialog for manipulating the contents of a building."""
510
511        place_button_map = {}
512
513        def update_button(animal, empty=False):
514            """Update a button image (either to the animal, or to empty)."""
515            if animal:
516                button = place_button_map.get(id(animal.abode))
517                if button:
518                    if empty:
519                        button.value = icons.EMPTY_NEST_ICON
520                    else:
521                        button.value = icons.animal_icon(animal)
522
523        def nest_clicked(place, button):
524            """Handle a nest being clicked."""
525            # sell_callback should return true if we need to remove the
526            # occupant
527            if place.occupant:
528                # there is an occupant, select or sell it
529                if not sell_callback:
530                    old_animal = self.animal_to_place
531                    self.select_animal_to_place(place.occupant)
532                    # deselect old animal (on button)
533                    update_button(old_animal)
534                    # select new animal (on button)
535                    update_button(self.animal_to_place)
536                else:
537                    # Attempt to sell the occupant
538                    if sell_callback(place.occupant):
539                        # empty the nest (on button)
540                        update_button(place.occupant, empty=True)
541                        self.remove_chicken(place.occupant)
542                    else:
543                        # Update for equipment changes, etc.
544                        update_button(place.occupant)
545            else:
546                # there is no occupant, attempt to fill the space
547                if self.animal_to_place is not None:
548                    # empty old nest (on button)
549                    update_button(self.animal_to_place, empty=True)
550                    self.relocate_animal(self.animal_to_place, place=place)
551                    # populate the new nest (on button)
552                    update_button(self.animal_to_place)
553
554        tbl = gui.Table()
555        columns = building.max_floor_width()
556        kwargs = { 'style': { 'padding_left': 10, 'padding_bottom': 10 }}
557        for floor in building.floors():
558            tbl.tr()
559            tbl.td(gui.Button(floor.title), colspan=columns, align=-1, **kwargs)
560            tbl.tr()
561            for row in floor.rows():
562                tbl.tr()
563                for place in row:
564                    if place.occupant is None:
565                        button = gui.Button(icons.EMPTY_NEST_ICON)
566                    else:
567                        button = gui.Button(icons.animal_icon(place.occupant))
568                    place_button_map[id(place)] = button
569                    button.connect(gui.CLICK, nest_clicked, place, button)
570                    tbl.td(button, **kwargs)
571
572        building.selected(True)
573        def close_callback():
574            building.selected(False)
575
576        def evict_callback():
577            if not self.animal_to_place:
578                return
579            for tile_pos in building.adjacent_tiles():
580                if self.tv.get(tile_pos) != self.GRASSLAND:
581                    continue
582                if self.get_outside_chicken(tile_pos) is None:
583                    update_button(self.animal_to_place, empty=True)
584                    self.place_animal(tile_pos)
585                    break
586
587        if not sell_callback:
588            tbl.tr()
589            button = gui.Button('Evict')
590            button.connect(gui.CLICK, evict_callback)
591            tbl.td(button, colspan=2, **kwargs)
592
593        self.open_dialog(tbl, close_callback=close_callback)
594
595    def buy_fence(self, tile_pos):
596        this_tile = self.tv.get(tile_pos)
597        if this_tile not in [self.GRASSLAND, self.BROKEN_FENCE]:
598            return
599        if this_tile == self.GRASSLAND:
600            cost = constants.BUY_PRICE_FENCE
601        else:
602            cost = constants.REPAIR_PRICE_FENCE
603        if any((chicken.pos.x, chicken.pos.y) == tile_pos for chicken in self.chickens):
604            return
605
606        if self.cash < cost:
607            print "You can't afford a fence."
608            return
609        self.add_cash(-cost)
610        self.tv.set(tile_pos, self.FENCE)
611
612    def sell_fence(self, tile_pos):
613        this_tile = self.tv.get(tile_pos)
614        if this_tile not in [self.FENCE, self.BROKEN_FENCE]:
615            return
616        if this_tile == self.FENCE:
617            self.add_cash(constants.SELL_PRICE_FENCE)
618        elif this_tile == self.BROKEN_FENCE:
619            self.add_cash(constants.SELL_PRICE_BROKEN_FENCE)
620        self.tv.set(tile_pos, self.GRASSLAND)
621
622    def logging_forest(self, tile_pos):
623        if self.tv.get(tile_pos) != self.WOODLAND:
624            return
625        if self.cash < constants.LOGGING_PRICE:
626            return
627        self.add_cash(-constants.LOGGING_PRICE)
628        self.tv.set(tile_pos, self.GRASSLAND)
629
630    def buy_building(self, tile_pos, building_cls):
631        building = building_cls(tile_pos)
632        if self.cash < building.buy_price():
633            return
634        if any(building.covers((chicken.pos.x, chicken.pos.y)) for chicken in self.chickens):
635            return
636        if building.place(self.tv):
637            self.add_cash(-building.buy_price())
638            self.add_building(building)
639
640    def buy_equipment(self, tile_pos, equipment_cls):
641        chicken = self.get_outside_chicken(tile_pos)
642        equipment = equipment_cls()
643        if chicken is None or self.cash < equipment.buy_price():
644            return
645        if equipment.place(chicken):
646            self.add_cash(-equipment.buy_price())
647            chicken.equip(equipment)
648
649    def sell_building(self, tile_pos):
650        if self.tv.get(tile_pos) in [self.FENCE, self.BROKEN_FENCE]:
651            return self.sell_fence(tile_pos)
652        building = self.get_building(tile_pos)
653        if building is None:
654            return
655        if list(building.occupants()):
656            warning = gui.Button("Occupied buildings may not be sold.")
657            self.open_dialog(warning)
658            return
659        self.add_cash(building.sell_price())
660        building.remove(self.tv)
661        self.remove_building(building)
662
663    def sell_equipment(self, tile_pos):
664        chicken = self.get_outside_chicken(tile_pos)
665        if chicken is None or not chicken.equipment:
666            return
667        if len(chicken.equipment) == 1:
668            item = chicken.equipment[0]
669            self.add_cash(item.sell_price())
670            chicken.unequip(item)
671        else:
672            self.open_equipment_dialog(chicken)
673
674    def open_equipment_dialog(self, chicken):
675        tbl = gui.Table()
676
677        def sell_item(item, button):
678            """Select item of equipment."""
679            self.add_cash(item.sell_price())
680            chicken.unequip(item)
681            self.disp.close(dialog)
682
683        kwargs = { 'style': { 'padding_left': 10, 'padding_bottom': 10 }}
684
685        tbl.tr()
686        tbl.td(gui.Button("Sell ...     "), align=-1, **kwargs)
687
688        for item in chicken.equipment:
689            tbl.tr()
690            button = gui.Button(item.name().title())
691            button.connect(gui.CLICK, sell_item, item, button)
692            tbl.td(button, align=1, **kwargs)
693
694        dialog = self.open_dialog(tbl)
695
696    def event(self, e):
697        if e.type == KEYDOWN and e.key in [K_UP, K_DOWN, K_LEFT, K_RIGHT]:
698            if e.key == K_UP:
699                self.tvw.move_view(0, -self.TILE_DIMENSIONS[1])
700            if e.key == K_DOWN:
701                self.tvw.move_view(0, self.TILE_DIMENSIONS[1])
702            if e.key == K_LEFT:
703                self.tvw.move_view(-self.TILE_DIMENSIONS[0], 0)
704            if e.key == K_RIGHT:
705                self.tvw.move_view(self.TILE_DIMENSIONS[0], 0)
706            return True
707        return False
708
709    def advance_day(self):
710        self.days += 1
711        self.toolbar.update_day_counter(self.days)
712
713    def clear_foxes(self):
714        for fox in self.foxes.copy():
715            # Any foxes that didn't make it to the woods are automatically
716            # killed
717            if self.in_bounds(fox.pos) and self.tv.get(fox.pos.to_tuple()) \
718                    != self.WOODLAND:
719                self.kill_fox(fox)
720            else:
721                self.tv.sprites.remove(fox)
722        self.foxes = set() # Remove all the foxes
723
724    def run_animations(self):
725        for anim in self.animations:
726            anim.animate()
727
728    def move_foxes(self):
729        """Move the foxes.
730       
731           We return True if there are no more foxes to move or all the
732           foxes are safely back. This end's the night"""
733        if not self.foxes:
734            return True
735        over = True
736        for fox in self.foxes:
737            fox.move(self)
738            if not fox.safe:
739                over = False
740        for chicken in self.chickens:
741            chicken.attack(self)
742        return over
743
744    def add_chicken(self, chicken):
745        self.chickens.add(chicken)
746        if chicken.outside():
747            self.tv.sprites.append(chicken)
748        self.toolbar.update_chicken_counter(len(self.chickens))
749
750    def add_fox(self, fox):
751        self.foxes.add(fox)
752        self.tv.sprites.append(fox)
753
754    def add_building(self, building):
755        self.buildings.append(building)
756        self.tv.sprites.append(building)
757
758    def lay_eggs(self):
759        self.eggs = 0
760        for building in self.buildings:
761            if building.NAME in buildings.HENHOUSES:
762                for chicken in building.occupants():
763                    chicken.lay()
764                    self.eggs += chicken.get_num_eggs()
765        self.toolbar.update_egg_counter(self.eggs)
766
767    def hatch_eggs(self):
768        for building in self.buildings:
769            if building.NAME in buildings.HENHOUSES:
770                for chicken in building.occupants():
771                    new_chick = chicken.hatch(self)
772                    if new_chick:
773                        try:
774                            building.add_occupant(new_chick)
775                            self.add_chicken(new_chick)
776                            new_chick.equip(equipment.Nest())
777                        except buildings.BuildingFullError:
778                            # No space in the hen house, look nearby
779                            for tile_pos in building.adjacent_tiles():
780                                if self.tv.get(tile_pos) != self.GRASSLAND:
781                                    continue
782                                if self.get_outside_chicken(tile_pos) is None:
783                                    self.add_chicken(new_chick)
784                                    self.relocate_animal(new_chick, tile_pos=tile_pos)
785                                    break
786                            # if there isn't a space for the
787                            # new chick it dies. :/ Farm life
788                            # is cruel.
789        self.toolbar.update_egg_counter(self.eggs)
790
791    def kill_fox(self, fox):
792        self.killed_foxes += 1
793        self.toolbar.update_fox_counter(self.killed_foxes)
794        self.add_cash(constants.SELL_PRICE_DEAD_FOX)
795        self.remove_fox(fox)
796
797    def remove_fox(self, fox):
798        self.foxes.discard(fox)
799        if fox in self.tv.sprites:
800            self.tv.sprites.remove(fox)
801
802    def remove_chicken(self, chick):
803        if chick is self.animal_to_place:
804            self.select_animal_to_place(None)
805        self.chickens.discard(chick)
806        self.eggs -= chick.get_num_eggs()
807        self.toolbar.update_egg_counter(self.eggs)
808        if chick.abode:
809            chick.abode.clear_occupant()
810        self.toolbar.update_chicken_counter(len(self.chickens))
811        if chick in self.tv.sprites and chick.outside():
812            self.tv.sprites.remove(chick)
813
814    def remove_building(self, building):
815        if building in self.buildings:
816            self.buildings.remove(building)
817            self.tv.sprites.remove(building)
818
819    def add_cash(self, amount):
820        self.cash += amount
821        self.toolbar.update_cash_counter(self.cash)
822
823    def add_some_chickens(self):
824        """Add some random chickens to start the game"""
825        x, y = 0, 0
826        width, height = self.tv.size
827        tries = 0
828        while len(self.chickens) < 10:
829            if x < width:
830                tile = self.tv.get((x, y))
831            else:
832                y += 1
833                if y >= height:
834                    y = 0
835                    tries += 1
836                    if tries > 3:
837                        break # Things have gone wierd
838                x = 0
839                continue
840            # See if we place a chicken
841            if 'grassland' == tiles.TILE_MAP[tile]:
842                # Farmland
843                roll = random.randint(1, 20)
844                # We don't place within a tile of the fence, this is to make things
845                # easier
846                for xx in range(x-1, x+2):
847                    if xx >= width or xx < 0:
848                        continue
849                    for yy in range(y-1, y+2):
850                        if yy >= height or yy < 0:
851                            continue
852                        neighbour = self.tv.get((xx, yy))
853                        if 'fence' == tiles.TILE_MAP[neighbour]:
854                            # Fence
855                            roll = 10
856                if roll == 1:
857                    # Create a chicken
858                    chick = animal.Chicken((x, y))
859                    self.add_chicken(chick)
860            x += 1
861
862    def _choose_fox(self, (x, y)):
863        fox_cls = misc.WeightedSelection(self.FOX_WEIGHTINGS).choose()
864        return fox_cls((x, y))
865
866    def spawn_foxes(self):
867        """The foxes come at night, and this is where they come from."""
868        # Foxes spawn just outside the map
869        x, y = 0, 0
870        width, height = self.tv.size
871        min_foxes = (self.days+3)/2 # always more than one fox
872        new_foxes = random.randint(min_foxes, min_foxes*2)
873        while len(self.foxes) < new_foxes:
874            side = random.randint(0, 3)
875            if side == 0:
876                # top
877                y = -1
878                x = random.randint(-1, width)
879            elif side == 1:
880                # bottom
881                y = height
882                x = random.randint(-1, width)
883            elif side == 2:
884                # left
885                x = -1
886                y = random.randint(-1, height)
887            else:
888                x = width
889                y = random.randint(-1, height)
890            skip = False
891            for other_fox in self.foxes:
892                if other_fox.pos.x == x and other_fox.pos.y == y:
893                    skip = True # Choose a new position
894                    break
895            if not skip:
896                self.add_fox(self._choose_fox((x, y)))
897
898    def fix_buildings(self):
899        """Go through the level map looking for buildings that haven't
900           been added to self.buildings and adding them.
901
902           Where partial buildings exist (i.e. places where the building
903           cannot fit on the available tiles) the building is added anyway
904           to the top left corner.
905
906           Could be a lot faster.
907           """
908        tile_to_building = dict((b.TILE_NO, b) for b in buildings.BUILDINGS)
909
910        w, h = self.tv.size
911        for x in xrange(w):
912            for y in xrange(h):
913                tile_pos = (x, y)
914                tile_no = self.tv.get(tile_pos)
915                if tile_no not in tile_to_building:
916                    continue
917
918                covered = False
919                for building in self.buildings:
920                    if building.covers(tile_pos):
921                        covered = True
922                        break
923
924                if covered:
925                    continue
926
927                building_cls = tile_to_building[tile_no]
928                building = building_cls(tile_pos)
929                building.remove(self.tv)
930                building.place(self.tv)
931                self.add_building(building)
932
933    def is_game_over(self):
934        """Return true if we're complete"""
935        if self.days > constants.TURN_LIMIT:
936            return True
937        if len(self.chickens) == 0:
938            return True
Note: See TracBrowser for help on using the repository browser.