1
0
Fork 0
mirror of https://github.com/ninjamuffin99/Funkin.git synced 2024-12-23 21:56:46 +00:00

Work in progress on Week 8 stuff (animation offsets, death animation)

This commit is contained in:
Eric Myllyoja 2022-07-06 21:49:42 -04:00
parent 05148ee4f0
commit ee3e1750df
26 changed files with 596 additions and 139 deletions

View file

@ -54,7 +54,7 @@
<library name="week5" preload="true" />
<library name="week6" preload="true" />
<library name="week7" preload="true" />
<library name="week8" preload="true" />
<library name="weekend1" preload="true" />
</section>
<section if="NO_PRELOAD_ALL">
@ -68,7 +68,7 @@
<library name="week5" preload="false" />
<library name="week6" preload="false" />
<library name="week7" preload="false" />
<library name="week8" preload="false" />
<library name="weekend1" preload="false" />
</section>
<assets path="assets/songs" library="songs" exclude="*.fla|*.ogg" if="web" />
@ -91,8 +91,8 @@
<assets path="assets/week6" library="week6" exclude="*.fla|*.mp3" unless="web" />
<assets path="assets/week7" library="week7" exclude="*.fla|*.ogg" if="web" />
<assets path="assets/week7" library="week7" exclude="*.fla|*.mp3" unless="web" />
<assets path="assets/week8" library="week8" exclude="*.fla|*.ogg" if="web" />
<assets path="assets/week8" library="week8" exclude="*.fla|*.mp3" unless="web" />
<assets path="assets/weekend1" library="weekend1" exclude="*.fla|*.ogg" if="web" />
<assets path="assets/weekend1" library="weekend1" exclude="*.fla|*.mp3" unless="web" />
<!-- <assets path='example_mods' rename='mods' embed='false'/> -->
@ -128,6 +128,7 @@
<!--haxelib name="newgrounds" unless="switch"/> -->
<haxelib name="faxe" if='switch' />
<haxelib name="polymod" />
<haxelib name="flxanimate" />
<haxelib name="thx.semver" />

View file

