Stricter PEP-8 conformance.
[python-bulletml.git] / bulletml / parser.py
1 """BulletML parser.
2
3 This is based on the format described at
4 http://www.asahi-net.or.jp/~cs8k-cyu/bulletml/bulletml_ref_e.html.
5
6 Unless you are adding support for new actions, the only class you
7 should care about in here is BulletML.
8 """
9
10 from __future__ import division
11
12 from math import sin, cos, radians, pi as PI
13
14 from xml.etree.ElementTree import ElementTree
15
16 # Python 3 moved this for no really good reason.
17 try:
18 from sys import intern
19 except ImportError:
20 pass
21
22 try:
23 from io import StringIO
24 except ImportError:
25 try:
26 from cStringIO import StringIO
27 except ImportError:
28 from StringIO import StringIO
29
30 from bulletml.errors import Error
31 from bulletml.expr import NumberDef, INumberDef
32
33
34 __all__ = ["ParseError", "BulletML"]
35
36 PI_2 = PI * 2
37
38 class ParseError(Error):
39 """Raised when an error occurs parsing the XML structure."""
40 pass
41
42 def realtag(element):
43 """Strip namespace poop off the front of a tag."""
44 try:
45 return element.tag.rsplit('}', 1)[1]
46 except ValueError:
47 return element.tag
48
49 class ParamList(object):
50 """List of parameter definitions."""
51
52 def __init__(self, params=()):
53 self.params = list(params)
54
55 @classmethod
56 def FromXML(cls, doc, element):
57 """Construct using an ElementTree-style element."""
58 return cls([NumberDef(subelem.text) for subelem in element
59 if realtag(subelem) == "param"])
60
61 def __call__(self, params, rank):
62 return [param(params, rank) for param in self.params]
63
64 def __repr__(self):
65 return "%s(%r)" % (type(self).__name__, self.params)
66
67 class Direction(object):
68 """Raw direction value."""
69
70 VALID_TYPES = ["relative", "absolute", "aim", "sequence"]
71
72 def __init__(self, type, value):
73 if type not in self.VALID_TYPES:
74 raise ValueError("invalid type %r" % type)
75 self.type = intern(type)
76 self.value = value
77
78 def __getstate__(self):
79 return [('type', self.type), ('value', self.value.expr)]
80
81 def __setstate__(self, state):
82 state = dict(state)
83 self.__init__(state["type"], NumberDef(state["value"]))
84
85 @classmethod
86 def FromXML(cls, doc, element, default="absolute"):
87 """Construct using an ElementTree-style element."""
88 return cls(element.get("type", default), NumberDef(element.text))
89
90 def __call__(self, params, rank):
91 return (radians(self.value(params, rank)), self.type)
92
93 def __repr__(self):
94 return "%s(%r, type=%r)" % (
95 type(self).__name__, self.value, self.type)
96
97 class ChangeDirection(object):
98 """Direction change over time."""
99
100 def __init__(self, term, direction):
101 self.term = term
102 self.direction = direction
103
104 def __getstate__(self):
105 return [('frames', self.term.expr),
106 ('type', self.direction.type),
107 ('value', self.direction.value.expr)]
108
109 def __setstate__(self, state):
110 state = dict(state)
111 self.__init__(INumberDef(state["frames"]),
112 Direction(state["type"], NumberDef(state["value"])))
113
114 @classmethod
115 def FromXML(cls, doc, element):
116 """Construct using an ElementTree-style element."""
117 for subelem in element.getchildren():
118 tag = realtag(subelem)
119 if tag == "direction":
120 direction = Direction.FromXML(doc, subelem)
121 elif tag == "term":
122 term = INumberDef(subelem.text)
123 try:
124 return cls(term, direction)
125 except UnboundLocalError as exc:
126 raise ParseError(str(exc))
127
128 def __call__(self, owner, action, params, rank, created):
129 frames = self.term(params, rank)
130 direction, type = self.direction(params, rank)
131 action.direction_frames = frames
132 action.aiming = False
133 if type == "sequence":
134 action.direction = direction
135 else:
136 if type == "absolute":
137 direction -= owner.direction
138 elif type != "relative": # aim or default
139 action.aiming = True
140 direction += owner.aim - owner.direction
141
142 # Normalize to [-pi, pi).
143 direction = (direction + PI) % PI_2 - PI
144 if frames <= 0:
145 owner.direction += direction
146 else:
147 action.direction = direction / frames
148
149 def __repr__(self):
150 return "%s(term=%r, direction=%r)" % (
151 type(self).__name__, self.term, self.direction)
152
153 class Speed(object):
154 """Raw speed value."""
155
156 VALID_TYPES = ["relative", "absolute", "sequence"]
157
158 def __init__(self, type, value):
159 if type not in self.VALID_TYPES:
160 raise ValueError("invalid type %r" % type)
161 self.type = intern(type)
162 self.value = value
163
164 def __getstate__(self):
165 return [('type', self.type), ('value', self.value.expr)]
166
167 def __setstate__(self, state):
168 state = dict(state)
169 self.__init__(state["type"], NumberDef(state["value"]))
170
171 @classmethod
172 def FromXML(cls, doc, element):
173 """Construct using an ElementTree-style element."""
174 return cls(element.get("type", "absolute"), NumberDef(element.text))
175
176 def __call__(self, params, rank):
177 return (self.value(params, rank), self.type)
178
179 def __repr__(self):
180 return "%s(%r, type=%r)" % (type(self).__name__, self.value, self.type)
181
182 class ChangeSpeed(object):
183 """Speed change over time."""
184
185 def __init__(self, term, speed):
186 self.term = term
187 self.speed = speed
188
189 def __getstate__(self):
190 return [('frames', self.term.expr),
191 ('type', self.speed.type),
192 ('value', self.speed.value.expr)]
193
194 def __setstate__(self, state):
195 state = dict(state)
196 self.__init__(INumberDef(state["frames"]),
197 Speed(state["type"], NumberDef(state["value"])))
198
199 @classmethod
200 def FromXML(cls, doc, element):
201 """Construct using an ElementTree-style element."""
202 for subelem in element.getchildren():
203 tag = realtag(subelem)
204 if tag == "speed":
205 speed = Speed.FromXML(doc, subelem)
206 elif tag == "term":
207 term = INumberDef(subelem.text)
208 try:
209 return cls(term, speed)
210 except UnboundLocalError as exc:
211 raise ParseError(str(exc))
212
213 def __call__(self, owner, action, params, rank, created):
214 frames = self.term(params, rank)
215 speed, type = self.speed(params, rank)
216 action.speed_frames = frames
217 if frames <= 0:
218 if type == "absolute":
219 owner.speed = speed
220 elif type == "relative":
221 owner.speed += speed
222 elif type == "sequence":
223 action.speed = speed
224 elif type == "relative":
225 action.speed = speed / frames
226 else:
227 action.speed = (speed - owner.speed) / frames
228
229 def __repr__(self):
230 return "%s(term=%r, speed=%r)" % (
231 type(self).__name__, self.term, self.speed)
232
233 class Wait(object):
234 """Wait for some frames."""
235
236 def __init__(self, frames):
237 self.frames = frames
238
239 def __getstate__(self):
240 return dict(frames=self.frames.expr)
241
242 def __setstate__(self, state):
243 self.__init__(INumberDef(state["frames"]))
244
245 @classmethod
246 def FromXML(cls, doc, element):
247 """Construct using an ElementTree-style element."""
248 return cls(INumberDef(element.text))
249
250 def __call__(self, owner, action, params, rank, created):
251 action.wait_frames = self.frames(params, rank)
252 return True
253
254 def __repr__(self):
255 return "%s(%r)" % (type(self).__name__, self.frames)
256
257 class Tag(object):
258 """Set a bullet tag."""
259
260 def __init__(self, tag):
261 self.tag = tag
262
263 def __getstate__(self):
264 return dict(tag=self.tag)
265
266 def __setstate__(self, state):
267 self.__init__(state["tag"])
268
269 @classmethod
270 def FromXML(cls, doc, element):
271 """Construct using an ElementTree-style element."""
272 return cls(element.text)
273
274 def __call__(self, owner, action, params, rank, created):
275 owner.tags.add(self.tag)
276
277 class Untag(object):
278 """Unset a bullet tag."""
279
280 def __init__(self, tag):
281 self.tag = tag
282
283 def __getstate__(self):
284 return dict(tag=self.tag)
285
286 def __setstate__(self, state):
287 self.__init__(state["tag"])
288
289 @classmethod
290 def FromXML(cls, doc, element):
291 """Construct using an ElementTree-style element."""
292 return cls(element.text)
293
294 def __call__(self, owner, action, params, rank, created):
295 try:
296 owner.tags.remove(self.tag)
297 except KeyError:
298 pass
299
300 class Appearance(object):
301 """Set a bullet appearance."""
302
303 def __init__(self, appearance):
304 self.appearance = appearance
305
306 def __getstate__(self):
307 return dict(appearance=self.appearance)
308
309 def __setstate__(self, state):
310 self.__init__(state["appearance"])
311
312 @classmethod
313 def FromXML(cls, doc, element):
314 """Construct using an ElementTree-style element."""
315 return cls(element.text)
316
317 def __call__(self, owner, action, params, rank, created):
318 owner.apearance = self.appearance
319
320 class Vanish(object):
321 """Make the owner disappear."""
322
323 def __init__(self):
324 pass
325
326 @classmethod
327 def FromXML(cls, doc, element):
328 """Construct using an ElementTree-style element."""
329 return cls()
330
331 def __repr__(self):
332 return "%s()" % (type(self).__name__)
333
334 def __call__(self, owner, action, params, rank, created):
335 owner.vanish()
336 return True
337
338 class Repeat(object):
339 """Repeat an action definition."""
340
341 def __init__(self, times, action):
342 self.times = times
343 self.action = action
344
345 def __getstate__(self):
346 return [('times', self.times.expr), ('action', self.action)]
347
348 def __setstate__(self, state):
349 state = dict(state)
350 self.__init__(INumberDef(state["times"]), state["action"])
351
352 @classmethod
353 def FromXML(cls, doc, element):
354 """Construct using an ElementTree-style element."""
355 for subelem in element.getchildren():
356 tag = realtag(subelem)
357 if tag == "times":
358 times = INumberDef(subelem.text)
359 elif tag == "action":
360 action = ActionDef.FromXML(doc, subelem)
361 elif tag == "actionRef":
362 action = ActionRef.FromXML(doc, subelem)
363 try:
364 return cls(times, action)
365 except UnboundLocalError as exc:
366 raise ParseError(str(exc))
367
368 def __call__(self, owner, action, params, rank, created):
369 repeat = self.times(params, rank)
370 return self.action(owner, action, params, rank, created, repeat)
371
372 def __repr__(self):
373 return "%s(%r, %r)" % (type(self).__name__, self.times, self.action)
374
375 class If(object):
376 """Conditional actions."""
377
378 def __init__(self, cond, then, else_=None):
379 self.cond = cond
380 self.then = then
381 self.else_ = else_
382
383 def __getstate__(self):
384 if self.else_:
385 return [('cond', self.cond.expr),
386 ('then', self.then),
387 ('else', self.else_)]
388 else:
389 return [('cond', self.cond.expr), ('then', self.then)]
390
391 def __setstate__(self, state):
392 state = dict(state)
393 state["else_"] = state.pop("else", None)
394 state["cond"] = INumberDef(state["cond"])
395 self.__init__(**state)
396
397 @classmethod
398 def FromXML(cls, doc, element):
399 """Construct using an ElementTree-style element."""
400 else_ = None
401 for subelem in element.getchildren():
402 tag = realtag(subelem)
403 if tag == "cond":
404 cond = INumberDef(subelem.text)
405 elif tag == "then":
406 then = ActionDef.FromXML(doc, subelem)
407 elif tag == "else":
408 else_ = ActionDef.FromXML(doc, subelem)
409 try:
410 return cls(cond, then, else_)
411 except UnboundLocalError as exc:
412 raise ParseError(str(exc))
413
414 def __call__(self, owner, action, params, rank, created):
415 if self.cond(params, rank):
416 branch = self.then
417 else:
418 branch = self.else_
419
420 if branch:
421 return branch(owner, action, params, rank, created)
422
423 def __repr__(self):
424 if self.else_:
425 return "%s(%r, then=%r, else_=%r)" % (
426 type(self).__name__, self.cond, self.then, self.else_)
427 else:
428 return "%s(%r, then=%r)" % (
429 type(self).__name__, self.cond, self.then)
430
431 class Accel(object):
432 """Accelerate over some time."""
433
434 horizontal = None
435 vertical = None
436
437 def __init__(self, term, horizontal=None, vertical=None):
438 self.term = term
439 self.horizontal = horizontal
440 self.vertical = vertical
441
442 def __getstate__(self):
443 state = [('frames', self.term.expr)]
444 if self.horizontal:
445 state.append(('horizontal', self.horizontal))
446 if self.vertical:
447 state.append(('vertical', self.vertical))
448 return state
449
450 def __setstate__(self, state):
451 state = dict(state)
452 self.__init__(INumberDef(state["frames"]), state.get("horizontal"),
453 state.get("vertical"))
454
455 @classmethod
456 def FromXML(cls, doc, element):
457 """Construct using an ElementTree-style element."""
458 horizontal = None
459 vertical = None
460
461 for subelem in element.getchildren():
462 tag = realtag(subelem)
463 if tag == "term":
464 term = INumberDef(subelem.text)
465 elif tag == "horizontal":
466 horizontal = Speed.FromXML(doc, subelem)
467 elif tag == "vertical":
468 vertical = Speed.FromXML(doc, subelem)
469
470 try:
471 return cls(term, horizontal, vertical)
472 except AttributeError:
473 raise ParseError
474
475 def __call__(self, owner, action, params, rank, created):
476 frames = self.term(params, rank)
477 horizontal = self.horizontal and self.horizontal(params, rank)
478 vertical = self.vertical and self.vertical(params, rank)
479 action.accel_frames = frames
480 if horizontal:
481 mx, type = horizontal
482 if frames <= 0:
483 if type == "absolute":
484 owner.mx = mx
485 elif type == "relative":
486 owner.mx += mx
487 elif type == "sequence":
488 action.mx = mx
489 elif type == "absolute":
490 action.mx = (mx - owner.mx) / frames
491 elif type == "relative":
492 action.mx = mx / frames
493 if vertical:
494 my, type = vertical
495 if frames <= 0:
496 if type == "absolute":
497 owner.my = my
498 elif type == "relative":
499 owner.my += my
500 elif type == "sequence":
501 action.my = my
502 elif type == "absolute":
503 action.my = (my - owner.my) / frames
504 elif type == "relative":
505 action.my = my / frames
506
507 def __repr__(self):
508 return "%s(%r, horizontal=%r, vertical=%r)" % (
509 type(self).__name__, self.term, self.horizontal, self.vertical)
510
511 class BulletDef(object):
512 """Bullet definition."""
513
514 def __init__(self, actions=(), direction=None, speed=None, tags=(),
515 appearance=None):
516 self.direction = direction
517 self.speed = speed
518 self.actions = list(actions)
519 self.tags = set(tags)
520 self.appearance = appearance
521
522 def __getstate__(self):
523 state = []
524 if self.direction:
525 state.append(("direction", self.direction))
526 if self.speed:
527 state.append(("speed", self.speed))
528 if self.actions:
529 state.append(("actions", self.actions))
530 if self.tags:
531 state.append(("tags", list(self.tags)))
532 if self.appearance:
533 state.append(("appearance", self.appearance))
534 return state
535
536 def __setstate__(self, state):
537 state = dict(state)
538 self.__init__(**state)
539
540 @classmethod
541 def FromXML(cls, doc, element):
542 """Construct using an ElementTree-style element."""
543 actions = []
544 speed = None
545 direction = None
546 tags = set()
547 for subelem in element.getchildren():
548 tag = realtag(subelem)
549 if tag == "direction":
550 direction = Direction.FromXML(doc, subelem)
551 elif tag == "speed":
552 speed = Speed.FromXML(doc, subelem)
553 elif tag == "action":
554 actions.append(ActionDef.FromXML(doc, subelem))
555 elif tag == "actionRef":
556 actions.append(ActionRef.FromXML(doc, subelem))
557 elif tag == "tag":
558 tags.add(subelem.text)
559 dfn = cls(actions, direction, speed, tags)
560 doc._bullets[element.get("label")] = dfn
561 return dfn
562
563 def __call__(self, owner, action, params, rank, created):
564 actions = [a(None, action, params, rank, created)
565 for a in self.actions]
566 return (
567 self.direction and self.direction(params, rank),
568 self.speed and self.speed(params, rank),
569 self.tags,
570 self.appearance,
571 actions)
572
573 def __repr__(self):
574 return "%s(direction=%r, speed=%r, actions=%r)" % (
575 type(self).__name__, self.direction, self.speed, self.actions)
576
577 class BulletRef(object):
578 """Create a bullet by name with parameters."""
579
580 def __init__(self, bullet, params=None):
581 self.bullet = bullet
582 self.params = ParamList() if params is None else params
583
584 def __getstate__(self):
585 state = []
586 if self.params.params:
587 params = [param.expr for param in self.params.params]
588 state.append(("params", params))
589 state.append(('bullet', self.bullet))
590 return state
591
592 def __setstate__(self, state):
593 state = dict(state)
594 bullet = state["bullet"]
595 params = [NumberDef(param) for param in state.get("params", [])]
596 self.__init__(bullet, ParamList(params))
597
598 @classmethod
599 def FromXML(cls, doc, element):
600 """Construct using an ElementTree-style element."""
601 bullet = cls(element.get("label"), ParamList.FromXML(doc, element))
602 doc._bullet_refs.append(bullet)
603 return bullet
604
605 def __call__(self, owner, action, params, rank, created):
606 params = self.params(params, rank)
607 return self.bullet(owner, action, params, rank, created)
608
609 def __repr__(self):
610 return "%s(params=%r, bullet=%r)" % (
611 type(self).__name__, self.params, self.bullet)
612
613 class ActionDef(object):
614 """Action definition.
615
616 To support parsing new actions, add tags to
617 ActionDef.CONSTRUCTORS. It maps tag names to classes with a
618 FromXML classmethod, which take the BulletML instance and
619 ElementTree element as arguments.
620 """
621
622 # This is self-referential, so it's filled in later.
623 CONSTRUCTORS = dict()
624
625 def __init__(self, actions):
626 self.actions = list(actions)
627
628 def __getstate__(self):
629 return dict(actions=self.actions)
630
631 def __setstate__(self, state):
632 state = dict(state)
633 self.__init__(state["actions"])
634
635 @classmethod
636 def FromXML(cls, doc, element):
637 """Construct using an ElementTree-style element."""
638 actions = []
639 for subelem in element.getchildren():
640 tag = realtag(subelem)
641 try:
642 ctr = cls.CONSTRUCTORS[tag]
643 except KeyError:
644 continue
645 else:
646 actions.append(ctr.FromXML(doc, subelem))
647 dfn = cls(actions)
648 doc._actions[element.get("label")] = dfn
649 return dfn
650
651 def __call__(self, owner, action, params, rank, created=(), repeat=1):
652 Action = action if isinstance(action, type) else type(action)
653 parent = None if owner is None else action
654 child = Action(parent, self.actions, params, rank, repeat)
655 if owner is not None:
656 owner.replace(parent, child)
657 child.step(owner, created)
658 return child
659
660 def __repr__(self):
661 return "%s(%r)" % (type(self).__name__, self.actions)
662
663 class ActionRef(object):
664 """Run an action by name with parameters."""
665
666 def __init__(self, action, params=None):
667 self.action = action
668 self.params = params or ParamList()
669
670 def __getstate__(self):
671 state = []
672 if self.params.params:
673 params = [param.expr for param in self.params.params]
674 state.append(("params", params))
675 state.append(('action', self.action))
676 return state
677
678 def __setstate__(self, state):
679 state = dict(state)
680 action = state["action"]
681 params = [NumberDef(param) for param in state.get("params", [])]
682 self.__init__(action, ParamList(params))
683
684 @classmethod
685 def FromXML(cls, doc, element):
686 """Construct using an ElementTree-style element."""
687 action = cls(element.get("label"), ParamList.FromXML(doc, element))
688 doc._action_refs.append(action)
689 return action
690
691 def __call__(self, owner, action, params, rank, created=(), repeat=1):
692 params = self.params(params, rank)
693 return self.action(owner, action, params, rank, created, repeat)
694
695 def __repr__(self):
696 return "%s(params=%r, action=%r)" % (
697 type(self).__name__, self.params, self.action)
698
699 class Offset(object):
700 """Provide an offset to a bullet's initial position."""
701
702 VALID_TYPES = ["relative", "absolute"]
703
704 def __init__(self, type, x, y):
705 if type not in self.VALID_TYPES:
706 raise ValueError("invalid type %r" % type)
707 self.type = intern(type)
708 self.x = x
709 self.y = y
710
711 def __getstate__(self):
712 state = [('type', self.type)]
713 if self.x:
714 state.append(('x', self.x.expr))
715 if self.y:
716 state.append(('y', self.y.expr))
717 return state
718
719 def __setstate__(self, state):
720 state = dict(state)
721 x = NumberDef(state["x"]) if "x" in state else None
722 y = NumberDef(state["y"]) if "y" in state else None
723 self.__init__(state["type"], x, y)
724
725 @classmethod
726 def FromXML(cls, doc, element):
727 """Construct using an ElementTree-style element."""
728 type = element.get("type", "relative")
729 x = None
730 y = None
731 for subelem in element:
732 tag = realtag(subelem)
733 if tag == "x":
734 x = NumberDef(subelem.text)
735 elif tag == "y":
736 y = NumberDef(subelem.text)
737 return cls(type, x, y)
738
739 def __call__(self, params, rank):
740 return (self.x(params, rank) if self.x else 0,
741 self.y(params, rank) if self.y else 0)
742
743 class FireDef(object):
744 """Fire definition (creates a bullet)."""
745
746 def __init__(self, bullet, direction=None, speed=None, offset=None,
747 tags=(), appearance=None):
748 self.bullet = bullet
749 self.direction = direction
750 self.speed = speed
751 self.offset = offset
752 self.tags = set(tags)
753 self.appearance = appearance
754
755 def __getstate__(self):
756 state = []
757 if self.direction:
758 state.append(("direction", self.direction))
759 if self.speed:
760 state.append(("speed", self.speed))
761 if self.offset:
762 state.append(("offset", self.offset))
763 if self.tags:
764 state.append(("tags", list(self.tags)))
765 if self.appearance:
766 state.append(("appearance", self.appearance))
767 try:
768 params = self.bullet.params
769 except AttributeError:
770 state.append(('bullet', self.bullet))
771 else:
772 if params.params:
773 state.append(('bullet', self.bullet))
774 else:
775 # Strip out empty BulletRefs.
776 state.append(('bullet', self.bullet.bullet))
777 return state
778
779 def __setstate__(self, state):
780 state = dict(state)
781 self.__init__(**state)
782
783 @classmethod
784 def FromXML(cls, doc, element):
785 """Construct using an ElementTree-style element."""
786 direction = None
787 speed = None
788 offset = None
789 tags = set()
790 appearance = None
791
792 for subelem in element.getchildren():
793 tag = realtag(subelem)
794 if tag == "direction":
795 direction = Direction.FromXML(doc, subelem, "aim")
796 elif tag == "speed":
797 speed = Speed.FromXML(doc, subelem)
798 elif tag == "bullet":
799 bullet = BulletDef.FromXML(doc, subelem)
800 elif tag == "bulletRef":
801 bullet = BulletRef.FromXML(doc, subelem)
802 elif tag == "offset":
803 offset = Offset.FromXML(doc, subelem)
804 elif tag == "tag":
805 tags.add(subelem.text)
806 elif tag == "appearance":
807 appearance = subelem.text
808 try:
809 fire = cls(bullet, direction, speed, offset, tags, appearance)
810 except UnboundLocalError as exc:
811 raise ParseError(str(exc))
812 else:
813 doc._fires[element.get("label")] = fire
814 return fire
815
816 def __call__(self, owner, action, params, rank, created):
817 direction, speed, tags, appearance, actions = self.bullet(
818 owner, action, params, rank, created)
819 if self.direction is not None:
820 direction = self.direction(params, rank)
821 if self.speed is not None:
822 speed = self.speed(params, rank)
823 tags = tags.union(self.tags)
824 if self.appearance is not None:
825 appearance = self.appearance
826
827 if direction is not None:
828 direction, type = direction
829 if type == "aim" or type is None:
830 direction += owner.aim
831 elif type == "sequence":
832 direction += action.previous_fire_direction
833 elif type == "relative":
834 direction += owner.direction
835 else:
836 direction = owner.aim
837 action.previous_fire_direction = direction
838
839 if speed is not None:
840 speed, type = speed
841 if type == "sequence":
842 speed += action.previous_fire_speed
843 elif type == "relative":
844 # The reference Noiz implementation uses
845 # prvFireSpeed here, but the standard is
846 # pretty clear -- "In case of the type is
847 # "relative", ... the speed is relative to the
848 # speed of this bullet."
849 speed += owner.speed
850 else:
851 speed = 1
852 action.previous_fire_speed = speed
853
854 x = owner.x
855 y = owner.y
856 if self.offset is not None:
857 off_x, off_y = self.offset(params, rank)
858 if self.offset.type == "relative":
859 s = sin(direction)
860 c = cos(direction)
861 x += c * off_x + s * off_y
862 y += s * off_x - c * off_y
863 else:
864 x += off_x
865 y += off_y
866
867 if appearance is None:
868 appearance = owner.appearance
869 bullet = owner.__class__(
870 x=x, y=y, direction=direction, speed=speed,
871 target=owner.target, actions=actions, rank=rank,
872 appearance=appearance, tags=tags)
873 created.append(bullet)
874
875 def __repr__(self):
876 return "%s(direction=%r, speed=%r, bullet=%r)" % (
877 type(self).__name__, self.direction, self.speed, self.bullet)
878
879 class FireRef(object):
880 """Fire a bullet by name with parameters."""
881
882 def __init__(self, fire, params=None):
883 self.fire = fire
884 self.params = params or ParamList()
885
886 def __getstate__(self):
887 state = []
888 if self.params.params:
889 params = [param.expr for param in self.params.params]
890 state.append(("params", params))
891 state.append(('fire', self.fire))
892 return state
893
894 def __setstate__(self, state):
895 state = dict(state)
896 fire = state["fire"]
897 params = [NumberDef(param) for param in state.get("params", [])]
898 self.__init__(fire, ParamList(params))
899
900 @classmethod
901 def FromXML(cls, doc, element):
902 """Construct using an ElementTree-style element."""
903 fired = cls(element.get("label"), ParamList.FromXML(doc, element))
904 doc._fire_refs.append(fired)
905 return fired
906
907 def __call__(self, owner, action, params, rank, created):
908 params = self.params(params, rank)
909 return self.fire(owner, action, params, rank, created)
910
911 def __repr__(self):
912 return "%s(params=%r, fire=%r)" % (
913 type(self).__name__, self.params, self.fire)
914
915 class BulletML(object):
916 """BulletML document.
917
918 A BulletML document is a collection of top-level actions and the
919 base game type.
920
921 You can add tags to the BulletML.CONSTRUCTORS dictionary to extend
922 its parsing. It maps tag names to classes with a FromXML
923 classmethod, which take the BulletML instance and ElementTree
924 element as arguments.
925
926 """
927
928 CONSTRUCTORS = dict(
929 bullet=BulletDef,
930 action=ActionDef,
931 fire=FireDef,
932 )
933
934 def __init__(self, type="none", actions=None):
935 self.type = intern(type)
936 self.actions = [] if actions is None else actions
937
938 def __getstate__(self):
939 return [('type', self.type), ('actions', self.actions)]
940
941 def __setstate__(self, state):
942 state = dict(state)
943 self.__init__(state["type"], actions=state.get("actions"))
944
945 @classmethod
946 def FromXML(cls, source):
947 """Return a BulletML instance based on XML."""
948 if not hasattr(source, 'read'):
949 source = StringIO(source)
950
951 tree = ElementTree()
952 root = tree.parse(source)
953
954 doc = cls(type=root.get("type", "none"))
955
956 doc._bullets = {}
957 doc._actions = {}
958 doc._fires = {}
959 doc._bullet_refs = []
960 doc._action_refs = []
961 doc._fire_refs = []
962
963 for element in root.getchildren():
964 tag = realtag(element)
965 if tag in doc.CONSTRUCTORS:
966 doc.CONSTRUCTORS[tag].FromXML(doc, element)
967
968 try:
969 for ref in doc._bullet_refs:
970 ref.bullet = doc._bullets[ref.bullet]
971 for ref in doc._fire_refs:
972 ref.fire = doc._fires[ref.fire]
973 for ref in doc._action_refs:
974 ref.action = doc._actions[ref.action]
975 except KeyError as exc:
976 raise ParseError("unknown reference %s" % exc)
977
978 doc.actions = [act for name, act in doc._actions.items()
979 if name and name.startswith("top")]
980
981 del(doc._bullet_refs)
982 del(doc._action_refs)
983 del(doc._fire_refs)
984 del(doc._bullets)
985 del(doc._actions)
986 del(doc._fires)
987
988 return doc
989
990 @classmethod
991 def FromYAML(cls, source):
992 """Create a BulletML instance based on YAML."""
993
994 # Late import to avoid a circular dependency.
995 try:
996 import bulletml.bulletyaml
997 import yaml
998 except ImportError:
999 raise ParseError("PyYAML is not available")
1000 else:
1001 try:
1002 return yaml.load(source)
1003 except Exception as exc:
1004 raise ParseError(str(exc))
1005
1006 @classmethod
1007 def FromDocument(cls, source):
1008 """Create a BulletML instance based on a seekable file or string.
1009
1010 This attempts to autodetect if the stream is XML or YAML.
1011 """
1012 if not hasattr(source, 'read'):
1013 source = StringIO(source)
1014 start = source.read(1)
1015 source.seek(0)
1016 if start == "<":
1017 return cls.FromXML(source)
1018 elif start == "!" or start == "#":
1019 return cls.FromYAML(source)
1020 else:
1021 raise ParseError("unknown initial character %r" % start)
1022
1023 def __repr__(self):
1024 return "%s(type=%r, actions=%r)" % (
1025 type(self).__name__, self.type, self.actions)
1026
1027 ActionDef.CONSTRUCTORS = dict(
1028 repeat=Repeat,
1029 fire=FireDef,
1030 fireRef=FireRef,
1031 changeSpeed=ChangeSpeed,
1032 changeDirection=ChangeDirection,
1033 accel=Accel,
1034 wait=Wait,
1035 vanish=Vanish,
1036 tag=Tag,
1037 appearance=Appearance,
1038 untag=Untag,
1039 action=ActionDef,
1040 actionRef=ActionRef)
1041 ActionDef.CONSTRUCTORS["if"] = If