/** * A Doll is a created in two ways : * 1) By a Launcher when solving a regular level * 2) Added to the scene from within the SceneEditor. * The later is usually done from a [Victory] scene, i.e. the Doll's House, but Dolls could also be added to * regular [Play] levels too. * * No matter how Dolls are created, the Doll itself is made up of many [DollPart]s, such as a head, torso, arms, legs. * The Doll itself isn't visible (only the DollParts are visible). * The DollParts are joined together using Tickle's built in JBox2d physics engine. */ class Doll : ActionRole(), Reward, Scalable { @Attribute( about="Which scene must be completed to see this Doll in a Doll's House?" ) var rewardForScene: String = "" override fun rewardForScene() = rewardForScene /* Multiple Doll types can be launched from a single launcher. This allows each Doll type to scale to the same size. A Launcher also has a 'scale' attribute, allowing it to always launch smaller or larger dolls than their sizes. */ @CostumeAttribute( about="The scaling factor to make this Doll the 'normal' size" ) var defaultScale = 1.0 // Sum of the mass of all the body parts. var totalMass = 0f // The actors for head, arms etc. val parts = listOf() /** * Used by the [hit] method to ignore some hits. */ var ignoreHitTick = 0L // Set then the doll is is killed (e.g. by Plasma). i.e. ending=true while the doll parts are fading. var ending = false var torso : Actor var abdomen : Actor // The time in seconds to fade. Rain sets this to much longer. var fadeTime = 2.0 override fun activated() { // [hit] will ignore the large change in velocity when the doll is first launched. ignoreHitTick = Game.instance.gameLoop.tickCount // The torso is created wherever the "head" is placed in the scene editor, // or where the launcher is. // The head, arms and torso are attached to it, and the legs attached to the torso. // Note that the [DollPart.offset] is always relative to the [Doll]'s starting position. // (previously they were relative to the part they were attached to. // The offset of the torso is ignored. // NOTE. arm-left and leg-left are from the camera's point of view, not the doll's left limb. torso = createPart("torso", 0.0, null) createPart("head", 0.2, torso) abdomen = createPart("abdomen", 0.1, torso) createPart("arm-left", -0.1, torso) createPart("arm-right", -0.2, torso) createPart("leg-left", -0.3, abdomen) createPart("leg-right", -0.4, abdomen) // Adjust the mass based on the scale (the masses of JBox2d haven't been scaled yet). // At some point I may "fix" tickle, in which case, this line will need to be removed. totalMass *= actor.scale.x * actor.scale.y super.activated() } fun createPart(part: String, deltaZOrder: float, joinTo: Actor): Actor { val newActor = actor.createChild(part) newActor.zOrder = actor.zOrder + deltaZOrder val newRole = newActor.role as DollPart newRole.doll = this newRole.offset *= actor.scale newActor.scale.set( actor.scale ) if (joinTo != null) { newActor.position.set( joinTo.position + newRole.offset ) val joint = TicklePinJoint(joinTo, newActor, newRole.actor.position ) joint.limitRotation(newRole.fromAngle, newRole.toAngle) } totalMass += newActor.body.mass parts.add(newActor) return newActor } override fun scale(scaleBy: Vector2) { actor.scale.set( scaleBy ) for (part in parts) { part.scale *= scaleBy part.body.scaleJoints( scaleBy ) } totalMass *= (scaleBy.x * scaleBy.y) } override fun end() { super.end() // Kill the parts (legs, arms etc) which make up this Doll. for (part in parts) { part.die() } } fun fadeAndDie() { if (ending) return ending = true val fades = ParallelAction() for (part in parts) { fades.add( Fade(part.color, fadeTime, 0f) ) } replaceAction( fades then Kill(actor) ) // NOTE, we cannot call explode NOW, because if this was called from a ContactListenerRole such as Plasma, // then JBox2D will be locked. Therefore we can't destroy joints yet. Game.instance.scene.actions.addOnce( this:>explode ) } fun explode() { for (part in parts) { // Prevent all of the parts from colliding with each other. val fixtures = part.body.fixtures() while (fixtures.hasNext() ) { fixtures.next().filterGroup = -1 } // Detach the body parts by destroying the joints for (joint in part.body.joints()) { if (joint.actorA === torso && joint.actorB === abdomen) { // Do not disconnect } else { joint.destroy() } } } } /** * Called from a DollPart when the part has substantially changed velocity (presumably because it has hit another * object). * Note, the sounds must be added to the [Doll]'s events, not the [DollPart] or [MajorDollPart]. * i.e. added to Annie, Mike, Fiona, etc. */ fun hit(deltaV: double) { val now = Game.instance.gameLoop.tickCount // Has a hit already occurred recently (the last 10 ticks) for this doll? Then ignore the hit. // This lets the head, body and torso all call hit, and only the first will cause a sound effect. if (now - ignoreHitTick > 10) { DollsHouseDirector.instance.onDollHit( this, deltaV > 200.0 ) } ignoreHitTick = now } fun zapped() { if (!ending) { actor.event("hitHard") fadeAndDie() DollsHouseDirector.instance.onDollZapped( this ) } } }