bastd.actor.flag

Implements a flag used for marking bases, capture-the-flag games, etc.

  1# Released under the MIT License. See LICENSE for details.
  2#
  3"""Implements a flag used for marking bases, capture-the-flag games, etc."""
  4
  5from __future__ import annotations
  6
  7from dataclasses import dataclass
  8from typing import TYPE_CHECKING
  9
 10import ba
 11from bastd.gameutils import SharedObjects
 12
 13if TYPE_CHECKING:
 14    from typing import Any, Sequence
 15
 16
 17class FlagFactory:
 18    """Wraps up media and other resources used by `Flag`s.
 19
 20    Category: **Gameplay Classes**
 21
 22    A single instance of this is shared between all flags
 23    and can be retrieved via FlagFactory.get().
 24    """
 25
 26    flagmaterial: ba.Material
 27    """The ba.Material applied to all `Flag`s."""
 28
 29    impact_sound: ba.Sound
 30    """The ba.Sound used when a `Flag` hits the ground."""
 31
 32    skid_sound: ba.Sound
 33    """The ba.Sound used when a `Flag` skids along the ground."""
 34
 35    no_hit_material: ba.Material
 36    """A ba.Material that prevents contact with most objects;
 37       applied to 'non-touchable' flags."""
 38
 39    flag_texture: ba.Texture
 40    """The ba.Texture for flags."""
 41
 42    _STORENAME = ba.storagename()
 43
 44    def __init__(self) -> None:
 45        """Instantiate a `FlagFactory`.
 46
 47        You shouldn't need to do this; call FlagFactory.get() to
 48        get a shared instance.
 49        """
 50        shared = SharedObjects.get()
 51        self.flagmaterial = ba.Material()
 52        self.flagmaterial.add_actions(
 53            conditions=(
 54                ('we_are_younger_than', 100),
 55                'and',
 56                ('they_have_material', shared.object_material),
 57            ),
 58            actions=('modify_node_collision', 'collide', False),
 59        )
 60
 61        self.flagmaterial.add_actions(
 62            conditions=(
 63                'they_have_material',
 64                shared.footing_material,
 65            ),
 66            actions=(
 67                ('message', 'our_node', 'at_connect', 'footing', 1),
 68                ('message', 'our_node', 'at_disconnect', 'footing', -1),
 69            ),
 70        )
 71
 72        self.impact_sound = ba.getsound('metalHit')
 73        self.skid_sound = ba.getsound('metalSkid')
 74        self.flagmaterial.add_actions(
 75            conditions=(
 76                'they_have_material',
 77                shared.footing_material,
 78            ),
 79            actions=(
 80                ('impact_sound', self.impact_sound, 2, 5),
 81                ('skid_sound', self.skid_sound, 2, 5),
 82            ),
 83        )
 84
 85        self.no_hit_material = ba.Material()
 86        self.no_hit_material.add_actions(
 87            conditions=(
 88                ('they_have_material', shared.pickup_material),
 89                'or',
 90                ('they_have_material', shared.attack_material),
 91            ),
 92            actions=('modify_part_collision', 'collide', False),
 93        )
 94
 95        # We also don't want anything moving it.
 96        self.no_hit_material.add_actions(
 97            conditions=(
 98                ('they_have_material', shared.object_material),
 99                'or',
100                ('they_dont_have_material', shared.footing_material),
101            ),
102            actions=(('modify_part_collision', 'collide', False),
103                     ('modify_part_collision', 'physical', False)),
104        )
105
106        self.flag_texture = ba.gettexture('flagColor')
107
108    @classmethod
109    def get(cls) -> FlagFactory:
110        """Get/create a shared `FlagFactory` instance."""
111        activity = ba.getactivity()
112        factory = activity.customdata.get(cls._STORENAME)
113        if factory is None:
114            factory = FlagFactory()
115            activity.customdata[cls._STORENAME] = factory
116        assert isinstance(factory, FlagFactory)
117        return factory
118
119
120@dataclass
121class FlagPickedUpMessage:
122    """A message saying a `Flag` has been picked up.
123
124    Category: **Message Classes**
125    """
126
127    flag: Flag
128    """The `Flag` that has been picked up."""
129
130    node: ba.Node
131    """The ba.Node doing the picking up."""
132
133
134@dataclass
135class FlagDiedMessage:
136    """A message saying a `Flag` has died.
137
138    Category: **Message Classes**
139    """
140
141    flag: Flag
142    """The `Flag` that died."""
143
144
145@dataclass
146class FlagDroppedMessage:
147    """A message saying a `Flag` has been dropped.
148
149    Category: **Message Classes**
150    """
151
152    flag: Flag
153    """The `Flag` that was dropped."""
154
155    node: ba.Node
156    """The ba.Node that was holding it."""
157
158
159class Flag(ba.Actor):
160    """A flag; used in games such as capture-the-flag or king-of-the-hill.
161
162    Category: **Gameplay Classes**
163
164    Can be stationary or carry-able by players.
165    """
166
167    def __init__(self,
168                 position: Sequence[float] = (0.0, 1.0, 0.0),
169                 color: Sequence[float] = (1.0, 1.0, 1.0),
170                 materials: Sequence[ba.Material] | None = None,
171                 touchable: bool = True,
172                 dropped_timeout: int | None = None):
173        """Instantiate a flag.
174
175        If 'touchable' is False, the flag will only touch terrain;
176        useful for things like king-of-the-hill where players should
177        not be moving the flag around.
178
179        'materials can be a list of extra `ba.Material`s to apply to the flag.
180
181        If 'dropped_timeout' is provided (in seconds), the flag will die
182        after remaining untouched for that long once it has been moved
183        from its initial position.
184        """
185
186        super().__init__()
187
188        self._initial_position: Sequence[float] | None = None
189        self._has_moved = False
190        shared = SharedObjects.get()
191        factory = FlagFactory.get()
192
193        if materials is None:
194            materials = []
195        elif not isinstance(materials, list):
196            # In case they passed a tuple or whatnot.
197            materials = list(materials)
198        if not touchable:
199            materials = [factory.no_hit_material] + materials
200
201        finalmaterials = ([shared.object_material, factory.flagmaterial] +
202                          materials)
203        self.node = ba.newnode('flag',
204                               attrs={
205                                   'position':
206                                       (position[0], position[1] + 0.75,
207                                        position[2]),
208                                   'color_texture': factory.flag_texture,
209                                   'color': color,
210                                   'materials': finalmaterials
211                               },
212                               delegate=self)
213
214        if dropped_timeout is not None:
215            dropped_timeout = int(dropped_timeout)
216        self._dropped_timeout = dropped_timeout
217        self._counter: ba.Node | None
218        if self._dropped_timeout is not None:
219            self._count = self._dropped_timeout
220            self._tick_timer = ba.Timer(1.0,
221                                        call=ba.WeakCall(self._tick),
222                                        repeat=True)
223            self._counter = ba.newnode('text',
224                                       owner=self.node,
225                                       attrs={
226                                           'in_world': True,
227                                           'color': (1, 1, 1, 0.7),
228                                           'scale': 0.015,
229                                           'shadow': 0.5,
230                                           'flatness': 1.0,
231                                           'h_align': 'center'
232                                       })
233        else:
234            self._counter = None
235
236        self._held_count = 0
237        self._score_text: ba.Node | None = None
238        self._score_text_hide_timer: ba.Timer | None = None
239
240    def _tick(self) -> None:
241        if self.node:
242
243            # Grab our initial position after one tick (in case we fall).
244            if self._initial_position is None:
245                self._initial_position = self.node.position
246
247                # Keep track of when we first move; we don't count down
248                # until then.
249            if not self._has_moved:
250                nodepos = self.node.position
251                if (max(
252                        abs(nodepos[i] - self._initial_position[i])
253                        for i in list(range(3))) > 1.0):
254                    self._has_moved = True
255
256            if self._held_count > 0 or not self._has_moved:
257                assert self._dropped_timeout is not None
258                assert self._counter
259                self._count = self._dropped_timeout
260                self._counter.text = ''
261            else:
262                self._count -= 1
263                if self._count <= 10:
264                    nodepos = self.node.position
265                    assert self._counter
266                    self._counter.position = (nodepos[0], nodepos[1] + 1.3,
267                                              nodepos[2])
268                    self._counter.text = str(self._count)
269                    if self._count < 1:
270                        self.handlemessage(ba.DieMessage())
271                else:
272                    assert self._counter
273                    self._counter.text = ''
274
275    def _hide_score_text(self) -> None:
276        assert self._score_text is not None
277        assert isinstance(self._score_text.scale, float)
278        ba.animate(self._score_text, 'scale', {
279            0: self._score_text.scale,
280            0.2: 0
281        })
282
283    def set_score_text(self, text: str) -> None:
284        """Show a message over the flag; handy for scores."""
285        if not self.node:
286            return
287        if not self._score_text:
288            start_scale = 0.0
289            math = ba.newnode('math',
290                              owner=self.node,
291                              attrs={
292                                  'input1': (0, 1.4, 0),
293                                  'operation': 'add'
294                              })
295            self.node.connectattr('position', math, 'input2')
296            self._score_text = ba.newnode('text',
297                                          owner=self.node,
298                                          attrs={
299                                              'text': text,
300                                              'in_world': True,
301                                              'scale': 0.02,
302                                              'shadow': 0.5,
303                                              'flatness': 1.0,
304                                              'h_align': 'center'
305                                          })
306            math.connectattr('output', self._score_text, 'position')
307        else:
308            assert isinstance(self._score_text.scale, float)
309            start_scale = self._score_text.scale
310            self._score_text.text = text
311        self._score_text.color = ba.safecolor(self.node.color)
312        ba.animate(self._score_text, 'scale', {0: start_scale, 0.2: 0.02})
313        self._score_text_hide_timer = ba.Timer(
314            1.0, ba.WeakCall(self._hide_score_text))
315
316    def handlemessage(self, msg: Any) -> Any:
317        assert not self.expired
318        if isinstance(msg, ba.DieMessage):
319            if self.node:
320                self.node.delete()
321                if not msg.immediate:
322                    self.activity.handlemessage(FlagDiedMessage(self))
323        elif isinstance(msg, ba.HitMessage):
324            assert self.node
325            assert msg.force_direction is not None
326            self.node.handlemessage(
327                'impulse', msg.pos[0], msg.pos[1], msg.pos[2], msg.velocity[0],
328                msg.velocity[1], msg.velocity[2], msg.magnitude,
329                msg.velocity_magnitude, msg.radius, 0, msg.force_direction[0],
330                msg.force_direction[1], msg.force_direction[2])
331        elif isinstance(msg, ba.PickedUpMessage):
332            self._held_count += 1
333            if self._held_count == 1 and self._counter is not None:
334                self._counter.text = ''
335            self.activity.handlemessage(FlagPickedUpMessage(self, msg.node))
336        elif isinstance(msg, ba.DroppedMessage):
337            self._held_count -= 1
338            if self._held_count < 0:
339                print('Flag held count < 0.')
340                self._held_count = 0
341            self.activity.handlemessage(FlagDroppedMessage(self, msg.node))
342        else:
343            super().handlemessage(msg)
344
345    @staticmethod
346    def project_stand(pos: Sequence[float]) -> None:
347        """Project a flag-stand onto the ground at the given position.
348
349        Useful for games such as capture-the-flag to show where a
350        movable flag originated from.
351        """
352        assert len(pos) == 3
353        ba.emitfx(position=pos, emit_type='flag_stand')
class FlagFactory:
 18class FlagFactory:
 19    """Wraps up media and other resources used by `Flag`s.
 20
 21    Category: **Gameplay Classes**
 22
 23    A single instance of this is shared between all flags
 24    and can be retrieved via FlagFactory.get().
 25    """
 26
 27    flagmaterial: ba.Material
 28    """The ba.Material applied to all `Flag`s."""
 29
 30    impact_sound: ba.Sound
 31    """The ba.Sound used when a `Flag` hits the ground."""
 32
 33    skid_sound: ba.Sound
 34    """The ba.Sound used when a `Flag` skids along the ground."""
 35
 36    no_hit_material: ba.Material
 37    """A ba.Material that prevents contact with most objects;
 38       applied to 'non-touchable' flags."""
 39
 40    flag_texture: ba.Texture
 41    """The ba.Texture for flags."""
 42
 43    _STORENAME = ba.storagename()
 44
 45    def __init__(self) -> None:
 46        """Instantiate a `FlagFactory`.
 47
 48        You shouldn't need to do this; call FlagFactory.get() to
 49        get a shared instance.
 50        """
 51        shared = SharedObjects.get()
 52        self.flagmaterial = ba.Material()
 53        self.flagmaterial.add_actions(
 54            conditions=(
 55                ('we_are_younger_than', 100),
 56                'and',
 57                ('they_have_material', shared.object_material),
 58            ),
 59            actions=('modify_node_collision', 'collide', False),
 60        )
 61
 62        self.flagmaterial.add_actions(
 63            conditions=(
 64                'they_have_material',
 65                shared.footing_material,
 66            ),
 67            actions=(
 68                ('message', 'our_node', 'at_connect', 'footing', 1),
 69                ('message', 'our_node', 'at_disconnect', 'footing', -1),
 70            ),
 71        )
 72
 73        self.impact_sound = ba.getsound('metalHit')
 74        self.skid_sound = ba.getsound('metalSkid')
 75        self.flagmaterial.add_actions(
 76            conditions=(
 77                'they_have_material',
 78                shared.footing_material,
 79            ),
 80            actions=(
 81                ('impact_sound', self.impact_sound, 2, 5),
 82                ('skid_sound', self.skid_sound, 2, 5),
 83            ),
 84        )
 85
 86        self.no_hit_material = ba.Material()
 87        self.no_hit_material.add_actions(
 88            conditions=(
 89                ('they_have_material', shared.pickup_material),
 90                'or',
 91                ('they_have_material', shared.attack_material),
 92            ),
 93            actions=('modify_part_collision', 'collide', False),
 94        )
 95
 96        # We also don't want anything moving it.
 97        self.no_hit_material.add_actions(
 98            conditions=(
 99                ('they_have_material', shared.object_material),
100                'or',
101                ('they_dont_have_material', shared.footing_material),
102            ),
103            actions=(('modify_part_collision', 'collide', False),
104                     ('modify_part_collision', 'physical', False)),
105        )
106
107        self.flag_texture = ba.gettexture('flagColor')
108
109    @classmethod
110    def get(cls) -> FlagFactory:
111        """Get/create a shared `FlagFactory` instance."""
112        activity = ba.getactivity()
113        factory = activity.customdata.get(cls._STORENAME)
114        if factory is None:
115            factory = FlagFactory()
116            activity.customdata[cls._STORENAME] = factory
117        assert isinstance(factory, FlagFactory)
118        return factory

