package funkin.game; import funkin.editors.CharacterEditor; import funkin.scripting.DummyScript; import funkin.menus.StoryMenuState.WeekData; import funkin.ui.FunkinText; import flixel.group.FlxSpriteGroup; import funkin.options.Options; import funkin.scripting.Script; import flixel.util.FlxDestroyUtil; #if desktop import funkin.system.Discord.DiscordClient; #end import funkin.system.Section.SwagSection; import funkin.system.Song.SwagSong; import funkin.scripting.ScriptPack; import funkin.shaders.WiggleEffect.WiggleEffectType; import flixel.FlxBasic; import flixel.FlxCamera; import flixel.FlxG; import flixel.FlxGame; import flixel.FlxObject; import flixel.FlxSprite; import flixel.FlxState; import flixel.FlxSubState; import flixel.addons.display.FlxGridOverlay; import flixel.addons.effects.FlxTrail; import flixel.addons.effects.FlxTrailArea; import flixel.addons.effects.chainable.FlxEffectSprite; import flixel.addons.effects.chainable.FlxWaveEffect; import flixel.addons.transition.FlxTransitionableState; import flixel.graphics.atlas.FlxAtlas; import flixel.graphics.frames.FlxAtlasFrames; import flixel.group.FlxGroup.FlxTypedGroup; import flixel.math.FlxMath; import flixel.math.FlxPoint; import flixel.math.FlxRect; import flixel.system.FlxSound; import flixel.text.FlxText; import flixel.tweens.FlxEase; import flixel.tweens.FlxTween; import flixel.ui.FlxBar; import flixel.util.FlxCollision; import flixel.util.FlxColor; import flixel.util.FlxSort; import flixel.util.FlxStringUtil; import flixel.util.FlxTimer; import haxe.Json; import haxe.io.Path; import lime.utils.Assets; import openfl.display.BlendMode; import openfl.display.StageQuality; import openfl.filters.ShaderFilter; import funkin.system.Conductor; import funkin.system.Song; import funkin.editors.ChartingState; import funkin.debug.AnimationDebug; import funkin.cutscenes.*; import funkin.menus.*; import funkin.scripting.events.*; using StringTools; @:access(flixel.text.FlxText.FlxTextFormatRange) class PlayState extends MusicBeatState { public static var instance:PlayState = null; /** * SONG METADATA */ public static var SONG:SwagSong; public static var isStoryMode:Bool = false; public static var storyWeek:WeekData = null; public static var storyPlaylist:Array = []; public static var difficulty:String = "normal"; public static var fromMods:Bool = false; public var scripts:ScriptPack; public var halloweenLevel:Bool = false; public var gameOverSong:String = "gameOver"; public var lossSFX:String = "gameOverSFX"; public var retrySFX:String = "gameOverEnd"; public var stage:Stage; public var canDie:Bool = true; public var scrollSpeed:Float = 0; public var downscroll(get, set):Bool; @:dox(hide) private function set_downscroll(v:Bool) {return camHUD.downscroll = v;} @:dox(hide) private function get_downscroll():Bool {return camHUD.downscroll;} public var inst:FlxSound; public var vocals:FlxSound; public var dad:Character; public var gf:Character; public var boyfriend:Character; public var notes:NoteGroup; public var strumLine:FlxObject; public var ratingNum:Int = 0; public var camFollow:FlxObject; private static var prevCamFollow:FlxObject; public var strumLineNotes:FlxTypedGroup; public var playerStrums:FlxTypedGroup; public var cpuStrums:FlxTypedGroup; public var muteVocalsOnMiss:Bool = true; public var canAccessDebugMenus:Bool = true; public var camZooming:Bool = false; public var camZoomingInterval:Int = 4; public var curSong:String = ""; public var curStage:String = ""; public var gfSpeed:Int = 1; public var health:Float = 1; public var combo:Int = 0; public var comboBreaks:Bool = false; public var healthBarBG:FlxSprite; public var healthBar:FlxBar; public var generatedMusic:Bool = false; public var startingSong:Bool = false; public var iconP1:HealthIcon; public var iconP2:HealthIcon; public var camHUD:HudCamera; public var camGame:FlxCamera; public var songScore:Int = 0; public var misses:Int = 0; public var accuracy(get, set):Float; private function get_accuracy():Float { if (accuracyPressedNotes <= 0) return -1; return totalAccuracyAmount / accuracyPressedNotes; } private function set_accuracy(v:Float):Float { if (accuracyPressedNotes <= 0) accuracyPressedNotes = 1; return totalAccuracyAmount = v * accuracyPressedNotes; } public var accuracyPressedNotes:Float = 0; public var totalAccuracyAmount:Float = 0; public var scoreTxt:FunkinText; public var missesTxt:FunkinText; public var accuracyTxt:FunkinText; public static var campaignScore:Int = 0; public var defaultCamZoom:Float = 1.05; // how big to stretch the pixel art assets public static var daPixelZoom:Float = 6; public var inCutscene:Bool = false; public var comboRatings:Array = [ new ComboRating(0, "F", 0xFFFF4444), new ComboRating(0.1, "E", 0xFFFF8844), new ComboRating(0.2, "D", 0xFFFFAA44), new ComboRating(0.4, "C", 0xFFFFFF44), new ComboRating(0.6, "B", 0xFFAAFF44), new ComboRating(0.8, "A", 0xFF88FF44), new ComboRating(0.9, "S", 0xFF44FFFF), new ComboRating(1, "S++", 0xFF44FFFF), ]; #if desktop // Discord RPC variables public var difficultyText:String = ""; public var iconRPC:String = ""; public var songLength:Float = 0; public var detailsText:String = ""; public var detailsPausedText:String = ""; #end public var curRating:ComboRating; public function updateRating() { var rating = null; var acc = get_accuracy(); for(e in comboRatings) { if (e.percent <= acc && (rating == null || rating.percent < e.percent)) rating = e; } var event = scripts.event("onRatingUpdate", new RatingUpdateEvent(rating, curRating)); if (!event.cancelled) curRating = event.rating; } override public function create() { instance = this; PauseSubState.script = ""; if (FlxG.sound.music != null) FlxG.sound.music.stop(); (scripts = new ScriptPack("PlayState")).setParent(this); camGame = camera; FlxG.cameras.add(camHUD = new HudCamera(), false); camHUD.bgColor.alpha = 0; downscroll = Options.downscroll; // camGame.widescreen = true; persistentUpdate = true; persistentDraw = true; if (SONG == null) SONG = Song.loadFromJson('tutorial', 'normal'); scrollSpeed = SONG.speed; Conductor.setupSong(SONG); #if desktop // TODO: Scriptable custom RPC iconRPC = (dad != null && dad.icon != null) ? dad.icon : SONG.player2; detailsText = isStoryMode ? ("Story Mode: " + storyWeek.name) : "Freeplay"; // Checks if cutscene files exists var cutscenePath = Paths.script('data/cutscenes/${SONG.song.toLowerCase()}'); var endCutscenePath = Paths.script('data/cutscenes/${SONG.song.toLowerCase()}-end'); if (Assets.exists(cutscenePath)) cutscene = cutscenePath; if (Assets.exists(endCutscenePath)) endCutscene = endCutscenePath; // String for when the game is paused detailsPausedText = "Paused - " + detailsText; // Updating Discord Rich Presence. DiscordClient.changePresence(detailsText, SONG.song + " (" + difficultyText + ")", iconRPC); #end dad = new Character(100, 100, SONG.player2); if (dad != null && dad.isGF) { dad.setPosition(400, 130); gf = dad; dad.scrollFactor.set(0.95, 0.95); } else { var gfVersion = SONG.gf; if (gfVersion == null) gfVersion = "gf"; if (gfVersion != "none") { gf = new Character(400, 130, gfVersion); gf.scrollFactor.set(0.95, 0.95); } } var camPos:FlxPoint = new FlxPoint(dad.getGraphicMidpoint().x, dad.getGraphicMidpoint().y); comboGroup = new FlxSpriteGroup(FlxG.width * 0.55, (FlxG.height * 0.5) - 60); boyfriend = new Character(770, 100, SONG.player1, true); if (SONG.stage == null || SONG.stage.trim() == "") SONG.stage = "stage"; add(stage = new Stage(SONG.stage)); switch(SONG.song) { // case "": // ADD YOUR HARDCODED SCRIPTS HERE! default: for(content in [ Paths.getFolderContent('data/charts/${SONG.song}/', false, true, !fromMods), Paths.getFolderContent('data/charts/', false, true, !fromMods)]) { for(file in content) { var ext = Path.extension(file).toLowerCase(); if (Script.scriptExtensions.contains(ext)) { scripts.add(Script.create(file)); } } } } scripts.load(); scripts.call("create"); if (gf != null) { gf.danceOnBeat = false; add(gf); } add(comboGroup); if (dad != null) add(dad); if (boyfriend != null) add(boyfriend); strumLine = new FlxObject(0, 50, FlxG.width, 10); strumLine.scrollFactor.set(); strumLineNotes = new FlxTypedGroup(); playerStrums = new FlxTypedGroup(); cpuStrums = new FlxTypedGroup(); add(strumLineNotes); generateSong(SONG); if (prevCamFollow != null) { camFollow = prevCamFollow; prevCamFollow = null; } else { camFollow = new FlxObject(0, 0, 2, 2); camFollow.setPosition(camPos.x, camPos.y); } add(camFollow); FlxG.camera.follow(camFollow, LOCKON, 0.04); FlxG.camera.zoom = defaultCamZoom; FlxG.camera.focusOn(camFollow.getPosition()); FlxG.worldBounds.set(0, 0, FlxG.width, FlxG.height); healthBarBG = new FlxSprite(0, FlxG.height * 0.9).loadAnimatedGraphic(Paths.image('game/healthBar')); healthBarBG.screenCenter(X); healthBarBG.scrollFactor.set(); add(healthBarBG); healthBar = new FlxBar(healthBarBG.x + 4, healthBarBG.y + 4, RIGHT_TO_LEFT, Std.int(healthBarBG.width - 8), Std.int(healthBarBG.height - 8), this, 'health', 0, 2); healthBar.scrollFactor.set(); healthBar.createFilledBar(0xFFFF0000, 0xFF66FF33); add(healthBar); iconP1 = new HealthIcon(boyfriend.getIcon(), true); iconP2 = new HealthIcon(dad.getIcon(), false); for(icon in [iconP1, iconP2]) { icon.y = healthBar.y - (icon.height / 2); add(icon); } scoreTxt = new FunkinText(healthBarBG.x + 50, healthBarBG.y + 30, Std.int(healthBarBG.width - 100), "Score:0", 16); missesTxt = new FunkinText(healthBarBG.x + 50, healthBarBG.y + 30, Std.int(healthBarBG.width - 100), "Misses:0", 16); accuracyTxt = new FunkinText(healthBarBG.x + 50, healthBarBG.y + 30, Std.int(healthBarBG.width - 100), "Acc:-% (N/A)", 16); accuracyTxt.addFormat(accFormat, 0, 1); for(text in [scoreTxt, missesTxt, accuracyTxt]) { text.scrollFactor.set(); add(text); } scoreTxt.alignment = RIGHT; missesTxt.alignment = CENTER; accuracyTxt.alignment = LEFT; for(e in [strumLineNotes, notes, healthBar, healthBarBG, iconP1, iconP2, scoreTxt, missesTxt, accuracyTxt]) e.cameras = [camHUD]; startingSong = true; super.create(); } // PATH TO STRING CUTSCENE (Paths.script public var playCutscenes:Bool = isStoryMode; public var cutscene:String = null; public var endCutscene:String = null; public override function createPost() { startCutscene(); super.createPost(); scripts.call("postCreate"); } public function startCutscene() { if (playCutscenes) { var videoCutscene = Paths.video('${PlayState.SONG.song.toLowerCase()}-cutscene'); persistentUpdate = false; if (cutscene != null) { openSubState(new ScriptedCutscene(cutscene, function() { startCountdown(); })); } else if (Assets.exists(videoCutscene)) { FlxTransitionableState.skipNextTransIn = true; openSubState(new VideoCutscene(videoCutscene, function() { startCountdown(); })); persistentDraw = false; } else { startCountdown(); } } else startCountdown(); } public function startEndCutscene() { if (playCutscenes) { var videoCutscene = Paths.video('${PlayState.SONG.song.toLowerCase()}-end-cutscene'); persistentUpdate = false; if (endCutscene != null) { openSubState(new ScriptedCutscene(endCutscene, function() { nextSong(); })); } else if (Assets.exists(videoCutscene)) { openSubState(new VideoCutscene(videoCutscene, function() { nextSong(); })); persistentDraw = false; } else { nextSong(); } } else nextSong(); } public var startTimer:FlxTimer; public var perfectMode:Bool = false; public var introLength:Int = 5; public var introSprites:Array = [null, 'game/ready', "game/set", "game/go"]; public var introSounds:Array = ['intro3', 'intro2', "intro1", "introGo"]; public function startCountdown():Void { inCutscene = false; generateStaticArrows(0); generateStaticArrows(1); scripts.call("onStartCountdown"); startedCountdown = true; Conductor.songPosition = 0; Conductor.songPosition -= Conductor.crochet * introLength; var swagCounter:Int = 0; startTimer = new FlxTimer().start(Conductor.crochet / 1000, function(tmr:FlxTimer) { for(char in [dad, gf, boyfriend]) if (char != null) char.dance(); countdown(swagCounter++); }, introLength); } /** * Creates a fake countdown. */ public function countdown(swagCounter:Int) { var event:CountdownEvent = scripts.event("onCountdown", new CountdownEvent( swagCounter, introSprites[swagCounter], introSounds[swagCounter], 1, 0.6, true)); var sprite:FlxSprite = null; var sound:FlxSound = null; var tween:FlxTween = null; if (!event.cancelled) { if (event.spritePath != null) { var spr = event.spritePath; if (!Assets.exists(spr)) spr = Paths.image('$spr'); sprite = new FlxSprite().loadAnimatedGraphic(spr); sprite.scrollFactor.set(); sprite.scale.set(event.scale, event.scale); sprite.updateHitbox(); sprite.screenCenter(); add(sprite); tween = FlxTween.tween(sprite, {y: sprite.y + 100, alpha: 0}, Conductor.crochet / 1000, { ease: FlxEase.cubeInOut, onComplete: function(twn:FlxTween) { sprite.destroy(); } }); } if (event.soundPath != null) { var sfx = event.soundPath; if (!Assets.exists(sfx)) sfx = Paths.sound(sfx); sound = FlxG.sound.play(sfx, event.volume); } } event.sprite = sprite; event.sound = sound; event.spriteTween = tween; event.cancelled = false; scripts.event("onPostCountdown", event); } public var previousFrameTime:Int = 0; public var lastReportedPlayheadPosition:Int = 0; public var songTime:Float = 0; function startSong():Void { scripts.call("onSongStart"); startingSong = false; previousFrameTime = FlxG.game.ticks; lastReportedPlayheadPosition = 0; if (!paused) { FlxG.sound.music = inst; FlxG.sound.music.play(); } FlxG.sound.music.onComplete = endSong; vocals.play(); updateDiscordStatus(); } public override function destroy() { scripts.call("destroy"); super.destroy(); FlxDestroyUtil.destroy(scripts); instance = null; if (vocals != null) { vocals.destroy(); } } public var debugNum:Int = 0; public function generateNotes(songData:SwagSong) { if (songData == null) return; if (songData.noteTypes == null) songData.noteTypes = []; var noteData:Array = songData.notes; var playerCounter:Int = 0; var daBeats:Int = 0; // Not exactly representative of 'daBeats' lol, just how much it has looped var stepCrochet = Conductor.stepCrochet; for (section in noteData) { if (section == null || section.sectionNotes == null) continue; if (section.changeBPM && section.bpm > 0) stepCrochet = ((60 / section.bpm) * 250); for (songNotes in section.sectionNotes) { if (songNotes == null) continue; var daStrumTime:Float = songNotes[0]; var daNoteData:Int = Std.int(songNotes[1] % 8); if (daNoteData < 0) continue; var daNoteType:Int = Std.int(songNotes[1] / 8); var gottaHitNote:Bool = daNoteData >= 4 ? !section.mustHitSection : section.mustHitSection; if (songNotes.length > 2) { if (songNotes[3] is Int) daNoteType = addNoteType(songData.noteTypes[Std.int(songNotes[3])-1], songNotes[3] == 0); else if (songNotes[3] is String) daNoteType = addNoteType(songNotes[3]); } else { daNoteType = addNoteType(songData.noteTypes[daNoteType-1], daNoteType == 0); } var swagNote:Note; if (notes.length > 0) swagNote = notes.members[Std.int(notes.members.length - 1)]; else swagNote = null; swagNote.nextNote = (swagNote = new Note(daStrumTime, daNoteData % 4, daNoteType, gottaHitNote, swagNote, false, section.altAnim ? "-alt" : "")); swagNote.sustainLength = songNotes[2]; swagNote.scrollFactor.set(0, 0); swagNote.stepLength = stepCrochet; notes.add(swagNote); // calculate sustain length and fix var susLength:Float = swagNote.sustainLength / stepCrochet; if (susLength > 0.75) susLength++; // create sustains for (susNote in 0...Math.floor(susLength)) { swagNote = new Note(daStrumTime + (stepCrochet * susNote), daNoteData % 4, daNoteType, gottaHitNote, swagNote, true, section.altAnim ? "-alt" : ""); swagNote.scrollFactor.set(); swagNote.stepLength = stepCrochet; notes.add(swagNote); } } daBeats += 1; } notes.sortNotes(); } private function generateSong(?songData:SwagSong):Void { if (songData == null) songData = SONG; Conductor.changeBPM(songData.bpm); curSong = songData.song; inst = FlxG.sound.load(Paths.inst(PlayState.SONG.song)); vocals = FlxG.sound.list.recycle(FlxSound); @:privateAccess vocals.reset(); if (SONG.needsVoices) vocals.loadEmbedded(Paths.voices(PlayState.SONG.song)); FlxG.sound.list.add(vocals); notes = new NoteGroup(); add(notes); generateNotes(songData); generatedMusic = true; } public function addNoteType(name:String, addAsDefaultIfUnknown:Bool = false):Int { if (name == null) { if (addAsDefaultIfUnknown) return 0; else name = "Unknown"; } for(k=>e in noteTypesArray) if (e == name) return k; noteTypesArray.push(name); // loads script var scriptPath = Paths.script('data/notes/${name}'); if (Assets.exists(scriptPath) && !scripts.contains(scriptPath)) { var script = Script.create(scriptPath); if (!(script is DummyScript)) { scripts.add(script); script.load(); } } return noteTypesArray.length-1; } function sortByShit(Obj1:Note, Obj2:Note):Int { return FlxSort.byValues(FlxSort.ASCENDING, Obj1.strumTime, Obj2.strumTime); } private function generateStaticArrows(player:Int):Void { for (i in 0...4) { // FlxG.log.add(i); var babyArrow:Strum = new Strum((FlxG.width * 0.25) + (Note.swagWidth * (i - 2)) + ((FlxG.width / 2) * player), strumLine.y); babyArrow.ID = i; var event = scripts.event("onStrumCreation", new StrumCreationEvent(babyArrow, player, i)); if (!event.cancelled) { switch (curStage) { // case "school": default: babyArrow.frames = Paths.getFrames(event.sprite); babyArrow.animation.addByPrefix('green', 'arrowUP'); babyArrow.animation.addByPrefix('blue', 'arrowDOWN'); babyArrow.animation.addByPrefix('purple', 'arrowLEFT'); babyArrow.animation.addByPrefix('red', 'arrowRIGHT'); babyArrow.antialiasing = true; babyArrow.setGraphicSize(Std.int(babyArrow.width * 0.7)); switch (babyArrow.ID % 4) { case 0: babyArrow.animation.addByPrefix('static', 'arrowLEFT'); babyArrow.animation.addByPrefix('pressed', 'left press', 24, false); babyArrow.animation.addByPrefix('confirm', 'left confirm', 24, false); case 1: babyArrow.animation.addByPrefix('static', 'arrowDOWN'); babyArrow.animation.addByPrefix('pressed', 'down press', 24, false); babyArrow.animation.addByPrefix('confirm', 'down confirm', 24, false); case 2: babyArrow.animation.addByPrefix('static', 'arrowUP'); babyArrow.animation.addByPrefix('pressed', 'up press', 24, false); babyArrow.animation.addByPrefix('confirm', 'up confirm', 24, false); case 3: babyArrow.animation.addByPrefix('static', 'arrowRIGHT'); babyArrow.animation.addByPrefix('pressed', 'right press', 24, false); babyArrow.animation.addByPrefix('confirm', 'right confirm', 24, false); } } } babyArrow.updateHitbox(); babyArrow.scrollFactor.set(); if (event.__doAnimation) { babyArrow.y -= 10; babyArrow.alpha = 0; FlxTween.tween(babyArrow, {y: babyArrow.y + 10, alpha: 1}, 1, {ease: FlxEase.circOut, startDelay: 0.5 + (0.2 * i)}); } if (event.player == 1) playerStrums.add(babyArrow); else { babyArrow.cpu = true; cpuStrums.add(babyArrow); } babyArrow.animation.play('static'); strumLineNotes.add(babyArrow); } } override function openSubState(SubState:FlxSubState) { var event = scripts.event("onSubstateOpen", new StateEvent(SubState)); if (!postCreated) FlxTransitionableState.skipNextTransIn = true; if (event.cancelled) return; if (paused) { if (FlxG.sound.music != null) { FlxG.sound.music.pause(); vocals.pause(); } if (!startTimer.finished) startTimer.active = false; } super.openSubState(SubState); } override function closeSubState() { var event = scripts.event("onSubstateClose", new StateEvent(subState)); if (event.cancelled) return; if (paused) { if (FlxG.sound.music != null && !startingSong) { resyncVocals(); } if (!startTimer.finished) startTimer.active = true; paused = false; #if desktop if (startTimer.finished) { DiscordClient.changePresence(detailsText, SONG.song + " (" + difficultyText + ")", iconRPC, true, songLength - Conductor.songPosition); } else { DiscordClient.changePresence(detailsText, SONG.song + " (" + difficultyText + ")", iconRPC); } #end } super.closeSubState(); } var __songPlaying:Bool = false; var __wasAutoPause:Bool = false; override public function onFocus():Void { scripts.call("onFocus"); #if desktop if (health > 0 && !paused) { if (Conductor.songPosition > 0.0) { DiscordClient.changePresence(detailsText, SONG.song + " (" + difficultyText + ")", iconRPC, true, songLength - Conductor.songPosition); } else { DiscordClient.changePresence(detailsText, SONG.song + " (" + difficultyText + ")", iconRPC); } } #end if (__wasAutoPause && __songPlaying) { inst.play(); vocals.play(); } super.onFocus(); } override public function onFocusLost():Void { scripts.call("onFocusLost"); #if desktop if (health > 0 && !paused) { DiscordClient.changePresence(detailsPausedText, SONG.song + " (" + difficultyText + ")", iconRPC); } #end if (__wasAutoPause = FlxG.autoPause) { __songPlaying = inst.playing; inst.pause(); vocals.pause(); } super.onFocusLost(); } function resyncVocals():Void { vocals.pause(); FlxG.sound.music.play(); Conductor.songPosition = FlxG.sound.music.time; vocals.time = Conductor.songPosition; vocals.play(); scripts.call("onVocalsResync"); } public var paused:Bool = false; public var startedCountdown:Bool = false; public var canPause:Bool = true; public var curSection(get, null):SwagSection; public function get_curSection() { return PlayState.SONG.notes[Std.int(curStep / 16)]; } public function pauseGame() { var e = scripts.event("onGamePause", new CancellableEvent()); if (e.cancelled) return; persistentUpdate = false; persistentDraw = true; paused = true; // 1 / 1000 chance for Gitaroo Man easter egg if (FlxG.random.bool(0.1)) { // gitaroo man easter egg FlxG.switchState(new GitarooPause()); } else openSubState(new PauseSubState(boyfriend.getScreenPosition().x, boyfriend.getScreenPosition().y)); updateDiscordStatus(); } // TODO: Update Discord Status public function updateDiscordStatus() { // TODO: Cancellable Discord Update Presence #if desktop // Song duration in a float, useful for the time left feature songLength = inst.length; // Updating Discord Rich Presence (with Time Left) DiscordClient.changePresence(detailsText, SONG.song + " (" + difficultyText + ")", iconRPC, true, songLength); #end scripts.call("onDiscordPresenceUpdate"); } var __vocalOffsetViolation:Float = 0; public var accFormat:FlxTextFormat = new FlxTextFormat(0xFF888888, false, false, 0); override public function update(elapsed:Float) { #if !debug perfectMode = false; #end super.update(elapsed); scripts.call("update", [elapsed]); scoreTxt.text = 'Score:$songScore'; missesTxt.text = '${comboBreaks ? "Combo Breaks" : "Misses"}:$misses'; var acc = accuracy; var rating:ComboRating = curRating == null ? new ComboRating(0, "[N/A]", 0xFF888888) : curRating; @:privateAccess accFormat.format.color = rating.color; accuracyTxt.text = 'Accuracy:${acc < 0 ? "-%" : '${FlxMath.roundDecimal(acc * 100, 2)}%'} - ${rating.rating}'; @:privateAccess var format = accuracyTxt._formatRanges[0]; format.range.start = accuracyTxt.text.length - rating.rating.length; format.range.end = accuracyTxt.text.length; // accuracyTxt.addFormat(accFormat, accuracyTxt.text.length - rating.rating.length, accuracyTxt.text.length); if (FlxG.keys.justPressed.ENTER && startedCountdown && canPause) pauseGame(); if (canAccessDebugMenus) { if (FlxG.keys.justPressed.SEVEN) { FlxG.switchState(new ChartingState()); #if desktop DiscordClient.changePresence("Chart Editor", null, null, true); #end } if (FlxG.keys.justPressed.F5) { Logs.trace('Reloading scripts...', WARNING, YELLOW); scripts.reload(); Logs.trace('Song scripts successfully reloaded.', WARNING, GREEN); } if (FlxG.keys.justPressed.EIGHT) FlxG.switchState(new CharacterEditor(SONG.player2)); if (FlxG.keys.justPressed.NINE) FlxG.switchState(new CharacterEditor(SONG.player1)); } // FlxG.watch.addQuick('VOL', vocals.amplitudeLeft); // FlxG.watch.addQuick('VOLRight', vocals.amplitudeRight); iconP1.scale.set(lerp(iconP1.scale.x, 1, 0.33), lerp(iconP1.scale.y, 1, 0.33)); iconP2.scale.set(lerp(iconP2.scale.x, 1, 0.33), lerp(iconP2.scale.y, 1, 0.33)); iconP1.updateHitbox(); iconP2.updateHitbox(); var iconOffset:Int = 26; iconP1.x = healthBar.x + (healthBar.width * (FlxMath.remapToRange(healthBar.percent, 0, 100, 1, 0)) - iconOffset); iconP2.x = healthBar.x + (healthBar.width * (FlxMath.remapToRange(healthBar.percent, 0, 100, 1, 0))) - (iconP2.width - iconOffset); if (health > 2) health = 2; iconP1.health = healthBar.percent / 100; iconP2.health = 1 - (healthBar.percent / 100); if (startingSong) { if (startedCountdown) { Conductor.songPosition += FlxG.elapsed * 1000; if (Conductor.songPosition >= 0) startSong(); } } else { __vocalOffsetViolation = Math.max(0, __vocalOffsetViolation + (FlxG.sound.music.time != vocals.time ? elapsed * 2 : -elapsed)); if (__vocalOffsetViolation > 25) { resyncVocals(); __vocalOffsetViolation = 0; } } if (generatedMusic && curSection != null) { var dadPos = dad.getCameraPosition(); var bfPos = boyfriend.getCameraPosition(); var section = curSection; // from dad to bf var ratio:Float = 0; if (section.camTarget != null) ratio = section.camTarget; else ratio = section.mustHitSection ? 1 : 0; var event = scripts.event("onCameraMove", new CamMoveEvent(bfPos, dadPos, ratio, FlxPoint.get(FlxMath.lerp(dadPos.x, bfPos.x, ratio), FlxMath.lerp(dadPos.y, bfPos.y, ratio)))); if (!event.cancelled) { camFollow.setPosition( event.position.x, event.position.y ); } for(e in [event.position, event.bfCamPos, event.dadCamPos]) e.put(); } if (camZooming) { FlxG.camera.zoom = lerp(FlxG.camera.zoom, defaultCamZoom, 0.05); camHUD.zoom = lerp(camHUD.zoom, 1, 0.05); } FlxG.watch.addQuick("beatShit", curBeat); FlxG.watch.addQuick("stepShit", curStep); // RESET = Quick Game Over Screen if (controls.RESET) health = 0; // CHEAT = brandon's a pussy if (controls.CHEAT) { health += 1; trace("User is cheating!"); } if (health <= 0 && canDie) gameOver(); if (generatedMusic) { notes.forEachAlive(function(daNote:Note) { var strum:Strum = null; for(e in (daNote.mustPress ? playerStrums : cpuStrums).members) { if (e.ID == daNote.noteData % 4) { strum = e; break; //ing bad } } var event = PlayState.instance.scripts.event("onNoteUpdate", new NoteUpdateEvent(daNote, elapsed, strum)); if (!event.cancelled) { if (event.__updateHitWindow) { if (daNote.mustPress) { daNote.canBeHit = (daNote.strumTime > Conductor.songPosition - (Conductor.safeZoneOffset * daNote.latePressWindow) && daNote.strumTime < Conductor.songPosition + (Conductor.safeZoneOffset * daNote.earlyPressWindow)); if (daNote.strumTime < Conductor.songPosition - Conductor.safeZoneOffset && !daNote.wasGoodHit) daNote.tooLate = true; } else daNote.canBeHit = false; } if (event.__autoCPUHit && !daNote.mustPress && !daNote.wasGoodHit && daNote.strumTime < Conductor.songPosition) goodNoteHit(daNote); if (daNote.wasGoodHit && daNote.isSustainNote && daNote.strumTime + (daNote.stepLength) < Conductor.songPosition) { deleteNote(daNote); return; } if (daNote.tooLate) { noteMiss(daNote); return; } if (event.strum == null) return; if (event.__reposNote) event.strum.updateNotePosition(daNote); event.strum.updateSustain(daNote); PlayState.instance.scripts.event("onNotePostUpdate", event); } }); } if (!inCutscene) keyShit(); #if debug if (FlxG.keys.justPressed.ONE) endSong(); #end scripts.call("postUpdate", [elapsed]); } function gameOver(?character:String, ?gameOverSong:String, ?lossSFX:String, ?retrySFX:String) { character = character.getDefault((boyfriend != null) ? boyfriend.gameOverCharacter : "bf-dead"); gameOverSong = gameOverSong.getDefault(this.gameOverSong); lossSFX = lossSFX.getDefault(this.lossSFX); retrySFX = retrySFX.getDefault(this.retrySFX); boyfriend.stunned = true; persistentUpdate = false; persistentDraw = false; paused = true; vocals.stop(); if (FlxG.sound.music != null) FlxG.sound.music.stop(); openSubState(new GameOverSubstate(boyfriend.x, boyfriend.y, character, gameOverSong, lossSFX, retrySFX)); #if desktop // Game Over doesn't get his own variable because it's only used here DiscordClient.changePresence("Game Over - " + detailsText, SONG.song + " (" + difficultyText + ")", iconRPC); #end } function endSong():Void { scripts.call("onSongEnd"); canPause = false; inst.volume = 0; vocals.volume = 0; inst.pause(); vocals.pause(); if (SONG.validScore) { #if !switch Highscore.saveScore(SONG.song, { score: songScore, misses: misses, accuracy: accuracy }, difficulty); #end } startEndCutscene(); } public function nextSong() { if (isStoryMode) { campaignScore += songScore; storyPlaylist.remove(storyPlaylist[0]); if (storyPlaylist.length <= 0) { transIn = FlxTransitionableState.defaultTransIn; transOut = FlxTransitionableState.defaultTransOut; FlxG.switchState(new StoryMenuState()); if (SONG.validScore) { // TODO: more week info saving Highscore.saveWeekScore(storyWeek.name, { score: campaignScore }, difficulty); } FlxG.save.flush(); } else { trace('LOADING NEXT SONG'); trace(PlayState.storyPlaylist[0].toLowerCase(), difficulty); FlxTransitionableState.skipNextTransIn = true; FlxTransitionableState.skipNextTransOut = true; prevCamFollow = camFollow; PlayState.SONG = Song.loadFromJson(PlayState.storyPlaylist[0].toLowerCase(), difficulty); FlxG.sound.music.stop(); FlxG.switchState(new PlayState()); } } else { trace('WENT BACK TO FREEPLAY??'); FlxG.switchState(new FreeplayState()); } } public var endingSong:Bool = false; public var comboGroup:FlxSpriteGroup; private function keyShit():Void { var pressed = [controls.NOTE_LEFT, controls.NOTE_DOWN, controls.NOTE_UP, controls.NOTE_RIGHT]; var justPressed = [controls.NOTE_LEFT_P, controls.NOTE_DOWN_P, controls.NOTE_UP_P, controls.NOTE_RIGHT_P]; var justReleased = [controls.NOTE_LEFT_R, controls.NOTE_DOWN_R, controls.NOTE_UP_R, controls.NOTE_RIGHT_R]; var event = scripts.event("onKeyShit", new InputSystemEvent(pressed, justPressed, justReleased)); if (event.cancelled) return; pressed = CoolUtil.getDefault(event.pressed, []); justPressed = CoolUtil.getDefault(event.justPressed, []); justReleased = CoolUtil.getDefault(event.justReleased, []); var funcsToExec:ArrayVoid> = []; if (pressed.contains(true)) { funcsToExec.push(function(note:Note) { if (pressed[note.strumID] && note.isSustainNote && note.canBeHit && note.mustPress && !note.wasGoodHit) { goodNoteHit(note); } }); } var notePerStrum = [for(i in 0...4) null]; var additionalNotes:Array = []; if (justPressed.contains(true)) { funcsToExec.push(function(note:Note) { if (justPressed[note.strumID] && !note.isSustainNote && note.mustPress && !note.wasGoodHit && note.canBeHit) { if (notePerStrum[note.strumID] == null) notePerStrum[note.strumID] = note; else if (Math.abs(notePerStrum[note.strumID].strumTime - note.strumTime) <= 10) additionalNotes.push(note); else if (note.strumTime < notePerStrum[note.strumID].strumTime) notePerStrum[note.strumID] = note; } }); } if (funcsToExec.length > 0) { notes.forEachAlive(function(note:Note) { for(e in funcsToExec) e(note); }); } for(e in notePerStrum) if (e != null) goodNoteHit(e); for(e in additionalNotes) goodNoteHit(e); playerStrums.forEach(function(str:Strum) { str.updatePlayerInput(pressed[str.ID], justPressed[str.ID], justReleased[str.ID]); }); scripts.call("onPostKeyShit"); } function noteMiss(note:Note):Void { if (!boyfriend.stunned) { var event:NoteHitEvent = scripts.event("onPlayerMiss", new NoteHitEvent(note, boyfriend, true, note.noteType, note.strumID, -0.04, false, -10)); if (event.cancelled) return; health += event.healthGain; if (gf != null && combo > 5 && gf.animOffsets.exists('sad')) { gf.playAnim('sad'); } combo = 0; songScore -= 10; misses++; FlxG.sound.play(Paths.soundRandom('missnote', 1, 3), FlxG.random.float(0.1, 0.2)); if (muteVocalsOnMiss) vocals.volume = 0; boyfriend.stunned = true; switch (note.strumID) { case 0: boyfriend.playAnim('singLEFTmiss', true, MISS); case 1: boyfriend.playAnim('singDOWNmiss', true, MISS); case 2: boyfriend.playAnim('singUPmiss', true, MISS); case 3: boyfriend.playAnim('singRIGHTmiss', true, MISS); } deleteNote(note); } } public var noteTypesArray:Array = [null]; public function getNoteType(id:Int):String { return noteTypesArray[id]; } function goodNoteHit(note:Note):Void { if (!note.wasGoodHit) { note.wasGoodHit = true; /** * CALCULATES RATING */ var noteDiff = Math.abs(Conductor.songPosition - note.strumTime); var daRating:String = "sick"; var score:Int = 300; var accuracy:Float = 1; if (noteDiff > Conductor.safeZoneOffset * 0.9) { daRating = 'shit'; score = 50; accuracy = 0.25; } else if (noteDiff > Conductor.safeZoneOffset * 0.75) { daRating = 'bad'; score = 100; accuracy = 0.45; } else if (noteDiff > Conductor.safeZoneOffset * 0.2) { daRating = 'good'; score = 200; accuracy = 0.75; } var event:NoteHitEvent; if (note.mustPress) event = scripts.event("onPlayerHit", new NoteHitEvent(note, boyfriend, true, note.noteType, note.strumID, note.noteData > 0 ? 0.023 : 0.004, score, note.animSuffix, daRating, note.isSustainNote ? null : accuracy, "game/score/", '')); else event = scripts.event("onDadHit", new NoteHitEvent(note, dad, false, note.noteType, note.strumID, 0, 0, note.animSuffix, daRating, null, "game/score/", '')); if (!event.cancelled) { if (event.accuracy != null) { accuracyPressedNotes++; totalAccuracyAmount += accuracy; updateRating(); } if (event.countAsCombo) combo++; if (event.showRating || (event.showRating == null && event.player && !note.isSustainNote)) { var rating:FlxSprite = new FlxSprite(-40, -60); songScore += score; rating.loadAnimatedGraphic(Paths.image('${event.ratingPrefix}$daRating${event.ratingSuffix}')); rating.acceleration.y = 550; rating.velocity.y -= FlxG.random.int(140, 175); rating.velocity.x -= FlxG.random.int(0, 10); var comboSpr:FlxSprite = new FlxSprite().loadAnimatedGraphic(Paths.image('${event.ratingPrefix}combo${event.ratingSuffix}')); comboSpr.acceleration.y = 600; comboSpr.velocity.y -= 150; comboSpr.velocity.x += FlxG.random.int(1, 10); rating.scale.set(event.ratingScale, event.ratingScale); rating.antialiasing = event.ratingAntialiasing; comboSpr.scale.set(event.ratingScale, event.ratingScale); comboSpr.antialiasing = event.ratingAntialiasing; comboSpr.updateHitbox(); rating.updateHitbox(); var separatedScore:String = Std.string(combo).addZeros(3); if (combo == 0 || combo >= 10) { comboGroup.add(comboSpr); for (i in 0...separatedScore.length) { var e = separatedScore.charAt(i); var numScore:FlxSprite = new FlxSprite((43 * i) - 90, 80).loadAnimatedGraphic(Paths.image('${event.ratingPrefix}num$e${event.ratingSuffix}')); numScore.antialiasing = event.numAntialiasing; numScore.scale.set(event.numScale, event.numScale); numScore.updateHitbox(); numScore.acceleration.y = FlxG.random.int(200, 300); numScore.velocity.y -= FlxG.random.int(140, 160); numScore.velocity.x = FlxG.random.float(-5, 5); comboGroup.add(numScore); FlxTween.tween(numScore, {alpha: 0}, 0.2, { onComplete: function(tween:FlxTween) { comboGroup.remove(numScore, true); numScore.destroy(); }, startDelay: Conductor.crochet * 0.002 }); } } comboGroup.add(rating); FlxTween.tween(rating, {alpha: 0}, 0.2, { startDelay: Conductor.crochet * 0.001 }); FlxTween.tween(comboSpr, {alpha: 0}, 0.2, { onComplete: function(tween:FlxTween) { comboGroup.remove(comboSpr, true); comboGroup.remove(rating, true); comboSpr.destroy(); rating.destroy(); }, startDelay: Conductor.crochet * 0.001 }); ratingNum += 1; } health += event.healthGain; if (!event.animCancelled) { event.character.playSingAnim(event.direction, event.animSuffix); } if (!event.strumGlowCancelled) (event.player ? playerStrums : cpuStrums).forEach(function(str:Strum) { if (str.ID == Math.abs(note.strumID)) { str.press(note.strumTime); } }); } if (event.unmuteVocals) vocals.volume = 1; if (event.enableCamZooming) camZooming = true; if (event.autoHitLastSustain) { if (note.nextSustain != null && note.nextSustain.nextSustain == null) { // its a tail!! note.wasGoodHit = true; } } if (event.deleteNote && !note.isSustainNote) deleteNote(note); } } public function deleteNote(note:Note) { var event:SimpleNoteEvent = scripts.event("onNoteDelete", new SimpleNoteEvent(note)); if (!event.cancelled) { scripts.call("onNoteDelete", [note]); note.kill(); notes.remove(note, true); note.destroy(); } } override function stepHit(curStep:Int) { super.stepHit(curStep); scripts.call("stepHit", [curStep]); } override function beatHit(curBeat:Int) { super.beatHit(curBeat); if (camZoomingInterval < 1) camZoomingInterval = 1; if (Options.camZoomOnBeat && camZooming && FlxG.camera.zoom < 1.35 && curBeat % camZoomingInterval == 0) { FlxG.camera.zoom += 0.015; camHUD.zoom += 0.03; } iconP1.scale.set(1.2, 1.2); iconP2.scale.set(1.2, 1.2); iconP1.updateHitbox(); iconP2.updateHitbox(); if (gf != null && curBeat % gfSpeed == 0) { gf.dance(); } scripts.call("beatHit", [curBeat]); } public var curLight:Int = 0; } class ComboRating { public var percent:Float; public var rating:String; public var color:FlxColor; public function new(percent:Float, rating:String, color:FlxColor) { this.percent = percent; this.rating = rating; this.color = color; } }