bascenev1lib.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, override 9 10import bascenev1 as bs 11 12from bascenev1lib.gameutils import SharedObjects 13 14if TYPE_CHECKING: 15 from typing import Any, Sequence 16 17 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: bs.Material 28 """The bs.Material applied to all `Flag`s.""" 29 30 impact_sound: bs.Sound 31 """The bs.Sound used when a `Flag` hits the ground.""" 32 33 skid_sound: bs.Sound 34 """The bs.Sound used when a `Flag` skids along the ground.""" 35 36 no_hit_material: bs.Material 37 """A bs.Material that prevents contact with most objects; 38 applied to 'non-touchable' flags.""" 39 40 flag_texture: bs.Texture 41 """The bs.Texture for flags.""" 42 43 _STORENAME = bs.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 = bs.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 = bs.getsound('metalHit') 74 self.skid_sound = bs.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 = bs.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 = bs.gettexture('flagColor') 110 111 @classmethod 112 def get(cls) -> FlagFactory: 113 """Get/create a shared `FlagFactory` instance.""" 114 activity = bs.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 121 122 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: bs.Node 134 """The bs.Node doing the picking up.""" 135 136 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.""" 146 147 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: bs.Node 159 """The bs.Node that was holding it.""" 160 161 162class Flag(bs.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[bs.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 `bs.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 = bs.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: bs.Node | None 225 if self._dropped_timeout is not None: 226 self._count = self._dropped_timeout 227 self._tick_timer = bs.Timer( 228 1.0, call=bs.WeakCall(self._tick), repeat=True 229 ) 230 self._counter = bs.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: bs.Node | None = None 247 self._score_text_hide_timer: bs.Timer | None = None 248 249 def _tick(self) -> None: 250 if self.node: 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(bs.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 bs.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 = bs.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 = bs.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 = bs.safecolor(self.node.color) 327 bs.animate(self._score_text, 'scale', {0: start_scale, 0.2: 0.02}) 328 self._score_text_hide_timer = bs.Timer( 329 1.0, bs.WeakCall(self._hide_score_text) 330 ) 331 332 @override 333 def handlemessage(self, msg: Any) -> Any: 334 assert not self.expired 335 if isinstance(msg, bs.DieMessage): 336 if self.node: 337 self.node.delete() 338 if not msg.immediate: 339 self.activity.handlemessage(FlagDiedMessage(self)) 340 elif isinstance(msg, bs.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, bs.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, bs.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 bs.emitfx(position=pos, emit_type='flag_stand')
19class FlagFactory: 20 """Wraps up media and other resources used by `Flag`s. 21 22 Category: **Gameplay Classes** 23 24 A single instance of this is shared between all flags 25 and can be retrieved via FlagFactory.get(). 26 """ 27 28 flagmaterial: bs.Material 29 """The bs.Material applied to all `Flag`s.""" 30 31 impact_sound: bs.Sound 32 """The bs.Sound used when a `Flag` hits the ground.""" 33 34 skid_sound: bs.Sound 35 """The bs.Sound used when a `Flag` skids along the ground.""" 36 37 no_hit_material: bs.Material 38 """A bs.Material that prevents contact with most objects; 39 applied to 'non-touchable' flags.""" 40 41 flag_texture: bs.Texture 42 """The bs.Texture for flags.""" 43 44 _STORENAME = bs.storagename() 45 46 def __init__(self) -> None: 47 """Instantiate a `FlagFactory`. 48 49 You shouldn't need to do this; call FlagFactory.get() to 50 get a shared instance. 51 """ 52 shared = SharedObjects.get() 53 self.flagmaterial = bs.Material() 54 self.flagmaterial.add_actions( 55 conditions=( 56 ('we_are_younger_than', 100), 57 'and', 58 ('they_have_material', shared.object_material), 59 ), 60 actions=('modify_node_collision', 'collide', False), 61 ) 62 63 self.flagmaterial.add_actions( 64 conditions=( 65 'they_have_material', 66 shared.footing_material, 67 ), 68 actions=( 69 ('message', 'our_node', 'at_connect', 'footing', 1), 70 ('message', 'our_node', 'at_disconnect', 'footing', -1), 71 ), 72 ) 73 74 self.impact_sound = bs.getsound('metalHit') 75 self.skid_sound = bs.getsound('metalSkid') 76 self.flagmaterial.add_actions( 77 conditions=( 78 'they_have_material', 79 shared.footing_material, 80 ), 81 actions=( 82 ('impact_sound', self.impact_sound, 2, 5), 83 ('skid_sound', self.skid_sound, 2, 5), 84 ), 85 ) 86 87 self.no_hit_material = bs.Material() 88 self.no_hit_material.add_actions( 89 conditions=( 90 ('they_have_material', shared.pickup_material), 91 'or', 92 ('they_have_material', shared.attack_material), 93 ), 94 actions=('modify_part_collision', 'collide', False), 95 ) 96 97 # We also don't want anything moving it. 98 self.no_hit_material.add_actions( 99 conditions=( 100 ('they_have_material', shared.object_material), 101 'or', 102 ('they_dont_have_material', shared.footing_material), 103 ), 104 actions=( 105 ('modify_part_collision', 'collide', False), 106 ('modify_part_collision', 'physical', False), 107 ), 108 ) 109 110 self.flag_texture = bs.gettexture('flagColor') 111 112 @classmethod 113 def get(cls) -> FlagFactory: 114 """Get/create a shared `FlagFactory` instance.""" 115 activity = bs.getactivity() 116 factory = activity.customdata.get(cls._STORENAME) 117 if factory is None: 118 factory = FlagFactory() 119 activity.customdata[cls._STORENAME] = factory 120 assert isinstance(factory, FlagFactory) 121 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().
46 def __init__(self) -> None: 47 """Instantiate a `FlagFactory`. 48 49 You shouldn't need to do this; call FlagFactory.get() to 50 get a shared instance. 51 """ 52 shared = SharedObjects.get() 53 self.flagmaterial = bs.Material() 54 self.flagmaterial.add_actions( 55 conditions=( 56 ('we_are_younger_than', 100), 57 'and', 58 ('they_have_material', shared.object_material), 59 ), 60 actions=('modify_node_collision', 'collide', False), 61 ) 62 63 self.flagmaterial.add_actions( 64 conditions=( 65 'they_have_material', 66 shared.footing_material, 67 ), 68 actions=( 69 ('message', 'our_node', 'at_connect', 'footing', 1), 70 ('message', 'our_node', 'at_disconnect', 'footing', -1), 71 ), 72 ) 73 74 self.impact_sound = bs.getsound('metalHit') 75 self.skid_sound = bs.getsound('metalSkid') 76 self.flagmaterial.add_actions( 77 conditions=( 78 'they_have_material', 79 shared.footing_material, 80 ), 81 actions=( 82 ('impact_sound', self.impact_sound, 2, 5), 83 ('skid_sound', self.skid_sound, 2, 5), 84 ), 85 ) 86 87 self.no_hit_material = bs.Material() 88 self.no_hit_material.add_actions( 89 conditions=( 90 ('they_have_material', shared.pickup_material), 91 'or', 92 ('they_have_material', shared.attack_material), 93 ), 94 actions=('modify_part_collision', 'collide', False), 95 ) 96 97 # We also don't want anything moving it. 98 self.no_hit_material.add_actions( 99 conditions=( 100 ('they_have_material', shared.object_material), 101 'or', 102 ('they_dont_have_material', shared.footing_material), 103 ), 104 actions=( 105 ('modify_part_collision', 'collide', False), 106 ('modify_part_collision', 'physical', False), 107 ), 108 ) 109 110 self.flag_texture = bs.gettexture('flagColor')
Instantiate a FlagFactory
.
You shouldn't need to do this; call FlagFactory.get() to get a shared instance.
A bs.Material that prevents contact with most objects; applied to 'non-touchable' flags.
112 @classmethod 113 def get(cls) -> FlagFactory: 114 """Get/create a shared `FlagFactory` instance.""" 115 activity = bs.getactivity() 116 factory = activity.customdata.get(cls._STORENAME) 117 if factory is None: 118 factory = FlagFactory() 119 activity.customdata[cls._STORENAME] = factory 120 assert isinstance(factory, FlagFactory) 121 return factory
Get/create a shared FlagFactory
instance.
124@dataclass 125class FlagPickedUpMessage: 126 """A message saying a `Flag` has been picked up. 127 128 Category: **Message Classes** 129 """ 130 131 flag: Flag 132 """The `Flag` that has been picked up.""" 133 134 node: bs.Node 135 """The bs.Node doing the picking up."""
A message saying a Flag
has been picked up.
Category: Message Classes
138@dataclass 139class FlagDiedMessage: 140 """A message saying a `Flag` has died. 141 142 Category: **Message Classes** 143 """ 144 145 flag: Flag 146 """The `Flag` that died."""
A message saying a Flag
has died.
Category: Message Classes
149@dataclass 150class FlagDroppedMessage: 151 """A message saying a `Flag` has been dropped. 152 153 Category: **Message Classes** 154 """ 155 156 flag: Flag 157 """The `Flag` that was dropped.""" 158 159 node: bs.Node 160 """The bs.Node that was holding it."""
A message saying a Flag
has been dropped.
Category: Message Classes
163class Flag(bs.Actor): 164 """A flag; used in games such as capture-the-flag or king-of-the-hill. 165 166 Category: **Gameplay Classes** 167 168 Can be stationary or carry-able by players. 169 """ 170 171 def __init__( 172 self, 173 position: Sequence[float] = (0.0, 1.0, 0.0), 174 color: Sequence[float] = (1.0, 1.0, 1.0), 175 materials: Sequence[bs.Material] | None = None, 176 touchable: bool = True, 177 dropped_timeout: int | None = None, 178 ): 179 """Instantiate a flag. 180 181 If 'touchable' is False, the flag will only touch terrain; 182 useful for things like king-of-the-hill where players should 183 not be moving the flag around. 184 185 'materials can be a list of extra `bs.Material`s to apply to the flag. 186 187 If 'dropped_timeout' is provided (in seconds), the flag will die 188 after remaining untouched for that long once it has been moved 189 from its initial position. 190 """ 191 192 super().__init__() 193 194 self._initial_position: Sequence[float] | None = None 195 self._has_moved = False 196 shared = SharedObjects.get() 197 factory = FlagFactory.get() 198 199 if materials is None: 200 materials = [] 201 elif not isinstance(materials, list): 202 # In case they passed a tuple or whatnot. 203 materials = list(materials) 204 if not touchable: 205 materials = [factory.no_hit_material] + materials 206 207 finalmaterials = [ 208 shared.object_material, 209 factory.flagmaterial, 210 ] + materials 211 self.node = bs.newnode( 212 'flag', 213 attrs={ 214 'position': (position[0], position[1] + 0.75, position[2]), 215 'color_texture': factory.flag_texture, 216 'color': color, 217 'materials': finalmaterials, 218 }, 219 delegate=self, 220 ) 221 222 if dropped_timeout is not None: 223 dropped_timeout = int(dropped_timeout) 224 self._dropped_timeout = dropped_timeout 225 self._counter: bs.Node | None 226 if self._dropped_timeout is not None: 227 self._count = self._dropped_timeout 228 self._tick_timer = bs.Timer( 229 1.0, call=bs.WeakCall(self._tick), repeat=True 230 ) 231 self._counter = bs.newnode( 232 'text', 233 owner=self.node, 234 attrs={ 235 'in_world': True, 236 'color': (1, 1, 1, 0.7), 237 'scale': 0.015, 238 'shadow': 0.5, 239 'flatness': 1.0, 240 'h_align': 'center', 241 }, 242 ) 243 else: 244 self._counter = None 245 246 self._held_count = 0 247 self._score_text: bs.Node | None = None 248 self._score_text_hide_timer: bs.Timer | None = None 249 250 def _tick(self) -> None: 251 if self.node: 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(bs.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 bs.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 = bs.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 = bs.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 = bs.safecolor(self.node.color) 328 bs.animate(self._score_text, 'scale', {0: start_scale, 0.2: 0.02}) 329 self._score_text_hide_timer = bs.Timer( 330 1.0, bs.WeakCall(self._hide_score_text) 331 ) 332 333 @override 334 def handlemessage(self, msg: Any) -> Any: 335 assert not self.expired 336 if isinstance(msg, bs.DieMessage): 337 if self.node: 338 self.node.delete() 339 if not msg.immediate: 340 self.activity.handlemessage(FlagDiedMessage(self)) 341 elif isinstance(msg, bs.HitMessage): 342 assert self.node 343 assert msg.force_direction is not None 344 self.node.handlemessage( 345 'impulse', 346 msg.pos[0], 347 msg.pos[1], 348 msg.pos[2], 349 msg.velocity[0], 350 msg.velocity[1], 351 msg.velocity[2], 352 msg.magnitude, 353 msg.velocity_magnitude, 354 msg.radius, 355 0, 356 msg.force_direction[0], 357 msg.force_direction[1], 358 msg.force_direction[2], 359 ) 360 elif isinstance(msg, bs.PickedUpMessage): 361 self._held_count += 1 362 if self._held_count == 1 and self._counter is not None: 363 self._counter.text = '' 364 self.activity.handlemessage(FlagPickedUpMessage(self, msg.node)) 365 elif isinstance(msg, bs.DroppedMessage): 366 self._held_count -= 1 367 if self._held_count < 0: 368 print('Flag held count < 0.') 369 self._held_count = 0 370 self.activity.handlemessage(FlagDroppedMessage(self, msg.node)) 371 else: 372 super().handlemessage(msg) 373 374 @staticmethod 375 def project_stand(pos: Sequence[float]) -> None: 376 """Project a flag-stand onto the ground at the given position. 377 378 Useful for games such as capture-the-flag to show where a 379 movable flag originated from. 380 """ 381 assert len(pos) == 3 382 bs.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.
171 def __init__( 172 self, 173 position: Sequence[float] = (0.0, 1.0, 0.0), 174 color: Sequence[float] = (1.0, 1.0, 1.0), 175 materials: Sequence[bs.Material] | None = None, 176 touchable: bool = True, 177 dropped_timeout: int | None = None, 178 ): 179 """Instantiate a flag. 180 181 If 'touchable' is False, the flag will only touch terrain; 182 useful for things like king-of-the-hill where players should 183 not be moving the flag around. 184 185 'materials can be a list of extra `bs.Material`s to apply to the flag. 186 187 If 'dropped_timeout' is provided (in seconds), the flag will die 188 after remaining untouched for that long once it has been moved 189 from its initial position. 190 """ 191 192 super().__init__() 193 194 self._initial_position: Sequence[float] | None = None 195 self._has_moved = False 196 shared = SharedObjects.get() 197 factory = FlagFactory.get() 198 199 if materials is None: 200 materials = [] 201 elif not isinstance(materials, list): 202 # In case they passed a tuple or whatnot. 203 materials = list(materials) 204 if not touchable: 205 materials = [factory.no_hit_material] + materials 206 207 finalmaterials = [ 208 shared.object_material, 209 factory.flagmaterial, 210 ] + materials 211 self.node = bs.newnode( 212 'flag', 213 attrs={ 214 'position': (position[0], position[1] + 0.75, position[2]), 215 'color_texture': factory.flag_texture, 216 'color': color, 217 'materials': finalmaterials, 218 }, 219 delegate=self, 220 ) 221 222 if dropped_timeout is not None: 223 dropped_timeout = int(dropped_timeout) 224 self._dropped_timeout = dropped_timeout 225 self._counter: bs.Node | None 226 if self._dropped_timeout is not None: 227 self._count = self._dropped_timeout 228 self._tick_timer = bs.Timer( 229 1.0, call=bs.WeakCall(self._tick), repeat=True 230 ) 231 self._counter = bs.newnode( 232 'text', 233 owner=self.node, 234 attrs={ 235 'in_world': True, 236 'color': (1, 1, 1, 0.7), 237 'scale': 0.015, 238 'shadow': 0.5, 239 'flatness': 1.0, 240 'h_align': 'center', 241 }, 242 ) 243 else: 244 self._counter = None 245 246 self._held_count = 0 247 self._score_text: bs.Node | None = None 248 self._score_text_hide_timer: bs.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 bs.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 = bs.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 = bs.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 = bs.safecolor(self.node.color) 328 bs.animate(self._score_text, 'scale', {0: start_scale, 0.2: 0.02}) 329 self._score_text_hide_timer = bs.Timer( 330 1.0, bs.WeakCall(self._hide_score_text) 331 )
Show a message over the flag; handy for scores.
333 @override 334 def handlemessage(self, msg: Any) -> Any: 335 assert not self.expired 336 if isinstance(msg, bs.DieMessage): 337 if self.node: 338 self.node.delete() 339 if not msg.immediate: 340 self.activity.handlemessage(FlagDiedMessage(self)) 341 elif isinstance(msg, bs.HitMessage): 342 assert self.node 343 assert msg.force_direction is not None 344 self.node.handlemessage( 345 'impulse', 346 msg.pos[0], 347 msg.pos[1], 348 msg.pos[2], 349 msg.velocity[0], 350 msg.velocity[1], 351 msg.velocity[2], 352 msg.magnitude, 353 msg.velocity_magnitude, 354 msg.radius, 355 0, 356 msg.force_direction[0], 357 msg.force_direction[1], 358 msg.force_direction[2], 359 ) 360 elif isinstance(msg, bs.PickedUpMessage): 361 self._held_count += 1 362 if self._held_count == 1 and self._counter is not None: 363 self._counter.text = '' 364 self.activity.handlemessage(FlagPickedUpMessage(self, msg.node)) 365 elif isinstance(msg, bs.DroppedMessage): 366 self._held_count -= 1 367 if self._held_count < 0: 368 print('Flag held count < 0.') 369 self._held_count = 0 370 self.activity.handlemessage(FlagDroppedMessage(self, msg.node)) 371 else: 372 super().handlemessage(msg)
General message handling; can be passed any message object.
374 @staticmethod 375 def project_stand(pos: Sequence[float]) -> None: 376 """Project a flag-stand onto the ground at the given position. 377 378 Useful for games such as capture-the-flag to show where a 379 movable flag originated from. 380 """ 381 assert len(pos) == 3 382 bs.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
- bascenev1._actor.Actor
- autoretain
- on_expire
- expired
- exists
- is_alive
- activity
- getactivity