comparison pyntnclick/state.py @ 548:ded4324b236e pyntnclick

Moved stuff around, broke everything.
author Jeremy Thurgood <firxen@gmail.com>
date Sat, 11 Feb 2012 13:10:18 +0200
parents gamelib/state.py@a6f9b6edb6c7
children 38fb04728ac5
comparison
equal deleted inserted replaced
547:33ce7ff757c3 548:ded4324b236e
1 """Utilities and base classes for dealing with scenes."""
2
3 import copy
4
5 from albow.resource import get_image
6 from albow.utils import frame_rect
7 from widgets import BoomLabel
8 from pygame.rect import Rect
9 from pygame.color import Color
10
11 import constants
12 from scenes import SCENE_LIST, INITIAL_SCENE
13 from sound import get_sound
14
15 # override the initial scene to for debugging
16 DEBUG_SCENE = None
17
18 # whether to show debugging rects
19 DEBUG_RECTS = False
20
21
22 class Result(object):
23 """Result of interacting with a thing"""
24
25 def __init__(self, message=None, soundfile=None, detail_view=None,
26 style=None, close_detail=False, end_game=False):
27 self.message = message
28 self.sound = None
29 if soundfile:
30 self.sound = get_sound(soundfile)
31 self.detail_view = detail_view
32 self.style = style
33 self.close_detail = close_detail
34 self.end_game = end_game
35
36 def process(self, scene_widget):
37 """Helper function to do the right thing with a result object"""
38 if self.sound:
39 self.sound.play()
40 if self.message:
41 scene_widget.show_message(self.message, self.style)
42 if self.detail_view:
43 scene_widget.show_detail(self.detail_view)
44 if (self.close_detail
45 and hasattr(scene_widget, 'parent')
46 and hasattr(scene_widget.parent, 'clear_detail')):
47 scene_widget.parent.clear_detail()
48 if self.end_game:
49 scene_widget.end_game()
50
51
52 def handle_result(result, scene_widget):
53 """Handle dealing with result or result sequences"""
54 if result:
55 if hasattr(result, 'process'):
56 result.process(scene_widget)
57 else:
58 for res in result:
59 if res:
60 # List may contain None's
61 res.process(scene_widget)
62
63
64 def initial_state():
65 """Load the initial state."""
66 state = GameState()
67 for scene in SCENE_LIST:
68 state.load_scenes(scene)
69 initial_scene = INITIAL_SCENE if DEBUG_SCENE is None else DEBUG_SCENE
70 state.set_current_scene(initial_scene)
71 state.set_do_enter_leave()
72 return state
73
74
75 class GameState(object):
76 """Complete game state.
77
78 Game state consists of:
79
80 * items
81 * scenes
82 """
83
84 def __init__(self):
85 # map of scene name -> Scene object
86 self.scenes = {}
87 # map of detail view name -> DetailView object
88 self.detail_views = {}
89 # map of item name -> Item object
90 self.items = {}
91 # list of item objects in inventory
92 self.inventory = []
93 # currently selected tool (item)
94 self.tool = None
95 # current scene
96 self.current_scene = None
97 # current detail view
98 self.current_detail = None
99 # scene we came from, for enter and leave processing
100 self.previous_scene = None
101 # scene transion helpers
102 self.do_check = None
103 self.old_pos = None
104 # current thing
105 self.current_thing = None
106 self.highlight_override = False
107
108 def add_scene(self, scene):
109 self.scenes[scene.name] = scene
110
111 def add_detail_view(self, detail_view):
112 self.detail_views[detail_view.name] = detail_view
113
114 def add_item(self, item):
115 self.items[item.name] = item
116 item.set_state(self)
117
118 def load_scenes(self, modname):
119 mod = __import__("gamelib.scenes.%s" % (modname,), fromlist=[modname])
120 for scene_cls in mod.SCENES:
121 self.add_scene(scene_cls(self))
122 if hasattr(mod, 'DETAIL_VIEWS'):
123 for scene_cls in mod.DETAIL_VIEWS:
124 self.add_detail_view(scene_cls(self))
125
126 def set_current_scene(self, name):
127 old_scene = self.current_scene
128 self.current_scene = self.scenes[name]
129 self.current_thing = None
130 if old_scene and old_scene != self.current_scene:
131 self.previous_scene = old_scene
132 self.set_do_enter_leave()
133
134 def set_current_detail(self, name):
135 self.current_thing = None
136 if name is None:
137 self.current_detail = None
138 else:
139 self.current_detail = self.detail_views[name]
140 return self.current_detail
141
142 def add_inventory_item(self, name):
143 self.inventory.append(self.items[name])
144
145 def is_in_inventory(self, name):
146 if name in self.items:
147 return self.items[name] in self.inventory
148 return False
149
150 def remove_inventory_item(self, name):
151 self.inventory.remove(self.items[name])
152 # Unselect tool if it's removed
153 if self.tool == self.items[name]:
154 self.set_tool(None)
155
156 def replace_inventory_item(self, old_item_name, new_item_name):
157 """Try to replace an item in the inventory with a new one"""
158 try:
159 index = self.inventory.index(self.items[old_item_name])
160 self.inventory[index] = self.items[new_item_name]
161 if self.tool == self.items[old_item_name]:
162 self.set_tool(self.items[new_item_name])
163 except ValueError:
164 return False
165 return True
166
167 def set_tool(self, item):
168 self.tool = item
169
170 def interact(self, pos):
171 return self.current_scene.interact(self.tool, pos)
172
173 def interact_detail(self, pos):
174 return self.current_detail.interact(self.tool, pos)
175
176 def cancel_doodah(self, screen):
177 if self.tool:
178 self.set_tool(None)
179 elif self.current_detail:
180 screen.state_widget.clear_detail()
181
182 def do_enter_detail(self):
183 if self.current_detail:
184 self.current_detail.enter()
185
186 def do_leave_detail(self):
187 if self.current_detail:
188 self.current_detail.leave()
189
190 def animate(self):
191 if not self.do_check:
192 return self.current_scene.animate()
193
194 def check_enter_leave(self, screen):
195 if not self.do_check:
196 return None
197 if self.do_check == constants.LEAVE:
198 self.do_check = constants.ENTER
199 if self.previous_scene:
200 return self.previous_scene.leave()
201 return None
202 elif self.do_check == constants.ENTER:
203 self.do_check = None
204 # Fix descriptions, etc.
205 if self.old_pos:
206 self.current_scene.update_current_thing(self.old_pos)
207 return self.current_scene.enter()
208 raise RuntimeError('invalid do_check value %s' % self.do_check)
209
210 def set_do_enter_leave(self):
211 """Flag that we need to run the enter loop"""
212 self.do_check = constants.LEAVE
213
214
215 class StatefulGizmo(object):
216
217 # initial data (optional, defaults to none)
218 INITIAL_DATA = None
219
220 def __init__(self):
221 self.data = {}
222 if self.INITIAL_DATA:
223 # deep copy of INITIAL_DATA allows lists, sets and
224 # other mutable types to safely be used in INITIAL_DATA
225 self.data.update(copy.deepcopy(self.INITIAL_DATA))
226
227 def set_data(self, key, value):
228 self.data[key] = value
229
230 def get_data(self, key):
231 return self.data.get(key, None)
232
233
234 class Scene(StatefulGizmo):
235 """Base class for scenes."""
236
237 # sub-folder to look for resources in
238 FOLDER = None
239
240 # name of background image resource
241 BACKGROUND = None
242
243 # name of scene (optional, defaults to folder)
244 NAME = None
245
246 # Offset of the background image
247 OFFSET = (0, 0)
248
249 def __init__(self, state):
250 StatefulGizmo.__init__(self)
251 # scene name
252 self.name = self.NAME if self.NAME is not None else self.FOLDER
253 # link back to state object
254 self.state = state
255 # map of thing names -> Thing objects
256 self.things = {}
257 self._background = None
258
259 def add_item(self, item):
260 self.state.add_item(item)
261
262 def add_thing(self, thing):
263 self.things[thing.name] = thing
264 thing.set_scene(self)
265
266 def remove_thing(self, thing):
267 del self.things[thing.name]
268 if thing is self.state.current_thing:
269 self.state.current_thing.leave()
270 self.state.current_thing = None
271
272 def _get_description(self):
273 text = (self.state.current_thing and
274 self.state.current_thing.get_description())
275 if text is None:
276 return None
277 label = BoomLabel(text)
278 label.set_margin(5)
279 label.border_width = 1
280 label.border_color = (0, 0, 0)
281 label.bg_color = Color(210, 210, 210, 255)
282 label.fg_color = (0, 0, 0)
283 return label
284
285 def draw_description(self, surface, screen):
286 description = self._get_description()
287 if description is not None:
288 w, h = description.size
289 sub = screen.get_root().surface.subsurface(
290 Rect(400 - w / 2, 5, w, h))
291 description.draw_all(sub)
292
293 def _cache_background(self):
294 if self.BACKGROUND and not self._background:
295 self._background = get_image(self.FOLDER, self.BACKGROUND)
296
297 def draw_background(self, surface):
298 self._cache_background()
299 if self._background is not None:
300 surface.blit(self._background, self.OFFSET, None)
301 else:
302 surface.fill((200, 200, 200))
303
304 def draw_things(self, surface):
305 for thing in self.things.itervalues():
306 thing.draw(surface)
307
308 def draw(self, surface, screen):
309 self.draw_background(surface)
310 self.draw_things(surface)
311 self.draw_description(surface, screen)
312
313 def interact(self, item, pos):
314 """Interact with a particular position.
315
316 Item may be an item in the list of items or None for the hand.
317
318 Returns a Result object to provide feedback to the player.
319 """
320 if self.state.current_thing is not None:
321 return self.state.current_thing.interact(item)
322
323 def animate(self):
324 """Animate all the things in the scene.
325
326 Return true if any of them need to queue a redraw"""
327 result = False
328 for thing in self.things.itervalues():
329 if thing.animate():
330 result = True
331 return result
332
333 def enter(self):
334 return None
335
336 def leave(self):
337 return None
338
339 def update_current_thing(self, pos):
340 if self.state.current_thing is not None:
341 if not self.state.current_thing.contains(pos):
342 self.state.current_thing.leave()
343 self.state.current_thing = None
344 for thing in self.things.itervalues():
345 if thing.contains(pos):
346 thing.enter(self.state.tool)
347 self.state.current_thing = thing
348 break
349
350 def mouse_move(self, pos):
351 """Call to check whether the cursor has entered / exited a thing.
352
353 Item may be an item in the list of items or None for the hand.
354 """
355 self.update_current_thing(pos)
356
357 def get_detail_size(self):
358 self._cache_background()
359 return self._background.get_size()
360
361
362 class InteractiveMixin(object):
363 def is_interactive(self, tool=None):
364 return True
365
366 def interact(self, tool):
367 if not self.is_interactive(tool):
368 return None
369 if tool is None:
370 return self.interact_without()
371 handler = getattr(self, 'interact_with_' + tool.tool_name, None)
372 inverse_handler = self.get_inverse_interact(tool)
373 if handler is not None:
374 return handler(tool)
375 elif inverse_handler is not None:
376 return inverse_handler(self)
377 else:
378 return self.interact_default(tool)
379
380 def get_inverse_interact(self, tool):
381 return None
382
383 def interact_without(self):
384 return self.interact_default(None)
385
386 def interact_default(self, item=None):
387 return None
388
389
390 class Thing(StatefulGizmo, InteractiveMixin):
391 """Base class for things in a scene that you can interact with."""
392
393 # name of thing
394 NAME = None
395
396 # sub-folder to look for resources in (defaults to scenes folder)
397 FOLDER = None
398
399 # list of Interact objects
400 INTERACTS = {}
401
402 # name first interact
403 INITIAL = None
404
405 # Interact rectangle hi-light color (for debugging)
406 # (set to None to turn off)
407 _interact_hilight_color = Color('red')
408
409 def __init__(self):
410 StatefulGizmo.__init__(self)
411 # name of the thing
412 self.name = self.NAME
413 # folder for resource (None is overridden by scene folder)
414 self.folder = self.FOLDER
415 # interacts
416 self.interacts = self.INTERACTS
417 # these are set by set_scene
418 self.scene = None
419 self.state = None
420 self.current_interact = None
421 self.rect = None
422 self.orig_rect = None
423
424 def _fix_rect(self):
425 """Fix rects to compensate for scene offset"""
426 # Offset logic is to always work with copies, to avoid
427 # flying effects from multiple calls to _fix_rect
428 # See footwork in draw
429 if hasattr(self.rect, 'collidepoint'):
430 self.rect = self.rect.move(self.scene.OFFSET)
431 else:
432 self.rect = [x.move(self.scene.OFFSET) for x in self.rect]
433
434 def set_scene(self, scene):
435 assert self.scene is None
436 self.scene = scene
437 if self.folder is None:
438 self.folder = scene.FOLDER
439 self.state = scene.state
440 for interact in self.interacts.itervalues():
441 interact.set_thing(self)
442 self.set_interact(self.INITIAL)
443
444 def set_interact(self, name):
445 self.current_interact = self.interacts[name]
446 self.rect = self.current_interact.interact_rect
447 if self.scene:
448 self._fix_rect()
449 assert self.rect is not None, name
450
451 def contains(self, pos):
452 if hasattr(self.rect, 'collidepoint'):
453 return self.rect.collidepoint(pos)
454 else:
455 # FIXME: add sanity check
456 for rect in list(self.rect):
457 if rect.collidepoint(pos):
458 return True
459 return False
460
461 def get_description(self):
462 return None
463
464 def enter(self, item):
465 """Called when the cursor enters the Thing."""
466 pass
467
468 def leave(self):
469 """Called when the cursr leaves the Thing."""
470 pass
471
472 def animate(self):
473 return self.current_interact.animate()
474
475 def draw(self, surface):
476 old_rect = self.current_interact.rect
477 if old_rect:
478 self.current_interact.rect = old_rect.move(self.scene.OFFSET)
479 self.current_interact.draw(surface)
480 self.current_interact.rect = old_rect
481 if DEBUG_RECTS and self._interact_hilight_color:
482 if hasattr(self.rect, 'collidepoint'):
483 frame_rect(surface, self._interact_hilight_color,
484 self.rect.inflate(1, 1), 1)
485 else:
486 for rect in self.rect:
487 frame_rect(surface, self._interact_hilight_color,
488 rect.inflate(1, 1), 1)
489
490
491 class Item(InteractiveMixin):
492 """Base class for inventory items."""
493
494 # image for inventory
495 INVENTORY_IMAGE = None
496
497 # name of item
498 NAME = None
499
500 # name for interactions (i.e. def interact_with_<TOOL_NAME>)
501 TOOL_NAME = None
502
503 # set to instance of CursorSprite
504 CURSOR = None
505
506 def __init__(self, name=None):
507 self.state = None
508 self.name = self.NAME
509 if name is not None:
510 self.name = name
511 self.tool_name = name
512 if self.TOOL_NAME is not None:
513 self.tool_name = self.TOOL_NAME
514 self.inventory_image = None
515
516 def _cache_inventory_image(self):
517 if not self.inventory_image:
518 self.inventory_image = get_image('items', self.INVENTORY_IMAGE)
519
520 def set_state(self, state):
521 assert self.state is None
522 self.state = state
523
524 def get_inventory_image(self):
525 self._cache_inventory_image()
526 return self.inventory_image
527
528 def get_inverse_interact(self, tool):
529 return getattr(tool, 'interact_with_' + self.tool_name, None)
530
531 def is_interactive(self, tool=None):
532 if tool:
533 return True
534 return False
535
536
537 class CloneableItem(Item):
538 _counter = 0
539
540 @classmethod
541 def _get_new_id(cls):
542 cls._counter += 1
543 return cls._counter - 1
544
545 def __init__(self, name=None):
546 super(CloneableItem, self).__init__(name)
547 my_count = self._get_new_id()
548 self.name = "%s.%s" % (self.name, my_count)