Wraps up media and other resources used by Flags.

Category: Gameplay Classes

A single instance of this is shared between all flags and can be retrieved via FlagFactory.get().

FlagFactory()
 45    def __init__(self) -> None:
 46        """Instantiate a `FlagFactory`.
 47
 48        You shouldn't need to do this; call FlagFactory.get() to
 49        get a shared instance.
 50        """
 51        shared = SharedObjects.get()
 52        self.flagmaterial = ba.Material()
 53        self.flagmaterial.add_actions(
 54            conditions=(
 55                ('we_are_younger_than', 100),
 56                'and',
 57                ('they_have_material', shared.object_material),
 58            ),
 59            actions=('modify_node_collision', 'collide', False),
 60        )
 61
 62        self.flagmaterial.add_actions(
 63            conditions=(
 64                'they_have_material',
 65                shared.footing_material,
 66            ),
 67            actions=(
 68                ('message', 'our_node', 'at_connect', 'footing', 1),
 69                ('message', 'our_node', 'at_disconnect', 'footing', -1),
 70            ),
 71        )
 72
 73        self.impact_sound = ba.getsound('metalHit')
 74        self.skid_sound = ba.getsound('metalSkid')
 75        self.flagmaterial.add_actions(
 76            conditions=(
 77                'they_have_material',
 78                shared.footing_material,
 79            ),
 80            actions=(
 81                ('impact_sound', self.impact_sound, 2, 5),
 82                ('skid_sound', self.skid_sound, 2, 5),
 83            ),
 84        )
 85
 86        self.no_hit_material = ba.Material()
 87        self.no_hit_material.add_actions(
 88            conditions=(
 89                ('they_have_material', shared.pickup_material),
 90                'or',
 91                ('they_have_material', shared.attack_material),
 92            ),
 93            actions=('modify_part_collision', 'collide', False),
 94        )
 95
 96        # We also don't want anything moving it.
 97        self.no_hit_material.add_actions(
 98            conditions=(
 99                ('they_have_material', shared.object_material),
100                'or',
101                ('they_dont_have_material', shared.footing_material),
102            ),
103            actions=(('modify_part_collision', 'collide', False),
104                     ('modify_part_collision', 'physical', False)),
105        )
106
107        self.flag_texture = ba.gettexture('flagColor')

