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=( 103 ('modify_part_collision', 'collide', False), 104 ('modify_part_collision', 'physical', False), 105 ), 106 ) 107 108 self.flag_texture = ba.gettexture('flagColor') 109 110 @classmethod 111 def get(cls) -> FlagFactory: 112 """Get/create a shared `FlagFactory` instance.""" 113 activity = ba.getactivity() 114 factory = activity.customdata.get(cls._STORENAME) 115 if factory is None: 116 factory = FlagFactory() 117 activity.customdata[cls._STORENAME] = factory 118 assert isinstance(factory, FlagFactory) 119 return factory 120 121 122@dataclass 123class FlagPickedUpMessage: 124 """A message saying a `Flag` has been picked up. 125 126 Category: **Message Classes** 127 """ 128 129 flag: Flag 130 """The `Flag` that has been picked up.""" 131 132 node: ba.Node 133 """The ba.Node doing the picking up.""" 134 135 136@dataclass 137class FlagDiedMessage: 138 """A message saying a `Flag` has died. 139 140 Category: **Message Classes** 141 """ 142 143 flag: Flag 144 """The `Flag` that died.""" 145 146 147@dataclass 148class FlagDroppedMessage: 149 """A message saying a `Flag` has been dropped. 150 151 Category: **Message Classes** 152 """ 153 154 flag: Flag 155 """The `Flag` that was dropped.""" 156 157 node: ba.Node 158 """The ba.Node that was holding it.""" 159 160 161class Flag(ba.Actor): 162 """A flag; used in games such as capture-the-flag or king-of-the-hill. 163 164 Category: **Gameplay Classes** 165 166 Can be stationary or carry-able by players. 167 """ 168 169 def __init__( 170 self, 171 position: Sequence[float] = (0.0, 1.0, 0.0), 172 color: Sequence[float] = (1.0, 1.0, 1.0), 173 materials: Sequence[ba.Material] | None = None, 174 touchable: bool = True, 175 dropped_timeout: int | None = None, 176 ): 177 """Instantiate a flag. 178 179 If 'touchable' is False, the flag will only touch terrain; 180 useful for things like king-of-the-hill where players should 181 not be moving the flag around. 182 183 'materials can be a list of extra `ba.Material`s to apply to the flag. 184 185 If 'dropped_timeout' is provided (in seconds), the flag will die 186 after remaining untouched for that long once it has been moved 187 from its initial position. 188 """ 189 190 super().__init__() 191 192 self._initial_position: Sequence[float] | None = None 193 self._has_moved = False 194 shared = SharedObjects.get() 195 factory = FlagFactory.get() 196 197 if materials is None: 198 materials = [] 199 elif not isinstance(materials, list): 200 # In case they passed a tuple or whatnot. 201 materials = list(materials) 202 if not touchable: 203 materials = [factory.no_hit_material] + materials 204 205 finalmaterials = [ 206 shared.object_material, 207 factory.flagmaterial, 208 ] + materials 209 self.node = ba.newnode( 210 'flag', 211 attrs={ 212 'position': (position[0], position[1] + 0.75, position[2]), 213 'color_texture': factory.flag_texture, 214 'color': color, 215 'materials': finalmaterials, 216 }, 217 delegate=self, 218 ) 219 220 if dropped_timeout is not None: 221 dropped_timeout = int(dropped_timeout) 222 self._dropped_timeout = dropped_timeout 223 self._counter: ba.Node | None 224 if self._dropped_timeout is not None: 225 self._count = self._dropped_timeout 226 self._tick_timer = ba.Timer( 227 1.0, call=ba.WeakCall(self._tick), repeat=True 228 ) 229 self._counter = ba.newnode( 230 'text', 231 owner=self.node, 232 attrs={ 233 'in_world': True, 234 'color': (1, 1, 1, 0.7), 235 'scale': 0.015, 236 'shadow': 0.5, 237 'flatness': 1.0, 238 'h_align': 'center', 239 }, 240 ) 241 else: 242 self._counter = None 243 244 self._held_count = 0 245 self._score_text: ba.Node | None = None 246 self._score_text_hide_timer: ba.Timer | None = None 247 248 def _tick(self) -> None: 249 if self.node: 250 251 # Grab our initial position after one tick (in case we fall). 252 if self._initial_position is None: 253 self._initial_position = self.node.position 254 255 # Keep track of when we first move; we don't count down 256 # until then. 257 if not self._has_moved: 258 nodepos = self.node.position 259 if ( 260 max( 261 abs(nodepos[i] - self._initial_position[i]) 262 for i in list(range(3)) 263 ) 264 > 1.0 265 ): 266 self._has_moved = True 267 268 if self._held_count > 0 or not self._has_moved: 269 assert self._dropped_timeout is not None 270 assert self._counter 271 self._count = self._dropped_timeout 272 self._counter.text = '' 273 else: 274 self._count -= 1 275 if self._count <= 10: 276 nodepos = self.node.position 277 assert self._counter 278 self._counter.position = ( 279 nodepos[0], 280 nodepos[1] + 1.3, 281 nodepos[2], 282 ) 283 self._counter.text = str(self._count) 284 if self._count < 1: 285 self.handlemessage(ba.DieMessage()) 286 else: 287 assert self._counter 288 self._counter.text = '' 289 290 def _hide_score_text(self) -> None: 291 assert self._score_text is not None 292 assert isinstance(self._score_text.scale, float) 293 ba.animate( 294 self._score_text, 'scale', {0: self._score_text.scale, 0.2: 0} 295 ) 296 297 def set_score_text(self, text: str) -> None: 298 """Show a message over the flag; handy for scores.""" 299 if not self.node: 300 return 301 if not self._score_text: 302 start_scale = 0.0 303 math = ba.newnode( 304 'math', 305 owner=self.node, 306 attrs={'input1': (0, 1.4, 0), 'operation': 'add'}, 307 ) 308 self.node.connectattr('position', math, 'input2') 309 self._score_text = ba.newnode( 310 'text', 311 owner=self.node, 312 attrs={ 313 'text': text, 314 'in_world': True, 315 'scale': 0.02, 316 'shadow': 0.5, 317 'flatness': 1.0, 318 'h_align': 'center', 319 }, 320 ) 321 math.connectattr('output', self._score_text, 'position') 322 else: 323 assert isinstance(self._score_text.scale, float) 324 start_scale = self._score_text.scale 325 self._score_text.text = text 326 self._score_text.color = ba.safecolor(self.node.color) 327 ba.animate(self._score_text, 'scale', {0: start_scale, 0.2: 0.02}) 328 self._score_text_hide_timer = ba.Timer( 329 1.0, ba.WeakCall(self._hide_score_text) 330 ) 331 332 def handlemessage(self, msg: Any) -> Any: 333 assert not self.expired 334 if isinstance(msg, ba.DieMessage): 335 if self.node: 336 self.node.delete() 337 if not msg.immediate: 338 self.activity.handlemessage(FlagDiedMessage(self)) 339 elif isinstance(msg, ba.HitMessage): 340 assert self.node 341 assert msg.force_direction is not None 342 self.node.handlemessage( 343 'impulse', 344 msg.pos[0], 345 msg.pos[1], 346 msg.pos[2], 347 msg.velocity[0], 348 msg.velocity[1], 349 msg.velocity[2], 350 msg.magnitude, 351 msg.velocity_magnitude, 352 msg.radius, 353 0, 354 msg.force_direction[0], 355 msg.force_direction[1], 356 msg.force_direction[2], 357 ) 358 elif isinstance(msg, ba.PickedUpMessage): 359 self._held_count += 1 360 if self._held_count == 1 and self._counter is not None: 361 self._counter.text = '' 362 self.activity.handlemessage(FlagPickedUpMessage(self, msg.node)) 363 elif isinstance(msg, ba.DroppedMessage): 364 self._held_count -= 1 365 if self._held_count < 0: 366 print('Flag held count < 0.') 367 self._held_count = 0 368 self.activity.handlemessage(FlagDroppedMessage(self, msg.node)) 369 else: 370 super().handlemessage(msg) 371 372 @staticmethod 373 def project_stand(pos: Sequence[float]) -> None: 374 """Project a flag-stand onto the ground at the given position. 375 376 Useful for games such as capture-the-flag to show where a 377 movable flag originated from. 378 """ 379 assert len(pos) == 3 380 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=( 104 ('modify_part_collision', 'collide', False), 105 ('modify_part_collision', 'physical', False), 106 ), 107 ) 108 109 self.flag_texture = ba.gettexture('flagColor') 110 111 @classmethod 112 def get(cls) -> FlagFactory: 113 """Get/create a shared `FlagFactory` instance.""" 114 activity = ba.getactivity() 115 factory = activity.customdata.get(cls._STORENAME) 116 if factory is None: 117 factory = FlagFactory() 118 activity.customdata[cls._STORENAME] = factory 119 assert isinstance(factory, FlagFactory) 120 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=( 104 ('modify_part_collision', 'collide', False), 105 ('modify_part_collision', 'physical', False), 106 ), 107 ) 108 109 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.
111 @classmethod 112 def get(cls) -> FlagFactory: 113 """Get/create a shared `FlagFactory` instance.""" 114 activity = ba.getactivity() 115 factory = activity.customdata.get(cls._STORENAME) 116 if factory is None: 117 factory = FlagFactory() 118 activity.customdata[cls._STORENAME] = factory 119 assert isinstance(factory, FlagFactory) 120 return factory
Get/create a shared FlagFactory
instance.
123@dataclass 124class FlagPickedUpMessage: 125 """A message saying a `Flag` has been picked up. 126 127 Category: **Message Classes** 128 """ 129 130 flag: Flag 131 """The `Flag` that has been picked up.""" 132 133 node: ba.Node 134 """The ba.Node doing the picking up."""
A message saying a Flag
has been picked up.
Category: Message Classes
137@dataclass 138class FlagDiedMessage: 139 """A message saying a `Flag` has died. 140 141 Category: **Message Classes** 142 """ 143 144 flag: Flag 145 """The `Flag` that died."""
A message saying a Flag
has died.
Category: Message Classes
148@dataclass 149class FlagDroppedMessage: 150 """A message saying a `Flag` has been dropped. 151 152 Category: **Message Classes** 153 """ 154 155 flag: Flag 156 """The `Flag` that was dropped.""" 157 158 node: ba.Node 159 """The ba.Node that was holding it."""
A message saying a Flag
has been dropped.
Category: Message Classes
162class Flag(ba.Actor): 163 """A flag; used in games such as capture-the-flag or king-of-the-hill. 164 165 Category: **Gameplay Classes** 166 167 Can be stationary or carry-able by players. 168 """ 169 170 def __init__( 171 self, 172 position: Sequence[float] = (0.0, 1.0, 0.0), 173 color: Sequence[float] = (1.0, 1.0, 1.0), 174 materials: Sequence[ba.Material] | None = None, 175 touchable: bool = True, 176 dropped_timeout: int | None = None, 177 ): 178 """Instantiate a flag. 179 180 If 'touchable' is False, the flag will only touch terrain; 181 useful for things like king-of-the-hill where players should 182 not be moving the flag around. 183 184 'materials can be a list of extra `ba.Material`s to apply to the flag. 185 186 If 'dropped_timeout' is provided (in seconds), the flag will die 187 after remaining untouched for that long once it has been moved 188 from its initial position. 189 """ 190 191 super().__init__() 192 193 self._initial_position: Sequence[float] | None = None 194 self._has_moved = False 195 shared = SharedObjects.get() 196 factory = FlagFactory.get() 197 198 if materials is None: 199 materials = [] 200 elif not isinstance(materials, list): 201 # In case they passed a tuple or whatnot. 202 materials = list(materials) 203 if not touchable: 204 materials = [factory.no_hit_material] + materials 205 206 finalmaterials = [ 207 shared.object_material, 208 factory.flagmaterial, 209 ] + materials 210 self.node = ba.newnode( 211 'flag', 212 attrs={ 213 'position': (position[0], position[1] + 0.75, position[2]), 214 'color_texture': factory.flag_texture, 215 'color': color, 216 'materials': finalmaterials, 217 }, 218 delegate=self, 219 ) 220 221 if dropped_timeout is not None: 222 dropped_timeout = int(dropped_timeout) 223 self._dropped_timeout = dropped_timeout 224 self._counter: ba.Node | None 225 if self._dropped_timeout is not None: 226 self._count = self._dropped_timeout 227 self._tick_timer = ba.Timer( 228 1.0, call=ba.WeakCall(self._tick), repeat=True 229 ) 230 self._counter = ba.newnode( 231 'text', 232 owner=self.node, 233 attrs={ 234 'in_world': True, 235 'color': (1, 1, 1, 0.7), 236 'scale': 0.015, 237 'shadow': 0.5, 238 'flatness': 1.0, 239 'h_align': 'center', 240 }, 241 ) 242 else: 243 self._counter = None 244 245 self._held_count = 0 246 self._score_text: ba.Node | None = None 247 self._score_text_hide_timer: ba.Timer | None = None 248 249 def _tick(self) -> None: 250 if self.node: 251 252 # Grab our initial position after one tick (in case we fall). 253 if self._initial_position is None: 254 self._initial_position = self.node.position 255 256 # Keep track of when we first move; we don't count down 257 # until then. 258 if not self._has_moved: 259 nodepos = self.node.position 260 if ( 261 max( 262 abs(nodepos[i] - self._initial_position[i]) 263 for i in list(range(3)) 264 ) 265 > 1.0 266 ): 267 self._has_moved = True 268 269 if self._held_count > 0 or not self._has_moved: 270 assert self._dropped_timeout is not None 271 assert self._counter 272 self._count = self._dropped_timeout 273 self._counter.text = '' 274 else: 275 self._count -= 1 276 if self._count <= 10: 277 nodepos = self.node.position 278 assert self._counter 279 self._counter.position = ( 280 nodepos[0], 281 nodepos[1] + 1.3, 282 nodepos[2], 283 ) 284 self._counter.text = str(self._count) 285 if self._count < 1: 286 self.handlemessage(ba.DieMessage()) 287 else: 288 assert self._counter 289 self._counter.text = '' 290 291 def _hide_score_text(self) -> None: 292 assert self._score_text is not None 293 assert isinstance(self._score_text.scale, float) 294 ba.animate( 295 self._score_text, 'scale', {0: self._score_text.scale, 0.2: 0} 296 ) 297 298 def set_score_text(self, text: str) -> None: 299 """Show a message over the flag; handy for scores.""" 300 if not self.node: 301 return 302 if not self._score_text: 303 start_scale = 0.0 304 math = ba.newnode( 305 'math', 306 owner=self.node, 307 attrs={'input1': (0, 1.4, 0), 'operation': 'add'}, 308 ) 309 self.node.connectattr('position', math, 'input2') 310 self._score_text = ba.newnode( 311 'text', 312 owner=self.node, 313 attrs={ 314 'text': text, 315 'in_world': True, 316 'scale': 0.02, 317 'shadow': 0.5, 318 'flatness': 1.0, 319 'h_align': 'center', 320 }, 321 ) 322 math.connectattr('output', self._score_text, 'position') 323 else: 324 assert isinstance(self._score_text.scale, float) 325 start_scale = self._score_text.scale 326 self._score_text.text = text 327 self._score_text.color = ba.safecolor(self.node.color) 328 ba.animate(self._score_text, 'scale', {0: start_scale, 0.2: 0.02}) 329 self._score_text_hide_timer = ba.Timer( 330 1.0, ba.WeakCall(self._hide_score_text) 331 ) 332 333 def handlemessage(self, msg: Any) -> Any: 334 assert not self.expired 335 if isinstance(msg, ba.DieMessage): 336 if self.node: 337 self.node.delete() 338 if not msg.immediate: 339 self.activity.handlemessage(FlagDiedMessage(self)) 340 elif isinstance(msg, ba.HitMessage): 341 assert self.node 342 assert msg.force_direction is not None 343 self.node.handlemessage( 344 'impulse', 345 msg.pos[0], 346 msg.pos[1], 347 msg.pos[2], 348 msg.velocity[0], 349 msg.velocity[1], 350 msg.velocity[2], 351 msg.magnitude, 352 msg.velocity_magnitude, 353 msg.radius, 354 0, 355 msg.force_direction[0], 356 msg.force_direction[1], 357 msg.force_direction[2], 358 ) 359 elif isinstance(msg, ba.PickedUpMessage): 360 self._held_count += 1 361 if self._held_count == 1 and self._counter is not None: 362 self._counter.text = '' 363 self.activity.handlemessage(FlagPickedUpMessage(self, msg.node)) 364 elif isinstance(msg, ba.DroppedMessage): 365 self._held_count -= 1 366 if self._held_count < 0: 367 print('Flag held count < 0.') 368 self._held_count = 0 369 self.activity.handlemessage(FlagDroppedMessage(self, msg.node)) 370 else: 371 super().handlemessage(msg) 372 373 @staticmethod 374 def project_stand(pos: Sequence[float]) -> None: 375 """Project a flag-stand onto the ground at the given position. 376 377 Useful for games such as capture-the-flag to show where a 378 movable flag originated from. 379 """ 380 assert len(pos) == 3 381 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.
170 def __init__( 171 self, 172 position: Sequence[float] = (0.0, 1.0, 0.0), 173 color: Sequence[float] = (1.0, 1.0, 1.0), 174 materials: Sequence[ba.Material] | None = None, 175 touchable: bool = True, 176 dropped_timeout: int | None = None, 177 ): 178 """Instantiate a flag. 179 180 If 'touchable' is False, the flag will only touch terrain; 181 useful for things like king-of-the-hill where players should 182 not be moving the flag around. 183 184 'materials can be a list of extra `ba.Material`s to apply to the flag. 185 186 If 'dropped_timeout' is provided (in seconds), the flag will die 187 after remaining untouched for that long once it has been moved 188 from its initial position. 189 """ 190 191 super().__init__() 192 193 self._initial_position: Sequence[float] | None = None 194 self._has_moved = False 195 shared = SharedObjects.get() 196 factory = FlagFactory.get() 197 198 if materials is None: 199 materials = [] 200 elif not isinstance(materials, list): 201 # In case they passed a tuple or whatnot. 202 materials = list(materials) 203 if not touchable: 204 materials = [factory.no_hit_material] + materials 205 206 finalmaterials = [ 207 shared.object_material, 208 factory.flagmaterial, 209 ] + materials 210 self.node = ba.newnode( 211 'flag', 212 attrs={ 213 'position': (position[0], position[1] + 0.75, position[2]), 214 'color_texture': factory.flag_texture, 215 'color': color, 216 'materials': finalmaterials, 217 }, 218 delegate=self, 219 ) 220 221 if dropped_timeout is not None: 222 dropped_timeout = int(dropped_timeout) 223 self._dropped_timeout = dropped_timeout 224 self._counter: ba.Node | None 225 if self._dropped_timeout is not None: 226 self._count = self._dropped_timeout 227 self._tick_timer = ba.Timer( 228 1.0, call=ba.WeakCall(self._tick), repeat=True 229 ) 230 self._counter = ba.newnode( 231 'text', 232 owner=self.node, 233 attrs={ 234 'in_world': True, 235 'color': (1, 1, 1, 0.7), 236 'scale': 0.015, 237 'shadow': 0.5, 238 'flatness': 1.0, 239 'h_align': 'center', 240 }, 241 ) 242 else: 243 self._counter = None 244 245 self._held_count = 0 246 self._score_text: ba.Node | None = None 247 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.
298 def set_score_text(self, text: str) -> None: 299 """Show a message over the flag; handy for scores.""" 300 if not self.node: 301 return 302 if not self._score_text: 303 start_scale = 0.0 304 math = ba.newnode( 305 'math', 306 owner=self.node, 307 attrs={'input1': (0, 1.4, 0), 'operation': 'add'}, 308 ) 309 self.node.connectattr('position', math, 'input2') 310 self._score_text = ba.newnode( 311 'text', 312 owner=self.node, 313 attrs={ 314 'text': text, 315 'in_world': True, 316 'scale': 0.02, 317 'shadow': 0.5, 318 'flatness': 1.0, 319 'h_align': 'center', 320 }, 321 ) 322 math.connectattr('output', self._score_text, 'position') 323 else: 324 assert isinstance(self._score_text.scale, float) 325 start_scale = self._score_text.scale 326 self._score_text.text = text 327 self._score_text.color = ba.safecolor(self.node.color) 328 ba.animate(self._score_text, 'scale', {0: start_scale, 0.2: 0.02}) 329 self._score_text_hide_timer = ba.Timer( 330 1.0, ba.WeakCall(self._hide_score_text) 331 )
Show a message over the flag; handy for scores.
333 def handlemessage(self, msg: Any) -> Any: 334 assert not self.expired 335 if isinstance(msg, ba.DieMessage): 336 if self.node: 337 self.node.delete() 338 if not msg.immediate: 339 self.activity.handlemessage(FlagDiedMessage(self)) 340 elif isinstance(msg, ba.HitMessage): 341 assert self.node 342 assert msg.force_direction is not None 343 self.node.handlemessage( 344 'impulse', 345 msg.pos[0], 346 msg.pos[1], 347 msg.pos[2], 348 msg.velocity[0], 349 msg.velocity[1], 350 msg.velocity[2], 351 msg.magnitude, 352 msg.velocity_magnitude, 353 msg.radius, 354 0, 355 msg.force_direction[0], 356 msg.force_direction[1], 357 msg.force_direction[2], 358 ) 359 elif isinstance(msg, ba.PickedUpMessage): 360 self._held_count += 1 361 if self._held_count == 1 and self._counter is not None: 362 self._counter.text = '' 363 self.activity.handlemessage(FlagPickedUpMessage(self, msg.node)) 364 elif isinstance(msg, ba.DroppedMessage): 365 self._held_count -= 1 366 if self._held_count < 0: 367 print('Flag held count < 0.') 368 self._held_count = 0 369 self.activity.handlemessage(FlagDroppedMessage(self, msg.node)) 370 else: 371 super().handlemessage(msg)
General message handling; can be passed any message object.
373 @staticmethod 374 def project_stand(pos: Sequence[float]) -> None: 375 """Project a flag-stand onto the ground at the given position. 376 377 Useful for games such as capture-the-flag to show where a 378 movable flag originated from. 379 """ 380 assert len(pos) == 3 381 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