@ -24,6 +24,13 @@
"type": "haxelib",
"version": "2.4.0"
},
{
"name": "flxanimate",
"type": "git",
"dir": null,
"ref": "master",
"url": "https://github.com/Dot-Stuff/flxanimate"
},
{
"name": "hscript",
"type": "git",

View file

@ -118,7 +118,6 @@ class FreeplayState extends MusicBeatSubstate
addWeek(['Ugh', 'Guns', 'Stress'], 7, ['tankman']);
addWeek(["Darnell", "lit-up", "2hot"], 8, ['darnell']);
addWeek(["bro"], 1, ['gf']);
// LOAD MUSIC

View file

@ -1,5 +1,6 @@
package funkin;
import flixel.FlxSprite;
import flixel.FlxObject;
import flixel.system.FlxSound;
import flixel.util.FlxColor;
@ -50,7 +51,11 @@ class GameOverSubstate extends MusicBeatSubstate
public function new()
{
super();
}
override public function create()
{
super.create();
FlxG.sound.list.add(gameOverMusic);
gameOverMusic.stop();
@ -73,13 +78,22 @@ class GameOverSubstate extends MusicBeatSubstate
}
}
// By adding a background we can make it transparent for testing.
var bg = new FlxSprite().makeGraphic(FlxG.width, FlxG.height, FlxColor.BLACK);
bg.alpha = 0.25;
bg.scrollFactor.set();
add(bg);
// We have to remove boyfriend from the stage. Then we can add him back at the end.
boyfriend = PlayState.instance.currentStage.getBoyfriend(true);
boyfriend.isDead = true;
boyfriend.playAnimation('firstDeath');
add(boyfriend);
boyfriend.resetCharacter();
boyfriend.playAnimation('firstDeath');
cameraFollowPoint = new FlxObject(PlayState.instance.cameraFollowPoint.x, PlayState.instance.cameraFollowPoint.y, 1, 1);
cameraFollowPoint.x = boyfriend.getGraphicMidpoint().x;
cameraFollowPoint.y = boyfriend.getGraphicMidpoint().y;
add(cameraFollowPoint);
// FlxG.camera.scroll.set();
@ -124,11 +138,10 @@ class GameOverSubstate extends MusicBeatSubstate
// Start panning the camera to BF after 12 frames.
// TODO: Should this be de-hardcoded?
if (boyfriend.getCurrentAnimation().startsWith('firstDeath') && boyfriend.animation.curAnim.curFrame == 12)
{
cameraFollowPoint.x = boyfriend.getGraphicMidpoint().x;
cameraFollowPoint.y = boyfriend.getGraphicMidpoint().y;
}
//if (boyfriend.getCurrentAnimation().startsWith('firstDeath') && boyfriend.animation.curAnim.curFrame == 12)
//{
//
//}
if (gameOverMusic.playing)
{

View file

@ -171,7 +171,7 @@ class InitState extends FlxTransitionableState
#elseif FREEPLAY
FlxG.switchState(new FreeplayState());
#elseif ANIMATE
FlxG.switchState(new animate.AnimTestStage());
FlxG.switchState(new funkin.animate.dotstuff.DotStuffTestStage());
#elseif CHARTING
FlxG.switchState(new ChartingState());
#elseif STAGEBUILD
@ -179,11 +179,7 @@ class InitState extends FlxTransitionableState
#elseif FIGHT
FlxG.switchState(new PicoFight());
#elseif ANIMDEBUG
<<<<<<< HEAD
FlxG.switchState(new funkin.ui.animDebugShit.DebugBoundingState());
=======
FlxG.switchState(new DebugBoundingState());
>>>>>>> origin/feature/scripted-modules
#elseif NETTEST
FlxG.switchState(new netTest.NetTest());
#else

View file

@ -188,8 +188,10 @@ class LoadingState extends MusicBeatState
{
Paths.setCurrentLevel('tutorial');
}
else
{
else if (PlayState.storyWeek == 8) {
// TODO: Refactor this code.
Paths.setCurrentLevel("weekend1");
} else {
Paths.setCurrentLevel("week" + PlayState.storyWeek);
}
#if NO_PRELOAD_ALL

View file

@ -1,5 +1,6 @@
package funkin;
import flixel.util.FlxColor;
import flixel.FlxSubState;
import funkin.Conductor.BPMChangeEvent;
import funkin.modding.events.ScriptEvent;
@ -10,9 +11,9 @@ import funkin.modding.module.ModuleHandler;
*/
class MusicBeatSubstate extends FlxSubState
{
public function new()
public function new(bgColor:FlxColor = FlxColor.TRANSPARENT)
{
super();
super(bgColor);
}
private var curStep:Int = 0;

View file

@ -283,7 +283,9 @@ class Note extends FlxSprite
static public function fromData(data:NoteData, prevNote:Note, isSustainNote = false)
{
return new Note(data.strumTime, data.noteData, prevNote, isSustainNote);
var result = new Note(data.strumTime, data.noteData, prevNote, isSustainNote);
result.data = data;
return result;
}
}
@ -292,20 +294,18 @@ typedef RawNoteData =
var strumTime:Float;
var noteData:NoteType;
var sustainLength:Float;
var altNote:String;
var noteKind:NoteKind;
}
@:forward
abstract NoteData(RawNoteData)
{
public function new(strumTime = 0.0, noteData:NoteType = 0, sustainLength = 0.0, altNote = "", noteKind = NORMAL)
public function new(strumTime = 0.0, noteData:NoteType = 0, sustainLength = 0.0, noteKind = NORMAL)
{
this = {
strumTime: strumTime,
noteData: noteData,
sustainLength: sustainLength,
altNote: altNote,
noteKind: noteKind
}
}
@ -470,11 +470,5 @@ enum abstract NoteKind(String) from String to String
* The default note type.
*/
var NORMAL = "normal";
// Testing shiz
var PYRO_LIGHT = "pyro_light";
var PYRO_KICK = "pyro_kick";
var PYRO_TOSS = "pyro_toss";
var PYRO_COCK = "pyro_cock"; // lol
var PYRO_SHOOT = "pyro_shoot";
var ALT = "alt";
}

View file

@ -191,11 +191,7 @@ class SongLoad
noteStuff[sectionIndex].sectionNotes[noteIndex].sustainLength = arrayDipshit[2];
if (arrayDipshit.length > 3)
{
noteStuff[sectionIndex].sectionNotes[noteIndex].altNote = arrayDipshit[3];
}
if (arrayDipshit.length > 4)
{
noteStuff[sectionIndex].sectionNotes[noteIndex].noteKind = arrayDipshit[4];
noteStuff[sectionIndex].sectionNotes[noteIndex].noteKind = arrayDipshit[3];
}
}
else if (noteDataArray != null)
@ -227,7 +223,7 @@ class SongLoad
noteTypeDefShit.strumTime,
noteTypeDefShit.noteData,
noteTypeDefShit.sustainLength,
noteTypeDefShit.altNote
noteTypeDefShit.noteKind
];
noteStuff[sectionIndex].sectionNotes[noteIndex] = cast dipshitArray;
@ -252,7 +248,15 @@ class SongLoad
public static function parseJSONshit(rawJson:String):SwagSong
{
var songParsed:Dynamic = Json.parse(rawJson);
var songParsed:Dynamic;
try {
songParsed = Json.parse(rawJson);
} catch (e) {
FlxG.log.warn("Error parsing JSON: " + e.message);
trace("Error parsing JSON: " + e.message);
return null;
}
var swagShit:SwagSong = cast songParsed.song;
swagShit.difficulties = []; // reset it to default before load
swagShit.noteMap = new Map();

View file

@ -1016,8 +1016,8 @@ class ChartingState extends MusicBeatState
if (curSelectedNote != null)
{
trace('ALT NOTE SHIT');
curSelectedNote.altNote = (curSelectedNote.altNote == "alt") ? "" : "alt";
trace(curSelectedNote.altNote);
curSelectedNote.noteKind = (curSelectedNote.noteKind == "alt") ? "" : "alt";
trace(curSelectedNote.noteKind);
}
}
@ -1358,7 +1358,7 @@ class ChartingState extends MusicBeatState
var noteStrum = getStrumTime(dummyArrow.y) + sectionStartTime();
var noteData = Math.floor(FlxG.mouse.x / GRID_SIZE);
var noteSus = 0;
var noteAlt = "";
var noteKind = "";
justPlacedNote = true;
@ -1399,7 +1399,7 @@ class ChartingState extends MusicBeatState
var daNewNote:Note = new Note(noteStrum, noteData);
daNewNote.data.sustainLength = noteSus;
daNewNote.data.altNote = noteAlt;
daNewNote.data.noteKind = noteKind;
SongLoad.getSong()[curSection].sectionNotes.push(daNewNote.data);
curSelectedNote = SongLoad.getSong()[curSection].sectionNotes[SongLoad.getSong()[curSection].sectionNotes.length - 1];

View file

@ -44,7 +44,7 @@ interface INoteScriptedClass extends IScriptedClass
*
* I previously considered adding events for onKeyDown, onKeyUp, mouse events, etc.
* However, I realized that you can simply call something like the following within a module:
* `FlxG.stage.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown);`
* `FlxG.state.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown);`
* This is more efficient than adding an entire event handler for every key press.
*
* -Eric

View file

@ -1,5 +1,6 @@
package funkin.modding;
import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.modding.module.ModuleHandler;
import funkin.play.stage.StageData;
import polymod.Polymod;
@ -157,7 +158,7 @@ class PolymodHandler
return {
assetLibraryPaths: [
"songs" => "songs", "shared" => "", "tutorial" => "tutorial", "scripts" => "scripts", "week1" => "week1", "week2" => "week2",
"week3" => "week3", "week4" => "week4", "week5" => "week5", "week6" => "week6", "week7" => "week7", "week8" => "week8",
"week3" => "week3", "week4" => "week4", "week5" => "week5", "week6" => "week6", "week7" => "week7", "weekend1" => "weekend1",
]
}
}
@ -227,9 +228,10 @@ class PolymodHandler
// Reload scripted classes so stages and modules will update.
polymod.hscript.PolymodScriptClass.registerAllScriptClasses();
// Reload the stages in cache.
// TODO: Currently this causes lag since you're reading a lot of files, how to fix?
// Reload everything that is cached.
// Currently this freezes the game for a second but I guess that's tolerable?
StageDataParser.loadStageCache();
CharacterDataParser.loadCharacterCache();
ModuleHandler.loadModuleCache();
}
}