Instantiate a FlagFactory.

You shouldn't need to do this; call FlagFactory.get() to get a shared instance.

flagmaterial: _ba.Material

The ba.Material applied to all Flags.

impact_sound: _ba.Sound

The ba.Sound used when a Flag hits the ground.

skid_sound: _ba.Sound

The ba.Sound used when a Flag skids along the ground.

no_hit_material: _ba.Material

A ba.Material that prevents contact with most objects; applied to 'non-touchable' flags.

flag_texture: _ba.Texture

The ba.Texture for flags.

@classmethod
def get(cls) -> bastd.actor.flag.FlagFactory:
109    @classmethod
110    def get(cls) -> FlagFactory:
111        """Get/create a shared `FlagFactory` instance."""
112        activity = ba.getactivity()
113        factory = activity.customdata.get(cls._STORENAME)
114        if factory is None:
115            factory = FlagFactory()
116            activity.customdata[cls._STORENAME] = factory
117        assert isinstance(factory, FlagFactory)
118        return factory

Get/create a shared FlagFactory instance.

@dataclass
class FlagPickedUpMessage:
121@dataclass
122class FlagPickedUpMessage:
123    """A message saying a `Flag` has been picked up.
124
125    Category: **Message Classes**
126    """
127
128    flag: Flag
129    """The `Flag` that has been picked up."""
130
131    node: ba.Node
132    """The ba.Node doing the picking up."""

