source: gamelib/gameboard.py @ 405:ae3cee7a1337

Last change on this file since 405:ae3cee7a1337 was 405:ae3cee7a1337, checked in by Neil Muller <drnlmuller@…>, 11 years ago

Fix crash due to invalid cache

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