diff --git a/source/funkin/modding/events/ScriptEvent.hx b/source/funkin/modding/events/ScriptEvent.hx index ef8fc3a06..ca671d8e4 100644 --- a/source/funkin/modding/events/ScriptEvent.hx +++ b/source/funkin/modding/events/ScriptEvent.hx @@ -294,15 +294,22 @@ class NoteScriptEvent extends ScriptEvent */ public var note(default, null):Note; - public function new(type:ScriptEventType, note:Note, cancelable:Bool = false):Void + /** + * The combo count as it is with this event. + * Will be (combo) on miss events and (combo + 1) on hit events (the stored combo count won't update if the event is cancelled). + */ + public var comboCount(default, null):Int; + + public function new(type:ScriptEventType, note:Note, comboCount:Int = 0, cancelable:Bool = false):Void { super(type, cancelable); this.note = note; + this.comboCount = comboCount; } public override function toString():String { - return 'NoteScriptEvent(type=' + type + ', cancelable=' + cancelable + ', note=' + note + ')'; + return 'NoteScriptEvent(type=' + type + ', cancelable=' + cancelable + ', note=' + note + ', comboCount=' + comboCount + ')'; } } diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index 20e39edb5..52d123a65 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -979,6 +979,7 @@ class PlayState extends MusicBeatState implements IHook regenNoteData(); // loads the note data from start health = 1; songScore = 0; + combo = 0; Countdown.performCountdown(currentStageId.startsWith('school')); needsReset = false; @@ -1221,7 +1222,7 @@ class PlayState extends MusicBeatState implements IHook if (currentSong.song != 'Tutorial') camZooming = true; - var event:NoteScriptEvent = new NoteScriptEvent(ScriptEvent.NOTE_HIT, daNote, true); + var event:NoteScriptEvent = new NoteScriptEvent(ScriptEvent.NOTE_HIT, daNote, combo, true); dispatchEvent(event); // Calling event.cancelEvent() in a module should force the CPU to miss the note. @@ -1305,19 +1306,6 @@ class PlayState extends MusicBeatState implements IHook daNote.clipRect = swagRect; } - function killCombo():Void - { - // Girlfriend gets sad if you combo break after hitting 5 notes. - if (currentStage != null && currentStage.getGirlfriend() != null) - if (combo > 5 && currentStage.getGirlfriend().hasAnimation('sad')) - currentStage.getGirlfriend().playAnimation('sad'); - - if (combo != 0) - { - combo = comboPopUps.displayCombo(0); - } - } - #if debug function changeSection(sec:Int):Void { @@ -1685,7 +1673,7 @@ class PlayState extends MusicBeatState implements IHook function noteMiss(note:Note):Void { - var event:NoteScriptEvent = new NoteScriptEvent(ScriptEvent.NOTE_MISS, note, true); + var event:NoteScriptEvent = new NoteScriptEvent(ScriptEvent.NOTE_MISS, note, combo, true); dispatchEvent(event); // Calling event.cancelEvent() skips all the other logic! Neat! if (event.eventCanceled) @@ -1695,7 +1683,11 @@ class PlayState extends MusicBeatState implements IHook if (!isPracticeMode) songScore -= 10; vocals.volume = 0; - killCombo(); + + if (combo != 0) + { + combo = comboPopUps.displayCombo(0); + } note.active = false; note.visible = false; @@ -1709,7 +1701,7 @@ class PlayState extends MusicBeatState implements IHook { if (!note.wasGoodHit) { - var event:NoteScriptEvent = new NoteScriptEvent(ScriptEvent.NOTE_HIT, note, true); + var event:NoteScriptEvent = new NoteScriptEvent(ScriptEvent.NOTE_HIT, note, combo + 1, true); dispatchEvent(event); // Calling event.cancelEvent() skips all the other logic! Neat! diff --git a/source/funkin/play/character/BaseCharacter.hx b/source/funkin/play/character/BaseCharacter.hx index 3f038e294..713733804 100644 --- a/source/funkin/play/character/BaseCharacter.hx +++ b/source/funkin/play/character/BaseCharacter.hx @@ -34,6 +34,16 @@ class BaseCharacter extends Bopper public var isDead:Bool = false; public var debugMode:Bool = false; + /** + * This character plays a given animation when hitting these specific combo numbers. + */ + public var comboNoteCounts(default, null):Array; + + /** + * This character plays a given animation when dropping combos larger than these numbers. + */ + public var dropNoteCounts(default, null):Array; + final _data:CharacterData; final singTimeCrochet:Float; @@ -147,6 +157,25 @@ class BaseCharacter extends Bopper shouldBop = false; } + function findCountAnimations(prefix:String):Array { + var animNames:Array = this.animation.getNameList(); + + var result:Array = []; + + for (anim in animNames) { + if (anim.startsWith(prefix)) { + var comboNum:Null = Std.parseInt(anim.substring(prefix.length)); + if (comboNum != null) { + result.push(comboNum); + } + } + } + + // Sort numerically. + result.sort((a, b) -> a - b); + return result; + } + /** * Set the sprite scale to the appropriate value. * @param scale @@ -181,6 +210,15 @@ class BaseCharacter extends Bopper var charCenterX = this.x + this.width / 2; var charCenterY = this.y + this.height / 2; this.cameraFocusPoint = new FlxPoint(charCenterX + _data.cameraOffsets[0], charCenterY + _data.cameraOffsets[1]); + + // Child class should have created animations by now, + // so we can query which ones are available. + this.comboNoteCounts = findCountAnimations('combo'); // example: combo50 + this.dropNoteCounts = findCountAnimations('drop'); // example: drop50 + trace('${this.animation.getNameList()}'); + trace('Combo note counts: ' + this.comboNoteCounts); + trace('Drop note counts: ' + this.dropNoteCounts); + super.onCreate(event); } @@ -369,6 +407,11 @@ class BaseCharacter extends Bopper { // If the note is from the same strumline, play the sing animation. this.playSingAnimation(event.note.data.dir, false, event.note.data.altNote); + } else if (characterType == GF) { + if (this.comboNoteCounts.contains(event.comboCount)) { + trace('Playing GF combo animation: combo${event.comboCount}'); + this.playAnimation('combo${event.comboCount}', true, true); + } } } @@ -389,6 +432,22 @@ class BaseCharacter extends Bopper { // If the note is from the same strumline, play the sing animation. this.playSingAnimation(event.note.data.dir, true, event.note.data.altNote); + } else if (event.note.mustPress && characterType == GF) { + var dropAnim = ''; + + // Choose the combo drop anim to play. + // If there are several (for example, drop10 and drop50) the highest one will be used. + // If the combo count is too low, no animation will be played. + for (count in dropNoteCounts) { + if (event.comboCount >= count) { + dropAnim = 'drop${count}'; + } + } + + if (dropAnim != '') { + trace('Playing GF combo drop animation: ${dropAnim}'); + this.playAnimation(dropAnim, true, true); + } } } @@ -435,8 +494,37 @@ class BaseCharacter extends Bopper enum CharacterType { + /** + * The BF character has the following behaviors. + * - At idle, dances with `danceLeft` and `danceRight` if available, or `idle` if not. + * - When the player hits a note, plays the appropriate `singDIR` animation until BF is done singing. + * - If there is a `singDIR-end` animation, the `singDIR` animation will play once before looping the `singDIR-end` animation until BF is done singing. + * - If the player misses or hits a ghost note, plays the appropriate `singDIR-miss` animation until BF is done singing. + */ BF; + /** + * The DAD character has the following behaviors. + * - At idle, dances with `danceLeft` and `danceRight` if available, or `idle` if not. + * - When the CPU hits a note, plays the appropriate `singDIR` animation until DAD is done singing. + * - If there is a `singDIR-end` animation, the `singDIR` animation will play once before looping the `singDIR-end` animation until DAD is done singing. + * - When the CPU misses a note (NOTE: This only happens via script, not by default), plays the appropriate `singDIR-miss` animation until DAD is done singing. + */ DAD; + /** + * The GF character has the following behaviors. + * - At idle, dances with `danceLeft` and `danceRight` if available, or `idle` if not. + * - If available, `combo###` animations will play when certain combo counts are reached. + * - For example, `combo50` will play when the player hits 50 notes in a row. + * - Multiple combo animations can be provided for different thresholds. + * - If available, `drop###` animations will play when combos are dropped above certain thresholds. + * - For example, `drop10` will play when the player drops a combo larger than 10. + * - Multiple drop animations can be provided for different thresholds (i.e. dropping larger combos). + * - No drop animation will play if one isn't applicable (i.e. if the combo count is too low). + */ GF; + /** + * The OTHER character will only perform the `danceLeft`/`danceRight` or `idle` animation by default, depending on what's available. + * Additional behaviors can be performed via scripts. + */ OTHER; }