import funkin.play.character.MultiSparrowCharacter; import funkin.play.character.CharacterType; import funkin.play.PlayState; import funkin.play.GameOverSubState; import funkin.graphics.adobeanimate.FlxAtlasSprite; import flixel.util.FlxTimer; import funkin.graphics.FunkinSprite; import funkin.audio.FunkinSound; import funkin.Paths; import flixel.FlxSprite; import flixel.FlxG; import funkin.modding.base.ScriptedFunkinSprite; import flixel.group.FlxTypedSpriteGroup; import flixel.effects.FlxFlicker; import funkin.play.PauseSubState; import funkin.modding.module.ModuleHandler; import flixel.tweens.FlxTween; class PicoPlayerCharacter extends MultiSparrowCharacter { function new() { super('pico-playable'); } function onCreate(event:ScriptEvent) { super.onCreate(event); // NOTE: this.x and this.y are not properly set here. GameOverSubState.musicSuffix = '-pico'; GameOverSubState.blueBallSuffix = '-pico'; PauseSubState.musicSuffix = '-pico'; } var deathSpriteRetry:FunkinSprite; var deathSpriteNene:FunkinSprite; var picoFade:FunkinSprite; /** * Initialize and cache sprites used for the death animation, * for use later. */ function createDeathSprites() { deathSpriteRetry = FunkinSprite.createSparrow(0, 0, "characters/Pico_Death_Retry"); deathSpriteRetry.animation.addByPrefix('idle', "Retry Text Loop0", 24, true); deathSpriteRetry.animation.addByPrefix('confirm', "Retry Text Confirm0", 24, false); deathSpriteRetry.visible = false; //FlxG.debugger.track(deathSpriteRetry); deathSpriteNene = FunkinSprite.createSparrow(0, 0, "characters/NeneKnifeToss"); var gf = PlayState.instance.currentStage.getGirlfriend(); deathSpriteNene.x = gf.originalPosition.x + 280; deathSpriteNene.y = gf.originalPosition.y + 70; deathSpriteNene.animation.addByPrefix('throw', "knife toss0", 24, false); deathSpriteNene.visible = true; deathSpriteNene.animation.finishCallback = function(name:String) { deathSpriteNene.visible = false; } } function onNoteHit(event:HitNoteScriptEvent) { if (event.eventCanceled) { // onNoteHit event was cancelled by the gameplay module. return; } if (event.note.noteData.getMustHitNote() && characterType == CharacterType.BF) { // Override the hit note animation. switch(event.note.kind) { case "weekend-1-cockgun": // HE'S PULLING HIS COCK OUT holdTimer = 0; playCockGunAnim(); case "weekend-1-firegun": holdTimer = 0; playFireGunAnim(); default: super.onNoteHit(event); } } } function onNoteMiss(event:NoteScriptEvent) { // Override the miss note animation. switch(event.note.kind) { case "weekend-1-cockgun": //playCockMissAnim(); case "weekend-1-firegun": playCanExplodeAnim(); default: super.onNoteMiss(event); } } function playAnimation(name:String, restart:Bool, ignoreOther:Bool) { if (name == "firstDeath") { if (GameOverSubState.blueBallSuffix == '-pico-explode') { // Explosion death animation. doExplosionDeath(); } else { // Standard death animation. createDeathSprites(); GameOverSubState.instance.add(deathSpriteRetry); GameOverSubState.instance.add(deathSpriteNene); deathSpriteNene.animation.play("throw"); } } else if (name == "deathConfirm") { if (picoDeathExplosion != null) { doExplosionConfirm(); } else { deathSpriteRetry.animation.play('confirm'); // I think the glow makes the overall animation larger, // but a plain FlxSprite doesn't have an animation offset option so we do it manually. deathSpriteRetry.x -= 250; deathSpriteRetry.y -= 200; // Skip playing the animation. return; } } super.playAnimation(name, restart, ignoreOther); } var picoFlicker:FlxFlicker = null; override function onAnimationFinished(name:String) { super.onAnimationFinished(name); if (name == 'shootMISS' && PlayState.instance.health > 0.0 && !PlayState.instance.isPlayerDying) { // ERIC: You have to use super instead of this or it breaks. // This is because typeof(this) is PolymodAbstractClass. picoFlicker = FlxFlicker.flicker(super, 1, 1 / 30, true, true, function(_) { picoFlicker = FlxFlicker.flicker(super, 0.5, 1 / 60, true, true, function(_) { picoFlicker = null; }); }); } } public override function onPause(event:PauseScriptEvent) { super.onPause(event); if (picoFlicker != null) { picoFlicker.pause(); this.visible = true; } } public override function onResume(event:ScriptEvent) { super.onResume(event); if (picoFlicker != null) { picoFlicker.resume(); } } public override function getDeathCameraOffsets():Array { var result = super.getDeathCameraOffsets(); if (GameOverSubState.blueBallSuffix == '-pico-explode') { return [result[0], result[1] + 100]; } return [result[0], result[1]]; } var picoDeathExplosion:FlxAtlasSprite; function doExplosionDeath() { if (picoFlicker != null) { picoFlicker.stop(); // this sets visible to true, but we make it false a few lines down anyways } // Suffixed death sound will already play. GameOverSubState.instance.resetCameraZoom(); // Move the camera up. GameOverSubState.instance.cameraFollowPoint.y -= 100; var picoDeathExplosionPath = Paths.animateAtlas("characters/picoExplosionDeath", "weekend1"); picoDeathExplosion = new FlxAtlasSprite(this.x - 640, this.y - 340, picoDeathExplosionPath); PlayState.instance.subState.add(picoDeathExplosion); picoDeathExplosion.zIndex = 1000; picoDeathExplosion.onAnimationFinish.add(onExplosionFinishAnim); picoDeathExplosion.visible = true; this.visible = false; new FlxTimer().start(3.0, afterPicoDeathExplosionIntro); picoDeathExplosion.playAnimation('intro'); } var singed:FunkinSound; function afterPicoDeathExplosionIntro(timer:FlxTimer) { // Start the (standard) death music, 3.5 seconds after the explosion starts, // not when the explosion sound finishes or when the loop starts. GameOverSubState.instance.startDeathMusic(1.0, false); singed = FunkinSound.load(Paths.sound('singed_loop'), true, false, true); // singed.fadeIn(0.5, 0.3, 1.0); } function doExplosionConfirm() { // Suffixed confirm music will already play. picoDeathExplosion.playAnimation('Confirm'); if (singed != null) { singed.stop(); singed = null; } } function onExplosionFinishAnim(animLabel:String) { if (animLabel == 'intro') { picoDeathExplosion.playAnimation('Loop Start', true, false, true); } else if (animLabel == 'Confirm') { // Do nothing, the animation will just play. } } override function onGameOver(event:ScriptEvent):Void { super.onGameOver(event); } override function onSongRetry(event:ScriptEvent):Void { super.onSongRetry(event); // Don't let these pile up. clearCasings(); // Reset to standard death animation. GameOverSubState.musicSuffix = '-pico'; GameOverSubState.blueBallSuffix = '-pico'; PauseSubState.musicSuffix = '-pico'; picoDeathExplosion = null; this.visible = true; } function onAnimationFrame(name:String, frameNumber:Int, frameIndex:Int) { super.onAnimationFrame(name, frameNumber, frameIndex); if (name == "firstDeath" && frameNumber == 36 - 1) { deathSpriteRetry.animation.play('idle'); deathSpriteRetry.visible = true; GameOverSubState.instance.startDeathMusic(1.0, false); // force the deathloop to play in here, since we are starting the music early it // doesn't check this in gameover substate ! // also no animation suffix 🤔 GameOverSubState.instance.boyfriend.playAnimation('deathLoop'); deathSpriteRetry.x = this.x + 416; deathSpriteRetry.y = this.y + 42; } if (name == "cock" && frameNumber == 3) { createCasing(); } } var casingGroup:FlxTypedSpriteGroup; function createCasing() { if (casingGroup == null) { casingGroup = new FlxTypedSpriteGroup(); casingGroup.x = this.x + 250; casingGroup.y = this.y + 100; casingGroup.zIndex = 1000; addToStage(casingGroup); } var casing = ScriptedFunkinSprite.init('CasingSprite', 0, 0); if (casing != null) casingGroup.add(casing); } function clearCasings() { // Clear the casing group. if (casingGroup != null) { casingGroup.clear(); casingGroup = null; } } /** * Play the animation where Pico readies his gun to shoot the can. */ function playCockGunAnim() { this.playAnimation('cock', true, true); picoFade = new FlxSprite(0, 0); picoFade.frames = this.frames; picoFade.frame = this.frame; picoFade.updateHitbox(); picoFade.x = this.x; picoFade.y = this.y; // picoFade.stamp(this, 0, 0); picoFade.alpha = 0.3; picoFade.zIndex = this.zIndex - 3; addToStage(picoFade); FlxTween.tween(picoFade.scale, {x: 1.3, y: 1.3}, 0.4); FlxTween.tween(picoFade, {alpha: 0}, 0.4); FunkinSound.playOnce(Paths.sound('Gun_Prep'), 1.0); } /** * Play the animation where Pico shoots the can successfully. */ function playFireGunAnim(hip:Bool) { this.playAnimation('shoot', true, true); FunkinSound.playOnce(Paths.soundRandom('shot', 1, 4)); } /** * Play the animation where Pico is hit by the exploding can. */ function playCanExplodeAnim() { this.playAnimation('shootMISS', true, true); // Donk. FunkinSound.playOnce(Paths.sound('Pico_Bonk'), 1.0); } function addToStage(sprite:FlxSprite) { if (this.debug) { // We are in the chart editor or something. // TODO: Make this work properly. } else if (PlayState.instance != null && PlayState.instance.currentStage != null) { PlayState.instance.currentStage.add(sprite); } else { trace('Could not add Pico sprite to stage.'); } } }