Mercurial > pyntnclick
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) |