A message saying a Flag has been picked up.

Category: Message Classes

FlagPickedUpMessage(flag: bastd.actor.flag.Flag, node: _ba.Node)

The Flag that has been picked up.

node: _ba.Node

The ba.Node doing the picking up.

@dataclass
class FlagDiedMessage:
135@dataclass
136class FlagDiedMessage:
137    """A message saying a `Flag` has died.
138
139    Category: **Message Classes**
140    """
141
142    flag: Flag
143    """The `Flag` that died."""

A message saying a Flag has died.

Category: Message Classes

FlagDiedMessage(flag: bastd.actor.flag.Flag)

The Flag that died.

@dataclass
class FlagDroppedMessage:
146@dataclass
147class FlagDroppedMessage:
148    """A message saying a `Flag` has been dropped.
149
150    Category: **Message Classes**
151    """
152
153    flag: Flag
154    """The `Flag` that was dropped."""
155
156    node: ba.Node
157    """The ba.Node that was holding it."""

A message saying a Flag has been dropped.

Category: Message Classes

FlagDroppedMessage(flag: bastd.actor.flag.Flag, node: _ba.Node)

The Flag that was dropped.

node: _ba.Node

The ba.Node that was holding it.

class Flag(ba._actor.Actor):
160class Flag(ba.Actor):
161    """A flag; used in games such as capture-the-flag or king-of-the-hill.
162
163    Category: **Gameplay Classes**
164
165    Can be stationary or carry-able by players.
166    """
167
168    def __init__(self,
169                 position: Sequence[float] = (0.0, 1.0, 0.0),
170                 color: Sequence[float] = (1.0, 1.0, 1.0),
171                 materials: Sequence[ba.Material] | None = None,
172                 touchable: bool = True,
173                 dropped_timeout: int | None = None):
174        """Instantiate a flag.
175
176        If 'touchable' is False, the flag will only touch terrain;
177        useful for things like king-of-the-hill where players should
178        not be moving the flag around.
179
180        'materials can be a list of extra `ba.Material`s to apply to the flag.
181
182        If 'dropped_timeout' is provided (in seconds), the flag will die
183        after remaining untouched for that long once it has been moved
184        from its initial position.
185        """
186
187        super().__init__()
188
189        self._initial_position: Sequence[float] | None = None
190        self._has_moved = False
191        shared = SharedObjects.get()
192        factory = FlagFactory.get()
193
194        if materials is None:
195            materials = []
196        elif not isinstance(materials, list):
197            # In case they passed a tuple or whatnot.
198            materials = list(materials)
199        if not touchable:
200            materials = [factory.no_hit_material] + materials
201
202        finalmaterials = ([shared.object_material, factory.flagmaterial] +
203                          materials)
204        self.node = ba.newnode('flag',
205                               attrs={
206                                   'position':
207                                       (position[0], position[1] + 0.75,
208                                        position[2]),
209                                   'color_texture': factory.flag_texture,
210                                   'color': color,
211                                   'materials': finalmaterials
212                               },
213                               delegate=self)
214
215        if dropped_timeout is not None:
216            dropped_timeout = int(dropped_timeout)
217        self._dropped_timeout = dropped_timeout
218        self._counter: ba.Node | None
219        if self._dropped_timeout is not None:
220            self._count = self._dropped_timeout
221            self._tick_timer = ba.Timer(1.0,
222                                        call=ba.WeakCall(self._tick),
223                                        repeat=True)
224            self._counter = ba.newnode('text',
225                                       owner=self.node,
226                                       attrs={
227                                           'in_world': True,
228                                           'color': (1, 1, 1, 0.7),
229                                           'scale': 0.015,
230                                           'shadow': 0.5,
231                                           'flatness': 1.0,
232                                           'h_align': 'center'
233                                       })
234        else:
235            self._counter = None
236
237        self._held_count = 0
238        self._score_text: ba.Node | None = None
239        self._score_text_hide_timer: ba.Timer | None = None
240
241    def _tick(self) -> None:
242        if self.node:
243
244            # Grab our initial position after one tick (in case we fall).
245            if self._initial_position is None:
246                self._initial_position = self.node.position
247
248                # Keep track of when we first move; we don't count down
249                # until then.
250            if not self._has_moved:
251                nodepos = self.node.position
252                if (max(
253                        abs(nodepos[i] - self._initial_position[i])
254                        for i in list(range(3))) > 1.0):
255                    self._has_moved = True
256
257            if self._held_count > 0 or not self._has_moved:
258                assert self._dropped_timeout is not None
259                assert self._counter
260                self._count = self._dropped_timeout
261                self._counter.text = ''
262            else:
263                self._count -= 1
264                if self._count <= 10:
265                    nodepos = self.node.position
266                    assert self._counter
267                    self._counter.position = (nodepos[0], nodepos[1] + 1.3,
268                                              nodepos[2])
269                    self._counter.text = str(self._count)
270                    if self._count < 1:
271                        self.handlemessage(ba.DieMessage())
272                else:
273                    assert self._counter
274                    self._counter.text = ''
275
276    def _hide_score_text(self) -> None:
277        assert self._score_text is not None
278        assert isinstance(self._score_text.scale, float)
279        ba.animate(self._score_text, 'scale', {
280            0: self._score_text.scale,
281            0.2: 0
282        })
283
284    def set_score_text(self, text: str) -> None:
285        """Show a message over the flag; handy for scores."""
286        if not self.node:
287            return
288        if not self._score_text:
289            start_scale = 0.0
290            math = ba.newnode('math',
291                              owner=self.node,
292                              attrs={
293                                  'input1': (0, 1.4, 0),
294                                  'operation': 'add'
295                              })
296            self.node.connectattr('position', math, 'input2')
297            self._score_text = ba.newnode('text',
298                                          owner=self.node,
299                                          attrs={
300                                              'text': text,
301                                              'in_world': True,
302                                              'scale': 0.02,
303                                              'shadow': 0.5,
304                                              'flatness': 1.0,
305                                              'h_align': 'center'
306                                          })
307            math.connectattr('output', self._score_text, 'position')
308        else:
309            assert isinstance(self._score_text.scale, float)
310            start_scale = self._score_text.scale
311            self._score_text.text = text
312        self._score_text.color = ba.safecolor(self.node.color)
313        ba.animate(self._score_text, 'scale', {0: start_scale, 0.2: 0.02})
314        self._score_text_hide_timer = ba.Timer(
315            1.0, ba.WeakCall(self._hide_score_text))
316
317    def handlemessage(self, msg: Any) -> Any:
318        assert not self.expired
319        if isinstance(msg, ba.DieMessage):
320            if self.node:
321                self.node.delete()
322                if not msg.immediate:
323                    self.activity.handlemessage(FlagDiedMessage(self))
324        elif isinstance(msg, ba.HitMessage):
325            assert self.node
326            assert msg.force_direction is not None
327            self.node.handlemessage(
328                'impulse', msg.pos[0], msg.pos[1], msg.pos[2], msg.velocity[0],
329                msg.velocity[1], msg.velocity[2], msg.magnitude,
330                msg.velocity_magnitude, msg.radius, 0, msg.force_direction[0],
331                msg.force_direction[1], msg.force_direction[2])
332        elif isinstance(msg, ba.PickedUpMessage):
333            self._held_count += 1
334            if self._held_count == 1 and self._counter is not None:
335                self._counter.text = ''
336            self.activity.handlemessage(FlagPickedUpMessage(self, msg.node))
337        elif isinstance(msg, ba.DroppedMessage):
338            self._held_count -= 1
339            if self._held_count < 0:
340                print('Flag held count < 0.')
341                self._held_count = 0
342            self.activity.handlemessage(FlagDroppedMessage(self, msg.node))
343        else:
344            super().handlemessage(msg)
345
346    @staticmethod
347    def project_stand(pos: Sequence[float]) -> None:
348        """Project a flag-stand onto the ground at the given position.
349
350        Useful for games such as capture-the-flag to show where a
351        movable flag originated from.
352        """
353        assert len(pos) == 3
354        ba.emitfx(position=pos, emit_type='flag_stand')