View file

@ -142,7 +142,7 @@ class ScriptEvent
public static inline final GAME_OVER:ScriptEventType = "GAME_OVER";
/**
* Called when the player presses a key to restart the game.
* Called after the player presses a key to restart the game.
* This can happen from the pause menu or the game over screen.
*
* This event IS cancelable! Canceling this event will prevent the game from restarting.

View file

@ -110,8 +110,6 @@ class HealthIcon extends FlxSprite
this.antialiasing = !isPixel;
this.flipX = playerId == 0;
initTargetSize();
}

View file

@ -28,7 +28,7 @@ class PicoFight extends MusicBeatState
override function create()
{
Paths.setCurrentLevel("week8");
Paths.setCurrentLevel("weekend1");
var bg:FlxSprite = new FlxSprite().makeGraphic(FlxG.width, FlxG.height);
bg.scrollFactor.set();

View file

@ -28,6 +28,7 @@ import funkin.play.Strumline.StrumlineArrow;
import funkin.play.Strumline.StrumlineStyle;
import funkin.play.character.BaseCharacter;
import funkin.play.character.CharacterData;
import funkin.play.scoring.Scoring;
import funkin.play.stage.Stage;
import funkin.play.stage.StageData;
import funkin.ui.PopUpStuff;
@ -279,6 +280,14 @@ class PlayState extends MusicBeatState implements IHook
{
super.create();
if (currentSong == null) {
lime.app.Application.current.window.alert(
"There was a critical error while accessing the selected song. Click OK to return to the main menu.",
"Error loading PlayState"
);
FlxG.switchState(new MainMenuState());
}
instance = this;
// Displays the camera follow point as a sprite for debug purposes.
@ -465,12 +474,13 @@ class PlayState extends MusicBeatState implements IHook
currentStageId = 'mallXmas';
case 'winter-horrorland':
currentStageId = 'mallEvil';
case 'senpai' | 'roses':
currentStageId = 'school';
case "darnell" | "lit-up" | "2hot":
// currentStageId = 'phillyStreets';
currentStageId = 'pyro';
case 'pyro':
currentStageId = 'pyro';
case 'senpai' | 'roses':
currentStageId = 'school';
case "darnell":
currentStageId = 'phillyStreets';
case 'thorns':
currentStageId = 'schoolEvil';
case 'guns' | 'stress' | 'ugh':
@ -504,6 +514,8 @@ class PlayState extends MusicBeatState implements IHook
switch (currentStageId)
{
case 'pyro' | 'phillyStreets':
gfVersion = 'nene';
case 'limoRide':
gfVersion = 'gf-car';
case 'mallXmas' | 'mallEvil':
@ -580,12 +592,19 @@ class PlayState extends MusicBeatState implements IHook
{
// We're using Eric's stage handler.
// Characters get added to the stage, not the main scene.
currentStage.addCharacter(girlfriend, GF);
currentStage.addCharacter(boyfriend, BF);
currentStage.addCharacter(dad, DAD);
if (girlfriend != null) {
currentStage.addCharacter(girlfriend, GF);
}
// Camera starts at dad.
cameraFollowPoint.setPosition(dad.cameraFocusPoint.x, dad.cameraFocusPoint.y);
if (boyfriend != null) {
currentStage.addCharacter(boyfriend, BF);
}
if (dad != null) {
currentStage.addCharacter(dad, DAD);
// Camera starts at dad.
cameraFollowPoint.setPosition(dad.cameraFocusPoint.x, dad.cameraFocusPoint.y);
}
// Redo z-indexes.
currentStage.refresh();
@ -867,7 +886,7 @@ class PlayState extends MusicBeatState implements IHook
var swagNote:Note = new Note(daStrumTime, daNoteData, oldNote, false, strumlineStyle);
// swagNote.data = songNotes;
swagNote.data.sustainLength = songNotes.sustainLength;
swagNote.data.altNote = songNotes.altNote;
swagNote.data.noteKind = songNotes.noteKind;
swagNote.scrollFactor.set(0, 0);
var susLength:Float = swagNote.data.sustainLength;
@ -881,6 +900,7 @@ class PlayState extends MusicBeatState implements IHook
var sustainNote:Note = new Note(daStrumTime + (Conductor.stepCrochet * susNote) + Conductor.stepCrochet, daNoteData, oldNote, true,
strumlineStyle);
sustainNote.data.noteKind = songNotes.noteKind;
sustainNote.scrollFactor.set();
inactiveNotes.push(sustainNote);
@ -976,6 +996,8 @@ class PlayState extends MusicBeatState implements IHook
FlxG.sound.music.time = 0;
currentStage.resetStage();
regenNoteData(); // loads the note data from start
health = 1;
songScore = 0;
@ -1037,7 +1059,9 @@ class PlayState extends MusicBeatState implements IHook
if (!event.eventCanceled)
{
// Pause updates while the substate is open, preventing the game state from advancing.
persistentUpdate = false;
// Enable drawing while the substate is open, allowing the game state to be shown behind the pause menu.
persistentDraw = true;
// There is a 1/1000 change to use a special pause menu.
@ -1062,6 +1086,21 @@ class PlayState extends MusicBeatState implements IHook
}
}
#if debug
// 1: End the song immediately.
if (FlxG.keys.justPressed.ONE)
endSong();
// 2: Gain 10% health.
if (FlxG.keys.justPressed.TWO)
health += 0.1 * 2.0;
// 3: Lose 5% health.
if (FlxG.keys.justPressed.THREE)
health -= 0.05 * 2.0;
#end
// 7: Move to the charter.
if (FlxG.keys.justPressed.SEVEN)
{
FlxG.switchState(new ChartingState());
@ -1071,25 +1110,26 @@ class PlayState extends MusicBeatState implements IHook
#end
}
// 8: Move to the offset editor.
if (FlxG.keys.justPressed.EIGHT)
FlxG.switchState(new funkin.ui.animDebugShit.DebugBoundingState());
// 9: Toggle the old icon.
if (FlxG.keys.justPressed.NINE)
iconP1.toggleOldIcon();
if (health > 2)
health = 2;
#if debug
if (FlxG.keys.justPressed.ONE)
endSong();
if (FlxG.keys.justPressed.PAGEUP)
changeSection(1);
if (FlxG.keys.justPressed.PAGEDOWN)
changeSection(-1);
#end
if (health > 2.0)
health = 2.0;
if (health < 0.0)
health = 0.0;
if (camZooming && subState == null)
{
FlxG.camera.zoom = FlxMath.lerp(defaultCameraZoom, FlxG.camera.zoom, 0.95);
@ -1139,9 +1179,6 @@ class PlayState extends MusicBeatState implements IHook
if (health <= 0 && !isPracticeMode)
{
persistentUpdate = false;
persistentDraw = false;
vocals.pause();
FlxG.sound.music.pause();
@ -1149,7 +1186,22 @@ class PlayState extends MusicBeatState implements IHook
dispatchEvent(new ScriptEvent(ScriptEvent.GAME_OVER));
openSubState(new GameOverSubstate());
// Disable updates, preventing animations in the background from playing.
persistentUpdate = false;
#if debug
if (FlxG.keys.pressed.THREE) {
// TODO: Change the key or delete this?
// In debug builds, pressing 3 to kill the player makes the background transparent.
persistentDraw = true;
} else {
#end
persistentDraw = false;
#if debug
}
#end
var gameOverSubstate = new GameOverSubstate();
openSubState(gameOverSubstate);
#if discord_rpc
// Game Over doesn't get his own variable because it's only used here
@ -1307,6 +1359,11 @@ class PlayState extends MusicBeatState implements IHook
}
#if debug
/**
* Jumps forward or backward a number of sections in the song.
* Accounts for BPM changes, does not prevent death from skipped notes.
* @param sec
*/
function changeSection(sec:Int):Void
{
FlxG.sound.music.pause();
@ -1432,30 +1489,22 @@ class PlayState extends MusicBeatState implements IHook
// boyfriend.playAnimation('hey');
vocals.volume = 1;
var score:Int = 350;
var daRating:String = "sick";
var isSick:Bool = false;
var healthMulti:Float = 1;
healthMulti *= daNote.lowStakes ? 0.002 : 0.033;
var score = Scoring.scoreNote(noteDiff, PBOT1);
var daRating = Scoring.judgeNote(noteDiff, PBOT1);
var healthMulti:Float = daNote.lowStakes ? 0.002 : 0.033;
if (noteDiff > Note.HIT_WINDOW * Note.BAD_THRESHOLD)
{
healthMulti *= 0; // no health on shit note
daRating = 'shit';
score = 50;
}
else if (noteDiff > Note.HIT_WINDOW * Note.GOOD_THRESHOLD)
{
healthMulti *= 0.2;
daRating = 'bad';
score = 100;
}
else if (noteDiff > Note.HIT_WINDOW * Note.SICK_THRESHOLD)
{
healthMulti *= 0.78;
daRating = 'good';
score = 200;
}
else
isSick = true;
@ -1795,6 +1844,7 @@ class PlayState extends MusicBeatState implements IHook
// bruh this var is bonkers i thot it was a function lmfaooo
var shouldShowComboText:Bool = (curBeat % 8 == 7) // End of measure. TODO: Is this always the correct time?
&& (SongLoad.getSong()[Std.int(curStep / 16)].mustHitSection) // Current section is BF's.
&& (combo > 5) // Don't want to show on small combos.
@ -1832,11 +1882,13 @@ class PlayState extends MusicBeatState implements IHook
if (currentStage == null)
return;
// TODO: Move this to a song event.
if (curBeat % 8 == 7 && currentSong.song == 'Bopeebo')
{
currentStage.getBoyfriend().playAnimation('hey', true);
}
// TODO: Move this to a song event.
if (curBeat % 16 == 15
&& currentSong.song == 'Tutorial'
&& currentStage.getDad().characterId == 'gf'

View file

@ -1,6 +0,0 @@
package funkin.play;
/**
* A static class which holds any functions related to scoring.
*/
class Scoring {}

View file

@ -92,7 +92,7 @@ class BaseCharacter extends Bopper
override function set_animOffsets(value:Array<Float>)
{
if (animOffsets == null)
animOffsets = [0, 0];
value = [0, 0];
if (animOffsets == value)
return value;
@ -157,6 +157,15 @@ class BaseCharacter extends Bopper
shouldBop = false;
}
/**
* Gets the value of flipX from the character data.
* `!getFlipX()` is the direction Boyfriend should face.
*/
public function getDataFlipX():Bool
{
return _data.flipX;
}
function findCountAnimations(prefix:String):Array<Int> {
var animNames:Array<String> = this.animation.getNameList();
@ -176,6 +185,27 @@ class BaseCharacter extends Bopper
return result;
}
/**
* Reset the character so it can be used at the start of the level.
* Call this when restarting the level.
*/
public function resetCharacter(resetCamera:Bool = true):Void {
// Reset the animation offsets. This will modify x and y to be the absolute position of the character.
this.animOffsets = [0, 0];
// Now we can set the x and y to be their original values without having to account for animOffsets.
this.resetPosition();
// Make sure we are playing the idle animation (to reapply animOffsets)...
this.dance();
// ...then update the hitbox so that this.width and this.height are correct.
this.updateHitbox();
// Reset the camera focus point while we're at it.
if (resetCamera)
this.resetCameraFocusPoint();
}
/**
* Set the sprite scale to the appropriate value.
* @param scale
@ -206,10 +236,14 @@ class BaseCharacter extends Bopper
override function onCreate(event:ScriptEvent):Void
{
// Camera focus point
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]);
// Make sure we are playing the idle animation...
this.dance();
// ...then update the hitbox so that this.width and this.height are correct.
this.updateHitbox();
// Without the above code, width and height (and therefore character position)
// will be based on the first animation in the sheet rather than the default animation.
this.resetCameraFocusPoint();
// Child class should have created animations by now,
// so we can query which ones are available.
@ -218,10 +252,18 @@ class BaseCharacter extends Bopper
trace('${this.animation.getNameList()}');
trace('Combo note counts: ' + this.comboNoteCounts);
trace('Drop note counts: ' + this.dropNoteCounts);
super.onCreate(event);
}
function resetCameraFocusPoint():Void
{
// Calculate the camera focus point
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]);
}
public function initHealthIcon(isOpponent:Bool):Void
{
if (!isOpponent)
@ -230,6 +272,7 @@ class BaseCharacter extends Bopper
PlayState.instance.iconP1.size.set(_data.healthIcon.scale, _data.healthIcon.scale);
PlayState.instance.iconP1.offset.x = _data.healthIcon.offsets[0];
PlayState.instance.iconP1.offset.y = _data.healthIcon.offsets[1];
PlayState.instance.iconP1.flipX = !_data.healthIcon.flipX;
}
else
{
@ -237,6 +280,7 @@ class BaseCharacter extends Bopper
PlayState.instance.iconP2.size.set(_data.healthIcon.scale, _data.healthIcon.scale);
PlayState.instance.iconP2.offset.x = _data.healthIcon.offsets[0];
PlayState.instance.iconP2.offset.y = _data.healthIcon.offsets[1];
PlayState.instance.iconP1.flipX = _data.healthIcon.flipX;
}
}
@ -255,16 +299,16 @@ class BaseCharacter extends Bopper
playDeathAnimation();
}
if (hasAnimation('idle-end') && getCurrentAnimation() == "idle" && isAnimationFinished())
playAnimation('idle-end');
if (hasAnimation('singLEFT-end') && getCurrentAnimation() == "singLEFT" && isAnimationFinished())
playAnimation('singLEFT-end');
if (hasAnimation('singDOWN-end') && getCurrentAnimation() == "singDOWN" && isAnimationFinished())
playAnimation('singDOWN-end');
if (hasAnimation('singUP-end') && getCurrentAnimation() == "singUP" && isAnimationFinished())
playAnimation('singUP-end');
if (hasAnimation('singRIGHT-end') && getCurrentAnimation() == "singRIGHT" && isAnimationFinished())
playAnimation('singRIGHT-end');
if (hasAnimation('idle-hold') && getCurrentAnimation() == "idle" && isAnimationFinished())
playAnimation('idle-hold');
if (hasAnimation('singLEFT-hold') && getCurrentAnimation() == "singLEFT" && isAnimationFinished())
playAnimation('singLEFT-hold');
if (hasAnimation('singDOWN-hold') && getCurrentAnimation() == "singDOWN" && isAnimationFinished())
playAnimation('singDOWN-hold');
if (hasAnimation('singUP-hold') && getCurrentAnimation() == "singUP" && isAnimationFinished())
playAnimation('singUP-hold');
if (hasAnimation('singRIGHT-hold') && getCurrentAnimation() == "singRIGHT" && isAnimationFinished())
playAnimation('singRIGHT-hold');
// Handle character note hold time.
if (getCurrentAnimation().startsWith("sing"))
@ -276,9 +320,8 @@ class BaseCharacter extends Bopper
var shouldStopSinging:Bool = (this.characterType == BF) ? !isHoldingNote() : true;
FlxG.watch.addQuick('singTimeMs-${characterId}', singTimeMs);
if (holdTimer > singTimeMs && shouldStopSinging && !getCurrentAnimation().endsWith("miss"))
if (holdTimer > singTimeMs && shouldStopSinging) // && !getCurrentAnimation().endsWith("miss")
{
trace(getCurrentAnimation());
// trace('holdTimer reached ${holdTimer}sec (> ${singTimeMs}), stopping sing animation');
holdTimer = 0;
dance(true);
@ -401,14 +444,14 @@ class BaseCharacter extends Bopper
if (event.note.mustPress && characterType == BF)
{
// If the note is from the same strumline, play the sing animation.
this.playSingAnimation(event.note.data.dir, false, event.note.data.altNote);
this.playSingAnimation(event.note.data.dir, false);
}
else if (!event.note.mustPress && characterType == DAD)
{
// If the note is from the same strumline, play the sing animation.
this.playSingAnimation(event.note.data.dir, false, event.note.data.altNote);
this.playSingAnimation(event.note.data.dir, false);
} else if (characterType == GF) {
if (this.comboNoteCounts.contains(event.comboCount)) {
if (event.note.mustPress && this.comboNoteCounts.contains(event.comboCount)) {
trace('Playing GF combo animation: combo${event.comboCount}');
this.playAnimation('combo${event.comboCount}', true, true);
}
@ -426,12 +469,12 @@ class BaseCharacter extends Bopper
if (event.note.mustPress && characterType == BF)
{
// If the note is from the same strumline, play the sing animation.
this.playSingAnimation(event.note.data.dir, true, event.note.data.altNote);
this.playSingAnimation(event.note.data.dir, true);
}
else if (!event.note.mustPress && characterType == DAD)
{
// If the note is from the same strumline, play the sing animation.
this.playSingAnimation(event.note.data.dir, true, event.note.data.altNote);
this.playSingAnimation(event.note.data.dir, true);
} else if (event.note.mustPress && characterType == GF) {
var dropAnim = '';
@ -466,9 +509,9 @@ class BaseCharacter extends Bopper
if (characterType == BF)
{
trace('Playing ghost miss animation...');
// If the note is from the same strumline, play the sing animation.
this.playSingAnimation(event.dir, true, null);
// trace('Playing ghost miss animation...');
this.playSingAnimation(event.dir, true);
}
}

View file

@ -109,7 +109,7 @@ class CharacterDataParser
trace(' Instantiating ${scriptedCharClassNames3.length} (Multi-Sparrow) scripted characters...');
for (charCls in scriptedCharClassNames3)
{
var character = ScriptedBaseCharacter.init(charCls, DEFAULT_CHAR_ID);
var character = ScriptedMultiSparrowCharacter.init(charCls, DEFAULT_CHAR_ID);
if (character == null)
{
trace(' Failed to instantiate scripted character: ${charCls}');
@ -362,6 +362,7 @@ class CharacterDataParser
input.healthIcon = {
id: null,
scale: null,
flipX: null,
offsets: null
};
}
@ -376,6 +377,11 @@ class CharacterDataParser
input.healthIcon.scale = DEFAULT_SCALE;
}
if (input.healthIcon.flipX == null)
{
input.healthIcon.flipX = DEFAULT_FLIPX;
}
if (input.healthIcon.offsets == null)
{
input.healthIcon.offsets = DEFAULT_OFFSETS;
@ -583,6 +589,12 @@ typedef HealthIconData =
*/
var scale:Null<Float>;
/**
* Whether to flip the health icon horizontally.
* @default false
*/
var flipX:Null<Bool>;
/**
* The offset of the health icon, in pixels.
* @default [0, 25]

View file

@ -50,8 +50,6 @@ class MultiSparrowCharacter extends BaseCharacter
buildSpritesheets();
buildAnimations();
playAnimation(_data.startingAnimation);
if (_data.isPixel)
{
this.antialiasing = false;
@ -124,7 +122,7 @@ class MultiSparrowCharacter extends BaseCharacter
if (members.exists(assetPath))
{
// Switch to a new set of sprites.
trace('Loading frames from asset path: ${assetPath}');
// trace('Loading frames from asset path: ${assetPath}');
this.frames = members.get(assetPath);
this.activeMember = assetPath;
this.setScale(_data.scale);
@ -176,7 +174,9 @@ class MultiSparrowCharacter extends BaseCharacter
public override function playAnimation(name:String, restart:Bool = false, ?ignoreOther:Bool = false):Void
{
if (!this.canPlayOtherAnims)
// Make sure we ignore other animations if we're currently playing a forced one,
// unless we're forcing a new animation.
if (!this.canPlayOtherAnims && !ignoreOther)
return;
loadFramesByAnimName(name);

View file

@ -23,8 +23,6 @@ class PackerCharacter extends BaseCharacter
loadSpritesheet();
loadAnimations();
playAnimation(_data.startingAnimation);
super.onCreate(event);
}

View file

@ -25,8 +25,6 @@ class SparrowCharacter extends BaseCharacter
loadSpritesheet();
loadAnimations();
playAnimation(_data.startingAnimation);
super.onCreate(event);
}

View file

@ -0,0 +1,198 @@
package funkin.play.scoring;
enum abstract ScoringSystem(String) {
/**
* The scoring system used in versions of the game Week 6 and older.
* Scores the player based on judgement, represented by a step function.
*/
var LEGACY;
/**
* The scoring system used in Week 7. It has tighter scoring windows than Legacy.
* Scores the player based on judgement, represented by a step function.
*/
var WEEK7;
/**
* Points Based On Timing scoring system, version 1
* Scores the player based on the offset based on timing, represented by a sigmoid function.
*/
var PBOT1;
// WIFE1
// WIFE3
}
/**
* A static class which holds any functions related to scoring.
*/
class Scoring {
/**
* Determine the score a note receives under a given scoring system.
* @param msTiming The difference between the note's time and when it was hit.
* @param scoringSystem The scoring system to use.
* @return The score the note receives.
*/
public static function scoreNote(msTiming:Float, scoringSystem:ScoringSystem = PBOT1) {
switch (scoringSystem) {
case LEGACY:
return scoreNote_LEGACY(msTiming);
case WEEK7:
return scoreNote_WEEK7(msTiming);
case PBOT1:
return scoreNote_PBOT1(msTiming);
default:
trace('ERROR: Unknown scoring system: ' + scoringSystem);
return 0;
}
}
/**
* Determine the judgement a note receives under a given scoring system.
* @param msTiming The difference between the note's time and when it was hit.
* @return The judgement the note receives.
*/
public static function judgeNote(msTiming:Float, scoringSystem:ScoringSystem = PBOT1):String {
switch (scoringSystem) {
case LEGACY:
return judgeNote_LEGACY(msTiming);
case WEEK7:
return judgeNote_WEEK7(msTiming);
case PBOT1:
return judgeNote_PBOT1(msTiming);
default:
trace('ERROR: Unknown scoring system: ' + scoringSystem);
return 'miss';
}
}
/**
* The maximum score received.
*/
public static var PBOT1_MAX_SCORE = 350;
/**
* The minimum score received.
*/
public static var PBOT1_MIN_SCORE = 0;
/**
* The threshold at which a note hit is considered perfect and always given the max score.
**/
public static var PBOT1_PERFECT_THRESHOLD = 5.0; // 5ms.
/**
* The threshold at which a note hit is considered missed and always given the min score.
**/
public static var PBOT1_MISS_THRESHOLD = (10/60) * 1000; // ~166ms
// Magic numbers used to tweak the shape of the scoring function.
public static var PBOT1_SCORING_SLOPE:Float = 0.052;
public static var PBOT1_SCORING_OFFSET:Float = 80.0;
static function scoreNote_PBOT1(msTiming:Float):Int {
// Absolute value because otherwise late hits are always given the max score.
var absTiming = Math.abs(msTiming);
if (absTiming > PBOT1_MISS_THRESHOLD) {
return PBOT1_MIN_SCORE;
} else if (absTiming < PBOT1_PERFECT_THRESHOLD) {
return PBOT1_MAX_SCORE;
} else {
// Calculate the score based on the timing using a sigmoid function.
var factor:Float = 1.0 - (1.0 / (1.0 + Math.exp(-PBOT1_SCORING_SLOPE * (absTiming - PBOT1_SCORING_OFFSET))));
var score = Std.int(PBOT1_MAX_SCORE * factor);
return score;
}
}
static function judgeNote_PBOT1(msTiming:Float):String {
return judgeNote_WEEK7(msTiming);
}
/**
* The window of time in which a note is considered to be hit, on the Funkin Legacy scoring system.
* Currently equal to 10 frames at 60fps, or ~166ms.
*/
public static var LEGACY_HIT_WINDOW:Float = (10 / 60) * 1000; // 166.67 ms hit window (10 frames at 60fps)
/**
* The threshold at which a note is considered a "Bad" hit rather than a "Shit" hit.
* Represented as a percentage of the total hit window.
*/
public static var LEGACY_BAD_THRESHOLD:Float = 0.9;
public static var LEGACY_GOOD_THRESHOLD:Float = 0.75;
public static var LEGACY_SICK_THRESHOLD:Float = 0.2;
public static var LEGACY_SHIT_SCORE = 50;
public static var LEGACY_BAD_SCORE = 100;
public static var LEGACY_GOOD_SCORE = 200;
public static var LEGACY_SICK_SCORE = 350;
static function scoreNote_LEGACY(msTiming:Float):Int {
var absTiming = Math.abs(msTiming);
if (absTiming < LEGACY_HIT_WINDOW * LEGACY_SICK_THRESHOLD) {
return LEGACY_SICK_SCORE;
} else if (absTiming < LEGACY_HIT_WINDOW * LEGACY_GOOD_THRESHOLD) {
return LEGACY_GOOD_SCORE;
} else if (absTiming < LEGACY_HIT_WINDOW * LEGACY_BAD_THRESHOLD) {
return LEGACY_BAD_SCORE;
} else if (absTiming < LEGACY_HIT_WINDOW) {
return LEGACY_SHIT_SCORE;
} else {
return 0;
}
}
static function judgeNote_LEGACY(msTiming:Float):String {
var absTiming = Math.abs(msTiming);
if (absTiming < LEGACY_HIT_WINDOW * LEGACY_SICK_THRESHOLD) {
return 'sick';
} else if (absTiming < LEGACY_HIT_WINDOW * LEGACY_GOOD_THRESHOLD) {
return 'good';
} else if (absTiming < LEGACY_HIT_WINDOW * LEGACY_BAD_THRESHOLD) {
return 'bad';
} else if (absTiming < LEGACY_HIT_WINDOW) {
return 'shit';
} else {
return 'miss';
}
}
/**
* The window of time in which a note is considered to be hit, on the Funkin Classic scoring system.
* Same as L 10 frames at 60fps, or ~166ms.
*/
public static var WEEK7_HIT_WINDOW = LEGACY_HIT_WINDOW;
public static var WEEK7_BAD_THRESHOLD = 0.8; // 80% of the hit window, or ~125ms
public static var WEEK7_GOOD_THRESHOLD = 0.55; // 55% of the hit window, or ~91ms
public static var WEEK7_SICK_THRESHOLD = 0.2; // 20% of the hit window, or ~33ms
public static var WEEK7_SHIT_SCORE = 50;
public static var WEEK7_BAD_SCORE = 100;
public static var WEEK7_GOOD_SCORE = 200;
public static var WEEK7_SICK_SCORE = 350;
static function scoreNote_WEEK7(msTiming:Float):Int {
var absTiming = Math.abs(msTiming);
if (absTiming < WEEK7_HIT_WINDOW * WEEK7_SICK_THRESHOLD) {
return WEEK7_SICK_SCORE;
} else if (absTiming < WEEK7_HIT_WINDOW * WEEK7_GOOD_THRESHOLD) {
return WEEK7_GOOD_SCORE;
} else if (absTiming < WEEK7_HIT_WINDOW * WEEK7_BAD_THRESHOLD) {
return WEEK7_BAD_SCORE;
} else if (absTiming < WEEK7_HIT_WINDOW) {
return WEEK7_SHIT_SCORE;
} else {
return 0;
}
}
static function judgeNote_WEEK7(msTiming:Float):String {
var absTiming = Math.abs(msTiming);
if (absTiming < WEEK7_HIT_WINDOW * WEEK7_SICK_THRESHOLD) {
return 'sick';
} else if (absTiming < WEEK7_HIT_WINDOW * WEEK7_GOOD_THRESHOLD) {
return 'good';
} else if (absTiming < WEEK7_HIT_WINDOW * WEEK7_BAD_THRESHOLD) {
return 'bad';
} else if (absTiming < WEEK7_HIT_WINDOW) {
return 'shit';
} else {
return 'miss';
}
}
}

View file

@ -104,11 +104,36 @@ class Bopper extends FlxSprite implements IPlayStateScriptedClass
{
super();
this.danceEvery = danceEvery;
this.animation.finishCallback = function(name)
{
if (finishCallbackMap[name] != null)
finishCallbackMap[name]();
};
this.animation.callback = this.onAnimationFrame;
this.animation.finishCallback = this.onAnimationFinished;
}
/**
* Called when an animation finishes.
* @param name The name of the animation that just finished.
*/
function onAnimationFinished(name:String) {
if (!canPlayOtherAnims) {
canPlayOtherAnims = true;
}
}
/**
* Called when the current animation's frame changes.
* @param name The name of the current animation.
* @param frameNumber The number of the current frame.
* @param frameIndex The index of the current frame.
*
* For example, if an animation was defined as having the indexes [3, 0, 1, 2],
* then the first callback would have frameNumber = 0 and frameIndex = 3.
*/
function onAnimationFrame(name:String = "", frameNumber:Int = -1, frameIndex:Int = -1) {
// Do nothing by default.
// This can be overridden by, for example, scripted characters.
// Try not to do anything expensive here, it runs many times a second.
// Sometimes this gets called with empty values? IDK why but adding defaults keeps it from crashing.
}
/**
@ -236,13 +261,6 @@ class Bopper extends FlxSprite implements IPlayStateScriptedClass
if (ignoreOther)
{
canPlayOtherAnims = false;
// doing it with this funny map, since overriding the animation.finishCallback is a bit messier IMO
finishCallbackMap[name] = function()
{
canPlayOtherAnims = true;
finishCallbackMap[name] = null;
};
}
applyAnimationOffsets(correctName);

View file

@ -64,6 +64,36 @@ class Stage extends FlxSpriteGroup implements IHook implements IPlayStateScripte
this.refresh();
}
public function resetStage():Void {
// Reset positions of characters.
if (getBoyfriend() != null) {
getBoyfriend().resetCharacter(false);
} else {
trace('STAGE RESET: No boyfriend found.');
}
if (getGirlfriend() != null) {
getGirlfriend().resetCharacter(false);
}
if (getDad() != null) {
getDad().resetCharacter(false);
}
// Reset positions of named props.
for (dataProp in _data.props) {
// Fetch the prop.
var prop:FlxSprite = getNamedProp(dataProp.name);
if (prop != null) {
// Reset the position.
prop.x = dataProp.position[0];
prop.y = dataProp.position[1];
prop.zIndex = dataProp.zIndex;
}
}
// We can assume unnamed props are not moving.
}
/**
* The default stage construction routine. Called when the stage is going to be played in.
* Instantiates each prop and adds it to the stage, while setting its parameters.
@ -253,9 +283,13 @@ class Stage extends FlxSpriteGroup implements IHook implements IPlayStateScripte
// Should display at the stage position of the character (before any offsets).
// TODO: Make this a toggle? It's useful to turn on from time to time.
var debugIcon:FlxSprite = new FlxSprite(0, 0);
var debugIcon2:FlxSprite = new FlxSprite(0, 0);
debugIcon.makeGraphic(8, 8, 0xffff00ff);
debugIcon.visible = false;
debugIcon2.makeGraphic(8, 8, 0xff00ffff);
debugIcon.visible = true;
debugIcon2.visible = true;
debugIcon.zIndex = 1000000;
debugIcon2.zIndex = 1000000;
#end
// Apply position and z-index.
@ -265,20 +299,25 @@ class Stage extends FlxSpriteGroup implements IHook implements IPlayStateScripte
case BF:
this.characters.set("bf", character);
charData = _data.characters.bf;
character.flipX = !character.flipX;
// flip offsets if flipX
character.flipX = !character.getDataFlipX();
character.initHealthIcon(false);
case GF:
this.characters.set("gf", character);
charData = _data.characters.gf;
character.flipX = character.getDataFlipX();
case DAD:
this.characters.set("dad", character);
charData = _data.characters.dad;
// flip offsets if flipX
character.flipX = character.getDataFlipX();
character.initHealthIcon(true);
default:
this.characters.set(character.characterId, character);
}
// Reset the character before adding it to the stage.
// This ensures positioning is based on the idle animation.
character.resetCharacter(true);
if (charData != null)
{
character.zIndex = charData.zIndex;
@ -289,17 +328,28 @@ class Stage extends FlxSpriteGroup implements IHook implements IPlayStateScripte
character.x = charData.position[0] - character.characterOrigin.x + character.globalOffsets[0];
character.y = charData.position[1] - character.characterOrigin.y + character.globalOffsets[1];
character.originalPosition.x = character.x;
character.originalPosition.y = character.y;
character.cameraFocusPoint.x += charData.cameraOffsets[0];
character.cameraFocusPoint.y += charData.cameraOffsets[1];
#if debug
// Draw the debug icon at the character's feet.
debugIcon.x = charData.position[0];
debugIcon.y = charData.position[1];
if (charType == BF || charType == DAD)
{
debugIcon.x = charData.position[0];
debugIcon.y = charData.position[1];
debugIcon2.x = character.x;
debugIcon2.y = character.y;
}
#end
}
// Add the character to the scene.
this.add(character);
this.add(debugIcon);
this.add(debugIcon2);
}
public inline function getGirlfriendPosition():FlxPoint

View file

@ -0,0 +1,77 @@
package funkin.util;
import flixel.math.FlxPoint;
class BezierUtil {
/**
* Linearly interpolate between two values.
* Depending on p, 0 = a, 1 = b, 0.5 = halfway between a and b.
*/
static inline function mix2(p:Float, a:Float, b:Float):Float {
return a * (1 - p) + (b * p);
}
/**
* Linearly interpolate between three values.
* Depending on p, 0 = a, 0.5 = b, 1 = c, 0.25 = halfway between a and c, etc.
*/
static inline function mix3(p:Float, a:Float, b:Float, c:Float):Float {
return mix2(p, mix2(p, a, b), mix2(p, b, c));
}
static inline function mix4(p:Float, a:Float, b:Float, c:Float, d:Float):Float {
return mix2(p, mix3(p, a, b, c), mix3(p, b, c, d));
}
static inline function mix5(p:Float, a:Float, b:Float, c:Float, d:Float, e:Float):Float {
return mix2(p, mix4(p, a, b, c, d), mix4(p, b, c, d, e));
}
static inline function mix6(p:Float, a:Float, b:Float, c:Float, d:Float, e:Float, f:Float):Float {
return mix2(p, mix5(p, a, b, c, d, e), mix5(p, b, c, d, e, f));
}
/**
* A bezier curve with two points.
* This is really just linear interpolation but whatever.
*/
public static function bezier2(p:Float, a:FlxPoint, b:FlxPoint):FlxPoint {
return new FlxPoint(mix2(p, a.x, b.x), mix2(p, a.y, b.y));
}
/**
* A bezier curve with three points.
* @param p The percentage of the way through the curve.
* @param a The start point.
* @param b The control point.
* @param c The end point.
*/
public static function bezier3(p:Float, a:FlxPoint, b:FlxPoint, c:FlxPoint):FlxPoint {
return new FlxPoint(mix3(p, a.x, b.x, c.x), mix3(p, a.y, b.y, c.y));
}
/**
* A bezier curve with four points.
* @param p The percentage of the way through the curve.
* @param a The start point.
* @param b The first control point.
* @param c The second control point.
* @param d The end point.
*/
public static function bezier4(p:Float, a:FlxPoint, b:FlxPoint, c:FlxPoint, d:FlxPoint):FlxPoint {
return new FlxPoint(mix4(p, a.x, b.x, c.x, d.x), mix4(p, a.y, b.y, c.y, d.y));
}
/**
* A bezier curve with four points.
* @param p The percentage of the way through the curve.
* @param a The start point.
* @param b The first control point.
* @param c The second control point.
* @param c The third control point.
* @param d The end point.
*/
public static function bezier5(p:Float, a:FlxPoint, b:FlxPoint, c:FlxPoint, d:FlxPoint, e:FlxPoint):FlxPoint {
return new FlxPoint(mix5(p, a.x, b.x, c.x, d.x, e.x), mix5(p, a.y, b.y, c.y, d.y, e.y));
}
}