class Play : AbstractDirector() { // Each scene can set the magnitude of the gravity. // The direction will depend on which chapter (i.e. upwards for chapter "Total Inversion" // Beware that chapter 2 also increases the gravity! @Attribute var gravity = 0.05 // In chapter one, both of these will be [0, -gravity], but in later chapters, things get weird. // Gravity for the ship can differ compared to everything else, and gravity doesn't always point downwards! val shipGravity = Vector2() val cargoGravity = Vector2() // The amount of water needed to be sent through the gate to complete the scene. @Attribute var waterRequired = 0 // The amount of gold needed to be sent through the gate to complete the scene. var goldRequired = 0 // The bounds for scrolling. Set by BottomLeft and TopRight roles. val bottomLeft = Vector2() val topRight = Vector2() var backgroundStage : Stage // Just background images var dynamicStage : Stage // The ship, ball, rod etc var rocksStage : Stage // Deadly solid objects (highest z-order) var glassStage : Stage // A non-scrolling view above all others. var backgroundView : StageView var dynamicView : StageView var rocksView : StageView var glassView : StageView static var instance : Play = null var restart : Input var pause : Input var quit : Input var cheatNextLevel : Input var ship : Ship = null var dome : Dome = null var waterIcons : List = null var goldIcons : List = null // An action, which alters gravity over time, use in chapter 3 "Phase Shift" var changeGravityAction = ActionHolder() // An action, which rotates the view - it doesn't affect game play. var nausiaAction = ActionHolder() override fun sceneLoaded() { super.sceneLoaded() val game = Game.instance val scene = game.scene val inputs = game.resources.inputs restart = inputs.find( "restart" ) pause = inputs.find("pause" ) quit = inputs.find("quit") cheatNextLevel = inputs.find("cheatNextLevel") backgroundStage = scene.findStage( "background" ) dynamicStage = scene.findStage( "dynamic" ) rocksStage = scene.findStage( "rocks" ) glassStage = scene.findStage( "glass" ) backgroundView = backgroundStage.firstView() dynamicView = dynamicStage.firstView() rocksView = rocksStage.firstView() glassView = glassStage.firstView() shipGravity.set( 0, -gravity) cargoGravity.set( 0, -gravity) val instalment = HiggsAnomaly.instance.instalment if (instalment == 2) { // High G shipGravity *= 1.6 cargoGravity *= 1.6 } else if (instalment == 3 ) { // Phase Shift changeGravityAction.replaceAction( createGravityAction( 30, 30 ) ) } else if (instalment == 4 ) { // Inversion shipGravity *= -1.2 } else if (instalment == 5 ) { // Total Inversion shipGravity *= -1 cargoGravity *= -1 } else if (instalment == 6 ) { // Phase Shift changeGravityAction.replaceAction( createGravityAction( 50, 60 ) ) } } override fun begin() { backgroundView.centerView() dynamicView.centerView() rocksView.centerView() nausiaAction.replaceAction( createNausiaAction() ) instance = this } override fun activated() { waterIcons = createIcons( "waterIcon", waterRequired, 0, 0.5 ) goldIcons = createIcons( "goldIcon", goldRequired, if (waterRequired == 0) 0 else 1, 0.8 ) checkCargo() } override fun tick() { changeGravityAction.act() nausiaAction.act() } override fun onKey( event : KeyEvent ) { if ( pause.matches( event ) ) { Game.instance.paused = ! Game.instance.paused } if ( restart.matches( event ) && ship == null ) { startScene( Game.instance.sceneName ) } if (quit.matches(event)) { startScene("mainMenu") } if (cheatNextLevel.matches(event)) { for( actor in rocksStage.actors ) { if (actor.role is ExitGate ) { val exitGate = actor.role as ExitGate Game.instance.startScene( exitGate.nextScene ) return } } } } fun createNausiaAction() : Action { val nausia = Options.instance.nausia val direction = Angle() backgroundView.direction = direction dynamicView.direction = direction rocksView.direction = direction if (nausia == 1) { return nausia(direction, 5, 10 ).forever() } else if (nausia == 2) { return nausia(direction, 10, 90 ).forever() } else if (nausia == 3) { return nausia(direction, 5, 90, 18 ).forever() } else if (nausia == 4) { return TurnBy(direction, 5, Angle.degrees(90) ).forever() } else { return null } } fun nausia( direction : Angle, seconds : double, degrees : double ) : Action { return nausia( direction, seconds, degrees, 0 ) } fun nausia( direction : Angle, seconds : double, degrees : double, extra : double ) : Action { return TurnBy( direction, seconds, Angle.degrees(degrees), Eases.easeOut ) .then( TurnBy( direction, seconds, Angle.degrees(-degrees - extra/2), Eases.easeIn ) ) .then( TurnBy( direction, seconds, Angle.degrees(-degrees - extra/2), Eases.easeOut ) ) .then( TurnBy( direction, seconds, Angle.degrees(degrees), Eases.easeIn ) ) } // Creates icons, which indicate how many more pieces of cargo need to be shipped to complete the scene. fun createIcons( costumeName : String, count : int, line : int, scale : double ) : List { val result = listOf() val costume = Game.instance.resources.costumes.find(costumeName) for ( i in 0 until count ) { val actor = costume.createActor() actor.scaleXY = scale val cargoIcon = actor.role as CargoIcon cargoIcon.index = i cargoIcon.line = line actor.stage = glassStage result.add( cargoIcon ) } return result } fun isOutsideDome( role : Role ) : bool { return dome != null && dome.isOutside( role.actor ) } fun overlappingSomethingDeadly( actor : Actor ) : Actor { if (dome != null && dome.isOutside( actor ) ) return dome.actor for (rock in rocksStage.actors) { if ( rock !== actor && PixelOverlapping.instance.overlapping( actor, rock ) ) { return rock } } return null } fun findNearbyCargo( shipActor: Actor, maxDistance : double ) : Cargo { var nearest : Cargo = null var nearestDistance = maxDistance for ( cargo in rocksStage.findRolesByClass(Cargo)) { val dist = cargo.actor.position.distance( shipActor.position ) if ( dist < nearestDistance ) { nearest = cargo nearestDistance = dist } } return nearest } fun scrollTowards( position : Vector2 ) { backgroundView.worldFocal.set( position ) // constraining the scroll position doesn't work, when "nausia" option is one, // so I've just disabled the constraint. if ( Options.instance.nausia == 0 ) { if (backgroundView.worldFocal.x < bottomLeft.x) backgroundView.worldFocal.x = bottomLeft.x if (backgroundView.worldFocal.y < bottomLeft.y) backgroundView.worldFocal.y = bottomLeft.y if (backgroundView.worldFocal.x > topRight.x) backgroundView.worldFocal.x = topRight.x if (backgroundView.worldFocal.y > topRight.y) backgroundView.worldFocal.y = topRight.y } dynamicView.worldFocal.set( backgroundView.worldFocal ) rocksView.worldFocal.set( backgroundView.worldFocal ) } fun explode( actor: Actor ) { actor.event("explode") // Ensure that the fragments don't kill the ship (we don't want it to be on the rocksStage). actor.stage = dynamicStage createFragmentActions(actor, this:>fragmentAction ) actor.die() if ( actor.role === ship ) { ship = null } if ( ship != null ) { if (actor.role is WaterCargo && Options.instance.isWaterVital) { ship.explode() } if (actor.role is Gold && Options.instance.isGoldVital) { ship.explode() } } } /** Creates an Action for the fragments made by FragmentMaker. See explode() above. */ fun fragmentAction( fragment : Actor, offset : Vector2 ) : Action { val seconds = 5 return ( MoveBy(fragment.position,seconds, offset * 30) and TurnBy(fragment.direction, seconds, Angle.degrees(Rand.between( -1000, 1000 ))) and Fade(fragment.color, seconds, 0f, Eases.easeInQuad) ) then Kill(fragment) } fun sentCargo( cargo : Cargo ) { println( "Sent $cargo" ) if (cargo is Gold) { println( "Gold" ) if ( goldRequired > 0 ) { goldRequired-- println( "Now $goldRequired" ) goldIcons.remove( goldIcons.size() - 1 ).remove() checkCargo() } } else if (cargo is WaterCargo) { val waterCargo = cargo as WaterCargo if (waterCargo.isFull) { if ( waterRequired > 0 ) { waterRequired-- waterIcons.remove( waterIcons.size() - 1 ).remove() checkCargo() } } } } fun checkCargo() { println( "Water $waterRequired Gold $goldRequired" ) if ( waterRequired == 0 && goldRequired == 0 ) { for( exitGate in rocksStage.findRolesByClass( ExitGate ) ) { exitGate.open() } for ( gate in rocksStage.findRolesByClass( Gate ) ) { gate.close() } showInfo("gatesOpen") } } /** Show roles of type Info where Info.event == event This can be be used to display messages. For example, in the tutorial, we can show a message when the exit gate is open (i.e. when all of the Cargo have been collected) See Play.checkCargo(). */ fun showInfo( event : String ) { for (actor in glassStage.actors) { if (actor.role is Info) { val info = actor.role as Info if (info.event == event) { info.show() } } } } fun shipDestroyed() { ship = null showInfo( "died" ) } val gravityDirection = Angle.degrees(-90) // Changes gravity over time fun createGravityAction(period : double, swing : double) : Action { val turns = TurnTo( gravityDirection, period/4, Angle.degrees(-90 + swing), Eases.easeIn ) .then( TurnTo( gravityDirection, period/4, Angle.degrees(-90), Eases.easeOut ) ) .then( TurnTo( gravityDirection, period/4, Angle.degrees(-90 - swing), Eases.easeIn ) ) .then( TurnTo( gravityDirection, period/4, Angle.degrees(-90), Eases.easeOut ) ) val result = turns.forever() and this:>changeGravity.action() turns.begin() return result } fun changeGravity() : bool { shipGravity.set( gravityDirection.vector() ) shipGravity *= gravity cargoGravity.set( shipGravity ) return false } } class DyingFragmentRoleFactory(var seconds: double) : FragmentRoleFactory { override fun create( actor : Actor, offset : Vector2 ) : Role { val action = Move(actor.position, offset * 5) .and( TurnBy(actor.direction, 1.0, Angle.degrees(Rand.between( -100, 100 ))).forever()) .whilst( Fade(actor.color, seconds, 0f, Eases.easeInQuad)) .then(Kill(actor)) return ActionRole( action ) } }