A flag; used in games such as capture-the-flag or king-of-the-hill.

Category: Gameplay Classes

Can be stationary or carry-able by players.

Flag( position: Sequence[float] = (0.0, 1.0, 0.0), color: Sequence[float] = (1.0, 1.0, 1.0), materials: Optional[Sequence[_ba.Material]] = None, touchable: bool = True, dropped_timeout: int | None = None)
168    def __init__(self,
169                 position: Sequence[float] = (0.0, 1.0, 0.0),
170                 color: Sequence[float] = (1.0, 1.0, 1.0),
171                 materials: Sequence[ba.Material] | None = None,
172                 touchable: bool = True,
173                 dropped_timeout: int | None = None):
174        """Instantiate a flag.
175
176        If 'touchable' is False, the flag will only touch terrain;
177        useful for things like king-of-the-hill where players should
178        not be moving the flag around.
179
180        'materials can be a list of extra `ba.Material`s to apply to the flag.
181
182        If 'dropped_timeout' is provided (in seconds), the flag will die
183        after remaining untouched for that long once it has been moved
184        from its initial position.
185        """
186
187        super().__init__()
188
189        self._initial_position: Sequence[float] | None = None
190        self._has_moved = False
191        shared = SharedObjects.get()
192        factory = FlagFactory.get()
193
194        if materials is None:
195            materials = []
196        elif not isinstance(materials, list):
197            # In case they passed a tuple or whatnot.
198            materials = list(materials)
199        if not touchable:
200            materials = [factory.no_hit_material] + materials
201
202        finalmaterials = ([shared.object_material, factory.flagmaterial] +
203                          materials)
204        self.node = ba.newnode('flag',
205                               attrs={
206                                   'position':
207                                       (position[0], position[1] + 0.75,
208                                        position[2]),
209                                   'color_texture': factory.flag_texture,
210                                   'color': color,
211                                   'materials': finalmaterials
212                               },
213                               delegate=self)
214
215        if dropped_timeout is not None:
216            dropped_timeout = int(dropped_timeout)
217        self._dropped_timeout = dropped_timeout
218        self._counter: ba.Node | None
219        if self._dropped_timeout is not None:
220            self._count = self._dropped_timeout
221            self._tick_timer = ba.Timer(1.0,
222                                        call=ba.WeakCall(self._tick),
223                                        repeat=True)
224            self._counter = ba.newnode('text',
225                                       owner=self.node,
226                                       attrs={
227                                           'in_world': True,
228                                           'color': (1, 1, 1, 0.7),
229                                           'scale': 0.015,
230                                           'shadow': 0.5,
231                                           'flatness': 1.0,
232                                           'h_align': 'center'
233                                       })
234        else:
235            self._counter = None
236
237        self._held_count = 0
238        self._score_text: ba.Node | None = None
239        self._score_text_hide_timer: ba.Timer | None = None

