/** * The base class of regular [Launcher] as well as the [AutoLauncher] used in the menu. * * Each doll is given a zOrder incremented by 1, and then resets to back to 1 when it reaches 100. * This ensures that each doll is either "above" or "below" other dolls. * Note that each DollPart has a zOrder difference in the range -0.1 to 0.1, so that * each doll part is ordered correctly. If a scene allows more than 100 dolls, then is it possible for * dolls to overlap strangely. Not a big deal though! * * All objects that must be draw on TOP of the dolls must have zOrder > 100 and those below the dolls * must have zOrder < 0. * * Note, this is a global, so that all Launchers use the same counter. */ abstract class AbstractLauncher : AbstractRole() { /** Each doll launcher is given a unique zOrder, so that there is consistant ordering of the dolls. Reset to 50 at the beginning of a scene. See [DollsHouseDirector.begin] */ static var dollZOrder = 50.0 // Not sure what the units are. Maybe world units per second? @Attribute( about="Indicates the speed of the dolls which are launched" ) var speed: float = 600 // Used to find Costumes. The Costume's Role must be of type Doll. @Attribute( about="Named of the dolls to be launched, separated by commas" ) var dollName: String = "annie" @Attribute( about="For predictable choice of dolls, and initial spin, pick any non-zero value" ) var randomSeed = 0 // 'normal' size is the size of a doll after Doll.defaultScale is applied. // Therefore the size of each doll part is Doll.defaultScale * this.scale @Attribute( about="How much to scale the launched dolls compared to their 'normal' size" ) var scale = 1.0 // Prevents the world being filled up with dolls lying around! @Attribute( about="When more dolls are launched, the oldest fade away and die" ) var maxDolls = 5 // dollNames are converted into Costume's whose Role must be of type Doll. val dollCostumes = listOf() /** * When the maximum number of dolls is exceeded, the earliest doll is killed * and removed from the list. A new Doll is added to the list when a Launcher creates a Doll. */ val dolls = listOf() // Uses [randomSeed], so we can have predictable outcomes, rather than acutal randomness. var random: RandomFactory override fun activated() { super.activated() random = RandomFactory(randomSeed.toLong()) for (it in dollName.split(",") ) { var name = it.trim() // Use the FrankenDolls if that option is set ;-) if ( Options.instance.useFrankenDolls ) { if (name == "bony" ) { name = "boxie" } else if ( name == "santa" ) { name = "semma" } else if ( name == "asul" || name == "eesha" || name == "jishna" ) { name = "shah" } else if ( name == "foxie" || name == "annie" ) { name = "fannie" } else if ( name == "mike" || name == "fiona" ) { name = "fike" } else { name = "monster" } } val dollCostume = Game.instance.resources.costumes.find(name) if (dollCostume != null) { dollCostumes.add(dollCostume) } } if (dollCostumes.isEmpty()) { dollCostumes.add(Game.instance.resources.costumes.find("annie")) } } fun launch(point: Vector2) { if (!clearToLaunch()) { return } val costume = random.listItem(dollCostumes) val dollA = costume.createActor() val role = dollA.role if (role is Doll) { val doll = role as Doll dollA.position.set(actor.position) dollA.scaleXY = scale * doll.defaultScale dollA.zOrder = dollZOrder dollZOrder++ if (dollZOrder > 99) { dollZOrder = 1.0 } actor.stage.add(dollA) val direction = Vector2(point) - actor.position val magnitude : float = Math.min(direction.length(), speed) / 1.0 // Used below to create a rotation of the doll. val shear = random.between(-magnitude * 1.0, magnitude * 1.0) val initialVelocity = direction.magnitude( magnitude ) val shearedVelocity = Vector2() val headMass = doll.parts[1].body.mass // Give each doll part an initial velocity, but also cause a rotation (by adding a // shear to the head and subtracting it from the abdomen and legs. for( index, part in doll.parts ) { val tickleBody = part.body if (index == 1) { // The head // Move the head in one direction shearedVelocity.set( initialVelocity.plus(shear, 0.0) ) } else if (index > 3) { // Abdomen and legs // Move the abdomen and legs in the opposite direction, // Causing an overall rotation of the Doll. shearedVelocity.set( initialVelocity.minus(shear * headMass / tickleBody.mass / 3, 0.0 ) ) // If I've got the maths correct, this shouldn't change the direction of the doll, as the // change in momentum is balanced between the head and these 3 parts (abdomen and legs) } else { shearedVelocity.set(initialVelocity) } tickleBody.setLinearVelocity(shearedVelocity) } dolls.add(doll) if (dolls.size() > maxDolls) { dolls.remove(0).fadeAndDie() } if ( Game.instance.director is Play ) { doll.replaceAction( Delay( 8 ) thenOnce doll:>fadeAndDie ) } } } fun launched(doll: Doll) {} /** * Cannot launch if there is a doll close to the launcher */ fun clearToLaunch(): bool { val distance = Vector2() for (dollPart in actor.stage.findRolesByClass(DollPart) ) { distance.set ( actor.position - dollPart.actor.position ) if (distance.length() < 100.0) { return false } } return true } } class Launcher : AbstractLauncher() { /** * The number of this launcher. Set by Play director when the scene begins. */ var number = 0 /** * The key to select this Launcher (Will be key 1..n) */ var select: Input override fun activated() { super.activated() select = Game.instance.resources.inputs.find("select$number") if (Play.instance.launcher === this) { actor.event("select$number") } else { actor.event("deselect$number") } } override fun tick() { if (select.isPressed() == true && Play.instance.launcher !== this) { select() } } /** Make this the "active" launcher, also calls deselect on the previously active launcher. */ fun select() { if ( Play.instance.launcher != this) { for( costume in dollCostumes ) { val sound = costume.chooseSound("name") if (sound != null) sound.play() } actor.event("select$number") } if (Play.instance.launcher != null) { Play.instance.launcher.deselect() } Play.instance.launcher = this } fun deselect() { actor.event("deselect$number") } override fun launched(doll: Doll) { doll.actor.event("name") } } class AutoLauncher : AbstractLauncher() { @Attribute var period = 2.0 @Attribute( isPosition=true ) val point = Vector2() var action: Action override fun activated() { super.activated() action = ( Delay(period) thenOnce this:>launch.curry(point) ).forever() action.begin() } override fun tick() { action.act() } }