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')
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 Flag
s.
Category: Gameplay Classes
A single instance of this is shared between all flags and can be retrieved via FlagFactory.get().
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.
A ba.Material that prevents contact with most objects; applied to 'non-touchable' flags.
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.
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
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
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
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.
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.Material
s 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.
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.
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.
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