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 9 10from bascenev1lib.gameutils import SharedObjects 11import bascenev1 as bs 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: bs.Material 27 """The bs.Material applied to all `Flag`s.""" 28 29 impact_sound: bs.Sound 30 """The bs.Sound used when a `Flag` hits the ground.""" 31 32 skid_sound: bs.Sound 33 """The bs.Sound used when a `Flag` skids along the ground.""" 34 35 no_hit_material: bs.Material 36 """A bs.Material that prevents contact with most objects; 37 applied to 'non-touchable' flags.""" 38 39 flag_texture: bs.Texture 40 """The bs.Texture for flags.""" 41 42 _STORENAME = bs.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 = bs.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 = bs.getsound('metalHit') 73 self.skid_sound = bs.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 = bs.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 = bs.gettexture('flagColor') 109 110 @classmethod 111 def get(cls) -> FlagFactory: 112 """Get/create a shared `FlagFactory` instance.""" 113 activity = bs.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: bs.Node 133 """The bs.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: bs.Node 158 """The bs.Node that was holding it.""" 159 160 161class Flag(bs.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[bs.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 `bs.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 = bs.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: bs.Node | None 224 if self._dropped_timeout is not None: 225 self._count = self._dropped_timeout 226 self._tick_timer = bs.Timer( 227 1.0, call=bs.WeakCall(self._tick), repeat=True 228 ) 229 self._counter = bs.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: bs.Node | None = None 246 self._score_text_hide_timer: bs.Timer | None = None 247 248 def _tick(self) -> None: 249 if self.node: 250 # Grab our initial position after one tick (in case we fall). 251 if self._initial_position is None: 252 self._initial_position = self.node.position 253 254 # Keep track of when we first move; we don't count down 255 # until then. 256 if not self._has_moved: 257 nodepos = self.node.position 258 if ( 259 max( 260 abs(nodepos[i] - self._initial_position[i]) 261 for i in list(range(3)) 262 ) 263 > 1.0 264 ): 265 self._has_moved = True 266 267 if self._held_count > 0 or not self._has_moved: 268 assert self._dropped_timeout is not None 269 assert self._counter 270 self._count = self._dropped_timeout 271 self._counter.text = '' 272 else: 273 self._count -= 1 274 if self._count <= 10: 275 nodepos = self.node.position 276 assert self._counter 277 self._counter.position = ( 278 nodepos[0], 279 nodepos[1] + 1.3, 280 nodepos[2], 281 ) 282 self._counter.text = str(self._count) 283 if self._count < 1: 284 self.handlemessage(bs.DieMessage()) 285 else: 286 assert self._counter 287 self._counter.text = '' 288 289 def _hide_score_text(self) -> None: 290 assert self._score_text is not None 291 assert isinstance(self._score_text.scale, float) 292 bs.animate( 293 self._score_text, 'scale', {0: self._score_text.scale, 0.2: 0} 294 ) 295 296 def set_score_text(self, text: str) -> None: 297 """Show a message over the flag; handy for scores.""" 298 if not self.node: 299 return 300 if not self._score_text: 301 start_scale = 0.0 302 math = bs.newnode( 303 'math', 304 owner=self.node, 305 attrs={'input1': (0, 1.4, 0), 'operation': 'add'}, 306 ) 307 self.node.connectattr('position', math, 'input2') 308 self._score_text = bs.newnode( 309 'text', 310 owner=self.node, 311 attrs={ 312 'text': text, 313 'in_world': True, 314 'scale': 0.02, 315 'shadow': 0.5, 316 'flatness': 1.0, 317 'h_align': 'center', 318 }, 319 ) 320 math.connectattr('output', self._score_text, 'position') 321 else: 322 assert isinstance(self._score_text.scale, float) 323 start_scale = self._score_text.scale 324 self._score_text.text = text 325 self._score_text.color = bs.safecolor(self.node.color) 326 bs.animate(self._score_text, 'scale', {0: start_scale, 0.2: 0.02}) 327 self._score_text_hide_timer = bs.Timer( 328 1.0, bs.WeakCall(self._hide_score_text) 329 ) 330 331 def handlemessage(self, msg: Any) -> Any: 332 assert not self.expired 333 if isinstance(msg, bs.DieMessage): 334 if self.node: 335 self.node.delete() 336 if not msg.immediate: 337 self.activity.handlemessage(FlagDiedMessage(self)) 338 elif isinstance(msg, bs.HitMessage): 339 assert self.node 340 assert msg.force_direction is not None 341 self.node.handlemessage( 342 'impulse', 343 msg.pos[0], 344 msg.pos[1], 345 msg.pos[2], 346 msg.velocity[0], 347 msg.velocity[1], 348 msg.velocity[2], 349 msg.magnitude, 350 msg.velocity_magnitude, 351 msg.radius, 352 0, 353 msg.force_direction[0], 354 msg.force_direction[1], 355 msg.force_direction[2], 356 ) 357 elif isinstance(msg, bs.PickedUpMessage): 358 self._held_count += 1 359 if self._held_count == 1 and self._counter is not None: 360 self._counter.text = '' 361 self.activity.handlemessage(FlagPickedUpMessage(self, msg.node)) 362 elif isinstance(msg, bs.DroppedMessage): 363 self._held_count -= 1 364 if self._held_count < 0: 365 print('Flag held count < 0.') 366 self._held_count = 0 367 self.activity.handlemessage(FlagDroppedMessage(self, msg.node)) 368 else: 369 super().handlemessage(msg) 370 371 @staticmethod 372 def project_stand(pos: Sequence[float]) -> None: 373 """Project a flag-stand onto the ground at the given position. 374 375 Useful for games such as capture-the-flag to show where a 376 movable flag originated from. 377 """ 378 assert len(pos) == 3 379 bs.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: 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
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 = 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')
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.
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
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: bs.Node 134 """The bs.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: bs.Node 159 """The bs.Node that was holding it."""
A message saying a Flag
has been dropped.
Category: Message Classes
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 def handlemessage(self, msg: Any) -> Any: 333 assert not self.expired 334 if isinstance(msg, bs.DieMessage): 335 if self.node: 336 self.node.delete() 337 if not msg.immediate: 338 self.activity.handlemessage(FlagDiedMessage(self)) 339 elif isinstance(msg, bs.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, bs.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, bs.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 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.
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
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.
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 )
Show a message over the flag; handy for scores.
332 def handlemessage(self, msg: Any) -> Any: 333 assert not self.expired 334 if isinstance(msg, bs.DieMessage): 335 if self.node: 336 self.node.delete() 337 if not msg.immediate: 338 self.activity.handlemessage(FlagDiedMessage(self)) 339 elif isinstance(msg, bs.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, bs.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, bs.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)
General message handling; can be passed any message object.
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 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