Instantiate a flag.

If 'touchable' is False, the flag will only touch terrain; useful for things like king-of-the-hill where players should not be moving the flag around.

'materials can be a list of extra ba.Materials to apply to the flag.

If 'dropped_timeout' is provided (in seconds), the flag will die after remaining untouched for that long once it has been moved from its initial position.

def set_score_text(self, text: str) -> None:
284    def set_score_text(self, text: str) -> None:
285        """Show a message over the flag; handy for scores."""
286        if not self.node:
287            return
288        if not self._score_text:
289            start_scale = 0.0
290            math = ba.newnode('math',
291                              owner=self.node,
292                              attrs={
293                                  'input1': (0, 1.4, 0),
294                                  'operation': 'add'
295                              })
296            self.node.connectattr('position', math, 'input2')
297            self._score_text = ba.newnode('text',
298                                          owner=self.node,
299                                          attrs={
300                                              'text': text,
301                                              'in_world': True,
302                                              'scale': 0.02,
303                                              'shadow': 0.5,
304                                              'flatness': 1.0,
305                                              'h_align': 'center'
306                                          })
307            math.connectattr('output', self._score_text, 'position')
308        else:
309            assert isinstance(self._score_text.scale, float)
310            start_scale = self._score_text.scale
311            self._score_text.text = text
312        self._score_text.color = ba.safecolor(self.node.color)
313        ba.animate(self._score_text, 'scale', {0: start_scale, 0.2: 0.02})
314        self._score_text_hide_timer = ba.Timer(
315            1.0, ba.WeakCall(self._hide_score_text))

Show a message over the flag; handy for scores.

def handlemessage(self, msg: Any) -> Any:
317    def handlemessage(self, msg: Any) -> Any:
318        assert not self.expired
319        if isinstance(msg, ba.DieMessage):
320            if self.node:
321                self.node.delete()
322                if not msg.immediate:
323                    self.activity.handlemessage(FlagDiedMessage(self))
324        elif isinstance(msg, ba.HitMessage):
325            assert self.node
326            assert msg.force_direction is not None
327            self.node.handlemessage(
328                'impulse', msg.pos[0], msg.pos[1], msg.pos[2], msg.velocity[0],
329                msg.velocity[1], msg.velocity[2], msg.magnitude,
330                msg.velocity_magnitude, msg.radius, 0, msg.force_direction[0],
331                msg.force_direction[1], msg.force_direction[2])
332        elif isinstance(msg, ba.PickedUpMessage):
333            self._held_count += 1
334            if self._held_count == 1 and self._counter is not None:
335                self._counter.text = ''
336            self.activity.handlemessage(FlagPickedUpMessage(self, msg.node))
337        elif isinstance(msg, ba.DroppedMessage):
338            self._held_count -= 1
339            if self._held_count < 0:
340                print('Flag held count < 0.')
341                self._held_count = 0
342            self.activity.handlemessage(FlagDroppedMessage(self, msg.node))
343        else:
344            super().handlemessage(msg)

General message handling; can be passed any message object.

@staticmethod
def project_stand(pos: Sequence[float]) -> None:
346    @staticmethod
347    def project_stand(pos: Sequence[float]) -> None:
348        """Project a flag-stand onto the ground at the given position.
349
350        Useful for games such as capture-the-flag to show where a
351        movable flag originated from.
352        """
353        assert len(pos) == 3
354        ba.emitfx(position=pos, emit_type='flag_stand')

Project a flag-stand onto the ground at the given position.

Useful for games such as capture-the-flag to show where a movable flag originated from.

Inherited Members
ba._actor.Actor
autoretain
on_expire
expired
exists
is_alive
activity
getactivity