1
0
Fork 0
mirror of https://github.com/ninjamuffin99/Funkin.git synced 2024-09-29 13:48:50 +00:00

Basic working tankman sprites and cutscenes

This commit is contained in:
EliteMasterEric 2023-02-21 20:58:15 -05:00
parent a598d88152
commit cdd8cd2ed2
27 changed files with 1749 additions and 565 deletions

View file

@ -5,6 +5,7 @@
"vshaxe.haxe-checkstyle", // Haxe code style and conventions
"vshaxe.hxcpp-debugger", // CPP debugging
"openfl.lime-vscode-extension", // Lime integration
"esbenp.prettier-vscode" // JSON formatting
"esbenp.prettier-vscode", // JSON formatting
"redhat.vscode-xml" // XML formatting
]
}

View file

@ -115,21 +115,21 @@
<assets path="assets/fonts" embed='true' />
<!-- _______________________________ Libraries ______________________________ -->
<haxelib name="openfl" />
<haxelib name="flixel" />
<haxelib name="lime" /> <!-- Game engine backend -->
<haxelib name="openfl" /> <!-- Game engine backend -->
<haxelib name="flixel" /> <!-- Game engine -->
<haxedev set='webgl' />
<!--In case you want to use the addons package-->
<haxelib name="flixel-addons" />
<haxelib name="hscript" />
<haxelib name="haxeui-core"/>
<haxelib name="haxeui-flixel"/>
<haxelib name="flixel-addons" /> <!-- Additional utilities for Flixel -->
<haxelib name="hscript" /> <!-- Scripting -->
<haxelib name="flixel-ui" />
<haxelib name="haxeui-core"/>
<haxelib name="haxeui-flixel"/>
<haxelib name="polymod" />
<haxelib name="flxanimate" />
<haxelib name="flixel-ui" /> <!-- UI framework (deprecate this? -->
<haxelib name="haxeui-core"/> <!-- UI framework -->
<haxelib name="haxeui-flixel"/> <!-- Integrate HaxeUI with Flixel -->
<haxelib name="polymod" /> <!-- Modding framework -->
<haxelib name="flxanimate" /> <!-- Texture atlas rendering -->
<haxelib name="hxcodec" /> <!-- Video playback -->
<haxelib name="thx.semver" />

View file

@ -217,7 +217,8 @@
},
{
"props": {
"character": " "
"character": " ",
"severity": "IGNORE"
},
"type": "Indentation"
},
@ -281,15 +282,22 @@
{
"props": {
"format": "^[A-Z][a-zA-Z0-9]*$",
"tokens": ["ENUM"]
"tokens": ["ABSTRACT"]
},
"type": "MemberName"
},
{
"props": {
"format": "^[_a-zA-Z][a-z][a-zA-Z0-9]*$",
"tokens": ["ABSTRACT"]
},
"type": "MemberName"
},
{
"props": {
"ignoreExtern": true,
"format": "^[a-z][a-zA-Z0-9]*$",
"tokens": ["PUBLIC", "PRIVATE", "CLASS", "ABSTRACT", "TYPEDEF"]
"format": "^[_a-z][a-zA-Z0-9]*$",
"tokens": ["PUBLIC", "PRIVATE", "CLASS", "TYPEDEF"]
},
"type": "MemberName"
},
@ -402,7 +410,6 @@
{
"props": {
"tokens": [
"=",
"+",
"-",
"*",
@ -442,6 +449,13 @@
},
"type": "OperatorWrap"
},
{
"props": {
"tokens": ["="],
"option": "eol"
},
"type": "OperatorWrap"
},
{
"props": {
"ignoreExtern": true,
@ -494,7 +508,8 @@
{
"props": {
"ignoreFormat": "^$",
"max": 2
"max": 2,
"severity": "IGNORE"
},
"type": "ReturnCount"
},

View file

@ -64,6 +64,13 @@
"type": "haxelib",
"version": "2.5.0"
},
{
"name": "hxcodec",
"type": "git",
"dir": null,
"ref": "master",
"url": "https://github.com/EliteMasterEric/hxCodec"
},
{
"name": "hxcpp",
"type": "haxelib",
@ -81,7 +88,7 @@
},
{
"name": "lime",
"type": "git",
"type": "haxelib",
"version": null
},
{
@ -104,4 +111,4 @@
"version": "0.2.2"
}
]
}
}

View file

@ -211,7 +211,7 @@ class InitState extends FlxTransitionableState
#elseif FREEPLAY
FlxG.switchState(new FreeplayState());
#elseif ANIMATE
FlxG.switchState(new funkin.animate.dotstuff.DotStuffTestStage());
FlxG.switchState(new funkin.ui.animDebugShit.FlxAnimateTest());
#elseif CHARTING
FlxG.switchState(new ChartingState());
#elseif STAGEBUILD

View file

@ -51,6 +51,11 @@ class Paths
return getPath(file, type, library);
}
public static inline function animateAtlas(path:String, library:String)
{
return getLibraryPathForce('images/$path', library);
}
inline static public function txt(key:String, ?library:String)
{
return getPath('data/$key.txt', TEXT, library);

View file

@ -49,8 +49,7 @@ class NGUtil
trace('checking NG.io version');
GAME_VER = "v" + Application.current.meta.get('version');
NG.core.calls.app.getCurrentVersion(GAME_VER).addDataHandler(function(response)
{
NG.core.calls.app.getCurrentVersion(GAME_VER).addDataHandler(function(response) {
GAME_VER = response.result.data.currentVersion;
trace('CURRENT NG VERSION: ' + GAME_VER);
callback(GAME_VER);
@ -141,8 +140,7 @@ class NGUtil
var onCancel:Void->Void = null;
if (onComplete != null)
{
onSuccess = function()
{
onSuccess = function() {
onNGLogin();
onComplete(Success);
}

View file

@ -29,8 +29,7 @@ class FlxAudioGroup extends FlxTypedGroup<FlxSound>
function set_time(time:Float):Float
{
forEachAlive(function(sound:FlxSound)
{
forEachAlive(function(sound:FlxSound) {
// account for different offsets per sound?
sound.time = time;
});
@ -52,8 +51,7 @@ class FlxAudioGroup extends FlxTypedGroup<FlxSound>
function set_volume(volume:Float):Float
{
forEachAlive(function(sound:FlxSound)
{
forEachAlive(function(sound:FlxSound) {
sound.volume = volume;
});
@ -80,8 +78,7 @@ class FlxAudioGroup extends FlxTypedGroup<FlxSound>
{
#if FLX_PITCH
trace('Setting audio pitch to ' + val);
forEachAlive(function(sound:FlxSound)
{
forEachAlive(function(sound:FlxSound) {
sound.pitch = val;
});
#end
@ -96,8 +93,7 @@ class FlxAudioGroup extends FlxTypedGroup<FlxSound>
function set_autoDestroyMembers(value:Bool):Bool
{
autoDestroyMembers = value;
forEachAlive(function(sound:FlxSound)
{
forEachAlive(function(sound:FlxSound) {
sound.autoDestroy = value;
});
return value;
@ -131,8 +127,7 @@ class FlxAudioGroup extends FlxTypedGroup<FlxSound>
*/
public function pause()
{
forEachAlive(function(sound:FlxSound)
{
forEachAlive(function(sound:FlxSound) {
sound.pause();
});
}
@ -142,8 +137,7 @@ class FlxAudioGroup extends FlxTypedGroup<FlxSound>
*/
public function play(forceRestart:Bool = false, startTime:Float = 0.0, ?endTime:Float)
{
forEachAlive(function(sound:FlxSound)
{
forEachAlive(function(sound:FlxSound) {
sound.play(forceRestart, startTime, endTime);
});
}
@ -153,8 +147,7 @@ class FlxAudioGroup extends FlxTypedGroup<FlxSound>
*/
public function resume()
{
forEachAlive(function(sound:FlxSound)
{
forEachAlive(function(sound:FlxSound) {
sound.resume();
});
}
@ -164,8 +157,7 @@ class FlxAudioGroup extends FlxTypedGroup<FlxSound>
*/
public function stop()
{
forEachAlive(function(sound:FlxSound)
{
forEachAlive(function(sound:FlxSound) {
sound.stop();
});
}
@ -188,8 +180,7 @@ class FlxAudioGroup extends FlxTypedGroup<FlxSound>
{
var deviation:Float = 0;
forEachAlive(function(sound:FlxSound)
{
forEachAlive(function(sound:FlxSound) {
if (targetTime == null) targetTime = sound.time;
else
{

View file

@ -1,28 +1,174 @@
package funkin.graphics.adobeanimate;
import flixel.util.FlxSignal.FlxTypedSignal;
import flxanimate.FlxAnimate;
import flxanimate.FlxAnimate.Settings;
import flixel.math.FlxPoint;
/**
* A sprite which provides convenience functions for rendering a texture atlas.
* A sprite which provides convenience functions for rendering a texture atlas with animations.
*/
class FlxAtlasSprite extends FlxAnimate
{
/**
* The animations this sprite has available.
* Keys are animation names, values are the animation data.
*/
var animations:Map<String, FlxAnimateAnimation>;
static final SETTINGS:Settings =
{
// ?ButtonSettings:Map<String, flxanimate.animate.FlxAnim.ButtonSettings>,
FrameRate: 24.0,
Reversed: false,
// ?OnComplete:Void -> Void,
ShowPivot: true,
Antialiasing: true,
ScrollFactor: new FlxPoint(1, 1),
// Offset: new FlxPoint(0, 0), // This is just FlxSprite.offset
};
public function new(?X:Float = 0, ?Y:Float = 0)
/**
* Signal dispatched when an animation finishes playing.
*/
public var onAnimationFinish:FlxTypedSignal<String->Void> = new FlxTypedSignal<String->Void>();
var currentAnimation:String;
var canPlayOtherAnims:Bool = true;
public function new(x:Float, y:Float, path:String)
{
super(X, Y);
super(x, y, path);
if (this.anim.curInstance == null)
{
throw 'FlxAtlasSprite not initialized properly. Are you sure the path (${path}) exists?';
}
this.antialiasing = true;
onAnimationFinish.add(cleanupAnimation);
// This defaults the sprite to play the first animation in the atlas,
// then pauses it. This ensures symbols are intialized properly.
this.anim.play('');
this.anim.pause();
}
/**
* @return A list of all the animations this sprite has available.
*/
public function listAnimations():Array<String>
{
return this.anim.getFrameLabels();
}
/**
* @param id A string ID of the animation.
* @return Whether the animation was found on this sprite.
*/
public function hasAnimation(id:String):Bool
{
return getLabelIndex(id) != -1;
}
/**
* @return The current animation being played.
*/
public function getCurrentAnimation():String
{
return this.currentAnimation;
}
/**
* Plays an animation.
* @param id A string ID of the animation to play.
* @param restart Whether to restart the animation if it is already playing.
* @param ignoreOther Whether to ignore all other animation inputs, until this one is done playing
*/
public function playAnimation(id:String, ?restart:Bool = false, ?ignoreOther:Bool = false):Void
{
// Skip if not allowed to play animations.
if ((!canPlayOtherAnims && !ignoreOther)) return;
if (id == null || id == '') id = this.currentAnimation;
if (this.currentAnimation == id && !restart)
{
if (anim.isPlaying)
{
// Skip if animation is already playing.
return;
}
else
{
// Resume animation if it's paused.
anim.play('', false, false);
}
}
// Skip if the animation doesn't exist
if (!hasAnimation(id))
{
trace('Animation ' + id + ' not found');
return;
}
// Stop the current animation if it is playing.
// This includes removing existing frame callbacks.
if (this.currentAnimation != null) this.stopAnimation();
// Add a callback to ensure `onAnimationFinish` is dispatched.
addFrameCallback(getNextFrameLabel(id), function() {
trace('Animation finished: ' + id);
onAnimationFinish.dispatch(id);
});
// Prevent other animations from playing if `ignoreOther` is true.
if (ignoreOther) canPlayOtherAnims = false;
// Move to the first frame of the animation.
goToFrameLabel(id);
this.currentAnimation = id;
}
/**
* Stops the current animation.
*/
public function stopAnimation():Void
{
if (this.currentAnimation == null) return;
this.anim.removeAllCallbacksFrom(getNextFrameLabel(this.currentAnimation));
goToFrameIndex(0);
}
function addFrameCallback(label:String, callback:Void->Void):Void
{
var frameLabel = this.anim.getFrameLabel(label);
frameLabel.add(callback);
}
inline function goToFrameLabel(label:String):Void
{
this.anim.goToFrameLabel(label);
}
inline function getNextFrameLabel(label:String):String
{
return listAnimations()[(getLabelIndex(label) + 1) % listAnimations().length];
}
inline function getLabelIndex(label:String):Int
{
return listAnimations().indexOf(label);
}
inline function goToFrameIndex(index:Int):Void
{
this.anim.curFrame = index;
}
public function cleanupAnimation(_:String):Void
{
canPlayOtherAnims = true;
this.currentAnimation = null;
this.anim.stop();
}
}
typedef FlxAnimateAnimation =
{
name:String;
startFrame:Int;
endFrame:Int;
loop:Bool;
}

View file

@ -1,4 +1,4 @@
package funkin;
package funkin.graphics.video;
import flixel.FlxBasic;
import flixel.FlxSprite;
@ -7,6 +7,9 @@ import openfl.media.Video;
import openfl.net.NetConnection;
import openfl.net.NetStream;
/**
* Plays a video via a NetStream. Only works on HTML5.
*/
class FlxVideo extends FlxBasic
{
var video:Video;

View file

@ -7,12 +7,11 @@ class Fighter extends BaseCharacter
{
public function new(?x:Float = 0, ?y:Float = 0, ?char:String = "pico-fighter")
{
super(char);
super(char, Custom);
this.x = x;
this.y = y;
animation.finishCallback = function(anim:String)
{
animation.finishCallback = function(anim:String) {
switch anim
{
case "punch low" | "punch high" | "block" | 'dodge':

View file

@ -1,14 +1,13 @@
package funkin.play;
import funkin.play.song.SongData.SongEventData;
import funkin.play.event.SongEvent.SongEventParser;
import flixel.FlxCamera;
import flixel.FlxObject;
import flixel.FlxSprite;
import flixel.FlxState;
import flixel.FlxSubState;
import flixel.addons.transition.FlxTransitionableState;
import flixel.group.FlxGroup;
import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.input.keyboard.FlxKey;
import flixel.math.FlxMath;
import flixel.math.FlxRect;
import flixel.text.FlxText;
@ -25,19 +24,20 @@ import funkin.SongLoad.SwagSong;
import funkin.charting.ChartingState;
import funkin.modding.events.ScriptEvent;
import funkin.modding.events.ScriptEventDispatcher;
import funkin.play.GameOverSubstate;
import funkin.play.HealthIcon;
import funkin.play.Strumline.StrumlineArrow;
import funkin.play.Strumline.StrumlineStyle;
import funkin.play.character.BaseCharacter;
import funkin.play.character.CharacterData;
import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.play.cutscene.VanillaCutscenes;
import funkin.play.event.SongEvent.SongEventParser;
import funkin.play.scoring.Scoring;
import funkin.play.song.Song;
import funkin.play.song.SongData.SongEventData;
import funkin.play.song.SongData.SongNoteData;
import funkin.play.song.SongData.SongPlayableChar;
import funkin.play.song.SongValidator;
import funkin.play.stage.Stage;
import funkin.play.stage.StageData;
import funkin.play.stage.StageData.StageDataParser;
import funkin.ui.PopUpStuff;
import funkin.ui.PreferencesMenu;
import funkin.ui.stageBuildShit.StageOffsetSubstate;
@ -48,6 +48,9 @@ import lime.ui.Haptic;
import Discord.DiscordClient;
#end
/**
* The gameplay state, where all the rhythm gaming happens.
*/
class PlayState extends MusicBeatState
{
/**
@ -81,10 +84,15 @@ class PlayState extends MusicBeatState
public static var isPracticeMode:Bool = false;
/**
* Whether the game is currently in a cutscene, and gameplay should be stopped.
* Whether the game is currently in an animated cutscene, and gameplay should be stopped.
*/
public static var isInCutscene:Bool = false;
/**
* Whether the game is currently in dialog, and gameplay should be stopped.
*/
public static var isInDialog:Bool = false;
/**
* Whether the game is currently in the countdown before the song resumes.
*/
@ -183,6 +191,12 @@ class PlayState extends MusicBeatState
*/
var criticalFailure:Bool = false;
/**
* How many beats between camera zooms.
* @default One camera zoom per four beats.
*/
var camZoomRate:Int = 4;
/**
* RENDER OBJECTS
*/
@ -238,6 +252,11 @@ class PlayState extends MusicBeatState
*/
public var camGame:FlxCamera;
/**
* The camera which contains, and controls visibility of, a video cutscene.
*/
public var camCutscene:FlxCamera;
/**
* PROPERTIES
*/
@ -268,9 +287,7 @@ class PlayState extends MusicBeatState
var vocals:VoicesGroup;
var vocalsFinished:Bool = false;
var camZooming:Bool = false;
var gfSpeed:Int = 1;
// private var combo:Int = 0;
var generatedMusic:Bool = false;
var startingSong:Bool = false;
@ -285,14 +302,14 @@ class PlayState extends MusicBeatState
#if discord_rpc
// Discord RPC variables
var storyDifficultyText:String = "";
var iconRPC:String = "";
var storyDifficultyText:String = '';
var iconRPC:String = '';
var songLength:Float = 0;
var detailsText:String = "";
var detailsPausedText:String = "";
var detailsText:String = '';
var detailsPausedText:String = '';
#end
override public function create()
override public function create():Void
{
super.create();
@ -453,18 +470,18 @@ class PlayState extends MusicBeatState
leftWatermarkText.cameras = [camHUD];
rightWatermarkText.cameras = [camHUD];
// if (SONG.song == 'South')
// FlxG.camera.alpha = 0.7;
// UI_camera.zoom = 1;
// cameras = [FlxG.cameras.list[1]];
// Starting song!
startingSong = true;
// TODO: Softcode cutscenes.
// TODO: Alternatively: make a song script that allows startCountdown to be called,
// then cancels the countdown, hides the UI, plays the cutscene,
// then calls PlayState.startCountdown later?
if (isStoryMode && !seenCutscene)
{
seenCutscene = true;
switch (currentSong.song.toLowerCase())
switch (currentSong_NEW.songId.toLowerCase())
{
case "winter-horrorland":
VanillaCutscenes.playHorrorStartCutscene();
@ -478,9 +495,6 @@ class PlayState extends MusicBeatState
VanillaCutscenes.playGunsCutscene();
default:
// VanillaCutscenes will call startCountdown later.
// TODO: Alternatively: make a song script that allows startCountdown to be called,
// then cancels the countdown, hides the strumline, plays the cutscene,
// then calls Countdown.performCountdown()
startCountdown();
}
}
@ -492,6 +506,10 @@ class PlayState extends MusicBeatState
#if debug
this.rightWatermarkText.text = Constants.VERSION;
#end
#if debug
FlxG.console.registerObject('playState', this);
#end
}
function get_currentChart():SongDifficulty
@ -503,7 +521,7 @@ class PlayState extends MusicBeatState
/**
* Initializes the game and HUD cameras.
*/
function initCameras()
function initCameras():Void
{
// Configure the default camera zoom level.
defaultCameraZoom = FlxCamera.defaultZoom * 1.05;
@ -511,12 +529,15 @@ class PlayState extends MusicBeatState
camGame = new SwagCamera();
camHUD = new FlxCamera();
camHUD.bgColor.alpha = 0;
camCutscene = new FlxCamera();
camCutscene.bgColor.alpha = 0;
FlxG.cameras.reset(camGame);
FlxG.cameras.add(camHUD, false);
FlxG.cameras.add(camCutscene, false);
}
function initStage()
function initStage():Void
{
if (currentSong_NEW != null)
{
@ -528,23 +549,21 @@ class PlayState extends MusicBeatState
switch (currentSong.song.toLowerCase())
{
case 'spookeez' | 'monster' | 'south':
currentStageId = "spookyMansion";
currentStageId = 'spookyMansion';
case 'pico' | 'blammed' | 'philly':
currentStageId = 'phillyTrain';
case "milf" | 'satin-panties' | 'high':
case 'milf' | 'satin-panties' | 'high':
currentStageId = 'limoRide';
case "cocoa" | 'eggnog':
case 'cocoa' | 'eggnog':
currentStageId = 'mallXmas';
case 'winter-horrorland':
currentStageId = 'mallEvil';
case 'senpai' | 'roses':
currentStageId = 'school';
case "darnell" | "lit-up" | "2hot":
case 'darnell' | 'lit-up' | '2hot':
currentStageId = 'phillyStreets';
// currentStageId = 'pyro';
case "blazin":
case 'blazin':
currentStageId = 'phillyBlazin';
// currentStageId = 'pyro';
case 'pyro':
currentStageId = 'pyro';
case 'thorns':
@ -552,13 +571,13 @@ class PlayState extends MusicBeatState
case 'guns' | 'stress' | 'ugh':
currentStageId = 'tankmanBattlefield';
default:
currentStageId = "mainStage";
currentStageId = 'mainStage';
}
// Loads the relevant stage based on its ID.
loadStage(currentStageId);
}
function initStage_NEW()
function initStage_NEW():Void
{
if (currentChart == null)
{
@ -795,11 +814,19 @@ class PlayState extends MusicBeatState
if (girlfriend != null)
{
currentStage.addCharacter(girlfriend, GF);
#if debug
FlxG.console.registerObject('gf', girlfriend);
#end
}
if (boyfriend != null)
{
currentStage.addCharacter(boyfriend, BF);
#if debug
FlxG.console.registerObject('bf', boyfriend);
#end
}
if (dad != null)
@ -807,6 +834,10 @@ class PlayState extends MusicBeatState
currentStage.addCharacter(dad, DAD);
// Camera starts at dad.
cameraFollowPoint.setPosition(dad.cameraFocusPoint.x, dad.cameraFocusPoint.y);
#if debug
FlxG.console.registerObject('dad', dad);
#end
}
// Rearrange by z-indexes.
@ -867,6 +898,10 @@ class PlayState extends MusicBeatState
// Add the stage to the scene.
this.add(currentStage);
#if debug
FlxG.console.registerObject('stage', currentStage);
#end
}
}
@ -928,8 +963,7 @@ class PlayState extends MusicBeatState
// moved senpai angry noise in here to clean up cutscene switch case lol
}
new FlxTimer().start(0.3, function(tmr:FlxTimer)
{
new FlxTimer().start(0.3, function(tmr:FlxTimer) {
black.alpha -= 0.15;
if (black.alpha > 0) tmr.reset(0.3);
@ -937,31 +971,27 @@ class PlayState extends MusicBeatState
{
if (dialogueBox != null)
{
isInCutscene = true;
isInDialog = true;
if (currentSong.song.toLowerCase() == 'thorns')
{
add(senpaiEvil);
senpaiEvil.alpha = 0;
new FlxTimer().start(0.3, function(swagTimer:FlxTimer)
{
new FlxTimer().start(0.3, function(swagTimer:FlxTimer) {
senpaiEvil.alpha += 0.15;
if (senpaiEvil.alpha < 1) swagTimer.reset();
else
{
senpaiEvil.animation.play('idle');
FlxG.sound.play(Paths.sound('Senpai_Dies'), 1, false, null, true, function()
{
FlxG.sound.play(Paths.sound('Senpai_Dies'), 1, false, null, true, function() {
remove(senpaiEvil);
remove(red);
FlxG.camera.fade(FlxColor.WHITE, 0.01, true, function()
{
FlxG.camera.fade(FlxColor.WHITE, 0.01, true, function() {
add(dialogueBox);
camHUD.visible = true;
}, true);
});
new FlxTimer().start(3.2, function(deadTime:FlxTimer)
{
new FlxTimer().start(3.2, function(deadTime:FlxTimer) {
FlxG.camera.fade(FlxColor.WHITE, 1.6, false);
});
}
@ -1016,7 +1046,7 @@ class PlayState extends MusicBeatState
function generateSong():Void
{
// FlxG.log.add(ChartParser.parse());
trace('===WARNING=== Song uses old chart format!!!!!');
Conductor.forceBPM(currentSong.bpm);
@ -1026,13 +1056,10 @@ class PlayState extends MusicBeatState
else
vocals = VoicesGroup.build(currentSong.song, null);
vocals.members[0].onComplete = function()
{
vocals.members[0].onComplete = function() {
vocalsFinished = true;
};
trace(vocals);
activeNotes = new FlxTypedGroup<Note>();
activeNotes.zIndex = 1000;
add(activeNotes);
@ -1053,8 +1080,7 @@ class PlayState extends MusicBeatState
// TODO: Fix grouped vocals
vocals = currentChart.buildVocals();
vocals.members[0].onComplete = function()
{
vocals.members[0].onComplete = function() {
vocalsFinished = true;
}
@ -1076,14 +1102,12 @@ class PlayState extends MusicBeatState
// make unspawn notes shit def empty
inactiveNotes = [];
activeNotes.forEach(function(nt)
{
activeNotes.forEach(function(nt) {
nt.followsTime = false;
FlxTween.tween(nt, {y: FlxG.height + nt.y}, 0.5,
{
ease: FlxEase.expoIn,
onComplete: function(twn)
{
onComplete: function(twn) {
nt.kill();
activeNotes.remove(nt, true);
nt.destroy();
@ -1177,8 +1201,7 @@ class PlayState extends MusicBeatState
}
}
inactiveNotes.sort(function(a:Note, b:Note):Int
{
inactiveNotes.sort(function(a:Note, b:Note):Int {
return SortUtil.byStrumtime(FlxSort.ASCENDING, a, b);
});
}
@ -1196,14 +1219,12 @@ class PlayState extends MusicBeatState
inactiveNotes = [];
// Destroy active notes.
activeNotes.forEach(function(nt)
{
activeNotes.forEach(function(nt) {
nt.followsTime = false;
FlxTween.tween(nt, {y: FlxG.height + nt.y}, 0.5,
{
ease: FlxEase.expoIn,
onComplete: function(twn)
{
onComplete: function(twn) {
nt.kill();
activeNotes.remove(nt, true);
nt.destroy();
@ -1387,10 +1408,14 @@ class PlayState extends MusicBeatState
startingSong = true;
// Reset music properly.
FlxG.sound.music.pause();
vocals.pause();
FlxG.sound.music.time = 0;
vocals.time = 0;
FlxG.sound.music.volume = 1;
vocals.volume = 1;
currentStage.resetStage();
@ -1527,7 +1552,7 @@ class PlayState extends MusicBeatState
if (health > 2.0) health = 2.0;
if (health < 0.0) health = 0.0;
if (camZooming && subState == null)
if (subState == null)
{
FlxG.camera.zoom = FlxMath.lerp(defaultCameraZoom, FlxG.camera.zoom, 0.95);
camHUD.zoom = FlxMath.lerp(1 * FlxCamera.defaultZoom, camHUD.zoom, 0.95);
@ -1546,7 +1571,6 @@ class PlayState extends MusicBeatState
switch (Conductor.currentBeat)
{
case 16:
camZooming = true;
gfSpeed = 2;
case 48:
gfSpeed = 1;
@ -1557,7 +1581,7 @@ class PlayState extends MusicBeatState
}
}
if (!isInCutscene && !_exiting)
if (!isInCutscene && !isInDialog && !_exiting)
{
// RESET = Quick Game Over Screen
if (controls.RESET)
@ -1623,8 +1647,7 @@ class PlayState extends MusicBeatState
if (generatedMusic && playerStrumline != null)
{
activeNotes.forEachAlive(function(daNote:Note)
{
activeNotes.forEachAlive(function(daNote:Note) {
if ((PreferencesMenu.getPref('downscroll') && daNote.y < -daNote.height)
|| (!PreferencesMenu.getPref('downscroll') && daNote.y > FlxG.height))
{
@ -1671,8 +1694,6 @@ class PlayState extends MusicBeatState
if (!daNote.mustPress && daNote.wasGoodHit && !daNote.tooLate)
{
if (currentSong != null && currentSong.song != 'Tutorial') camZooming = true;
var event:NoteScriptEvent = new NoteScriptEvent(ScriptEvent.NOTE_HIT, daNote, Highscore.tallies.combo, true);
dispatchEvent(event);
@ -1744,14 +1765,25 @@ class PlayState extends MusicBeatState
}
}
if (!isInCutscene) keyShit(true);
if (!isInCutscene && !isInDialog) keyShit(true);
if (isInCutscene) handleCutsceneKeys();
}
static final CUTSCENE_KEYS:Array<FlxKey> = [SPACE, ESCAPE, ENTER];
function handleCutsceneKeys():Void
{
if (FlxG.keys.anyJustPressed(CUTSCENE_KEYS))
{
VanillaCutscenes.finishCutscene();
}
}
function applyClipRect(daNote:Note):Void
{
// clipRect is applied to graphic itself so use frame Heights
var swagRect:FlxRect = new FlxRect(0, 0, daNote.frameWidth, daNote.frameHeight);
var strumLineMid = playerStrumline.y + Note.swagWidth / 2;
var strumLineMid:Float = playerStrumline.y + Note.swagWidth / 2;
if (PreferencesMenu.getPref('downscroll'))
{
@ -1883,8 +1915,7 @@ class PlayState extends MusicBeatState
camHUD.visible = false;
isInCutscene = true;
FlxG.sound.play(Paths.sound('Lights_Shut_off'), function()
{
FlxG.sound.play(Paths.sound('Lights_Shut_off'), function() {
// no camFollow so it centers on horror tree
currentSong = SongLoad.loadFromJson(storyPlaylist[0].toLowerCase() + difficulty, storyPlaylist[0]);
LoadingState.loadAndSwitchState(new PlayState());
@ -1904,7 +1935,7 @@ class PlayState extends MusicBeatState
trace('WENT TO RESULTS SCREEN!');
// unloadAssets();
camZooming = false;
camZoomRate = 0;
FlxG.camera.follow(PlayState.instance.currentStage.getGirlfriend(), null, 0.05);
FlxG.camera.targetOffset.y -= 350;
@ -1912,15 +1943,13 @@ class PlayState extends MusicBeatState
FlxTween.tween(camHUD, {alpha: 0}, 0.6);
new FlxTimer().start(0.8, _ ->
{
new FlxTimer().start(0.8, _ -> {
currentStage.getGirlfriend().animation.play("cheer");
FlxTween.tween(FlxG.camera, {zoom: 1200}, 1.1,
{
ease: FlxEase.expoIn,
onComplete: _ ->
{
onComplete: _ -> {
persistentUpdate = false;
vocals.stop();
camHUD.alpha = 1;
@ -2065,8 +2094,7 @@ class PlayState extends MusicBeatState
// HOLDS, check for sustain notes
if (holdArray.contains(true) && PlayState.instance.generatedMusic)
{
PlayState.instance.activeNotes.forEachAlive(function(daNote:Note)
{
PlayState.instance.activeNotes.forEachAlive(function(daNote:Note) {
if (daNote.isSustainNote && daNote.canBeHit && daNote.mustPress && holdArray[daNote.data.noteData]) PlayState.instance.goodNoteHit(daNote);
});
}
@ -2082,8 +2110,7 @@ class PlayState extends MusicBeatState
var directionList:Array<Int> = []; // directions that can be hit
var dumbNotes:Array<Note> = []; // notes to kill later
PlayState.instance.activeNotes.forEachAlive(function(daNote:Note)
{
PlayState.instance.activeNotes.forEachAlive(function(daNote:Note) {
if (daNote.canBeHit && daNote.mustPress && !daNote.tooLate && !daNote.wasGoodHit)
{
if (directionList.contains(daNote.data.noteData))
@ -2301,25 +2328,22 @@ class PlayState extends MusicBeatState
}
}
// Manage the camera focus, if necessary.
// controlCamera();
// HARDCODING FOR MILF ZOOMS!
if (PreferencesMenu.getPref('camera-zoom'))
{
// TODO: Move this into a song script.
if (currentSong != null
&& currentSong.song.toLowerCase() == 'milf'
&& Conductor.currentBeat >= 168
&& Conductor.currentBeat < 200
&& camZooming
&& FlxG.camera.zoom < 1.35)
&& Conductor.currentBeat < 200)
{
FlxG.camera.zoom += 0.015 * FlxCamera.defaultZoom;
camHUD.zoom += 0.03;
camZoomRate = 1;
}
if (currentSong != null && currentSong.song.toLowerCase() == 'milf' && Conductor.currentBeat >= 200)
{
camZoomRate = 4;
}
if (camZooming && FlxG.camera.zoom < (1.35 * FlxCamera.defaultZoom) && Conductor.currentBeat % 4 == 0)
if (FlxG.camera.zoom < (1.35 * FlxCamera.defaultZoom) && camZoomRate > 0 && Conductor.currentBeat % camZoomRate == 0)
{
FlxG.camera.zoom += 0.015 * FlxCamera.defaultZoom;
camHUD.zoom += 0.03;
@ -2355,8 +2379,7 @@ class PlayState extends MusicBeatState
var frameShit:Float = (1 / 24) * 2; // equals 2 frames in the animation
new FlxTimer().start(((Conductor.crochet / 1000) * 1.25) - frameShit, function(tmr)
{
new FlxTimer().start(((Conductor.crochet / 1000) * 1.25) - frameShit, function(tmr) {
animShit.forceFinish();
});
}
@ -2438,7 +2461,7 @@ class PlayState extends MusicBeatState
* Function called before opening a new substate.
* @param subState The substate to open.
*/
override function openSubState(subState:FlxSubState)
public override function openSubState(subState:FlxSubState)
{
// If there is a substate which requires the game to continue,
// then make this a condition.
@ -2464,7 +2487,7 @@ class PlayState extends MusicBeatState
* Function called before closing the current substate.
* @param subState
*/
override function closeSubState()
public override function closeSubState()
{
if (isGamePaused)
{
@ -2474,7 +2497,7 @@ class PlayState extends MusicBeatState
if (event.eventCanceled) return;
if (FlxG.sound.music != null && !startingSong && !isInCutscene) resyncVocals();
if (FlxG.sound.music != null && !startingSong && !isInCutscene && !isInDialog) resyncVocals();
// Resume the countdown.
Countdown.resumeCountdown();
@ -2494,12 +2517,14 @@ class PlayState extends MusicBeatState
* Prepares to start the countdown.
* Ends any running cutscenes, creates the strumlines, and starts the countdown.
*/
function startCountdown():Void
public function startCountdown():Void
{
var result = Countdown.performCountdown(currentStageId.startsWith('school'));
// If Countdown.performCountdown returns false, then the countdown was canceled by a script.
var result:Bool = Countdown.performCountdown(currentStageId.startsWith('school'));
if (!result) return;
isInCutscene = false;
isInDialog = false;
camHUD.visible = true;
talking = false;
@ -2521,6 +2546,8 @@ class PlayState extends MusicBeatState
if (currentStage != null) currentStage.dispatchToCharacters(event);
// TODO: Dispatch event to song script
// TODO: Dispatch event to note script
}
/**

View file

@ -1,105 +0,0 @@
package funkin.play;
import flixel.FlxSprite;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
import flixel.tweens.FlxTween;
import flixel.util.FlxColor;
import flixel.util.FlxTimer;
/**
* Static methods for playing cutscenes in the PlayState.
* TODO: Un-hardcode this shit!!!!!1!
*/
class VanillaCutscenes
{
public static function playUghCutscene():Void
{
playVideoCutscene('music/ughCutscene.mp4');
}
public static function playGunsCutscene():Void
{
playVideoCutscene('music/gunsCutscene.mp4');
}
public static function playStressCutscene():Void
{
playVideoCutscene('music/stressCutscene.mp4');
}
static var blackScreen:FlxSprite;
/**
* Plays a cutscene from a video file, then starts the countdown once the video is done.
* TODO: Cutscene is currently skipped on native platforms.
*/
static function playVideoCutscene(path:String):Void
{
PlayState.isInCutscene = true;
PlayState.instance.camHUD.visible = false;
blackScreen = new FlxSprite(-200, -200).makeGraphic(FlxG.width * 2, FlxG.height * 2, FlxColor.BLACK);
blackScreen.scrollFactor.set(0, 0);
PlayState.instance.add(blackScreen);
#if html5
var vid:FlxVideo = new FlxVideo(path);
vid.finishCallback = finishVideoCutscene;
#else
finishVideoCutscene();
#end
}
/**
* Does the cleanup to start the countdown after the video is done.
* Gets called immediately if the video can't be played.
*/
static function finishVideoCutscene():Void
{
PlayState.instance.remove(blackScreen);
blackScreen = null;
FlxTween.tween(FlxG.camera, {zoom: PlayState.defaultCameraZoom}, (Conductor.crochet / 1000) * 5, {ease: FlxEase.quadInOut});
@:privateAccess
PlayState.instance.startCountdown();
// @:privateAccess
// PlayState.instance.controlCamera();
}
public static function playHorrorStartCutscene()
{
PlayState.isInCutscene = true;
PlayState.instance.camHUD.visible = false;
blackScreen = new FlxSprite(-200, -200).makeGraphic(FlxG.width * 2, FlxG.height * 2, FlxColor.BLACK);
blackScreen.scrollFactor.set(0, 0);
PlayState.instance.add(blackScreen);
new FlxTimer().start(0.1, function(tmr:FlxTimer)
{
PlayState.instance.remove(blackScreen);
FlxG.sound.play(Paths.sound('Lights_Turn_On'));
PlayState.instance.cameraFollowPoint.y = -2050;
PlayState.instance.cameraFollowPoint.x += 200;
FlxG.camera.focusOn(PlayState.instance.cameraFollowPoint.getPosition());
FlxG.camera.zoom = 1.5;
new FlxTimer().start(0.8, function(tmr:FlxTimer)
{
PlayState.instance.camHUD.visible = true;
PlayState.instance.remove(blackScreen);
blackScreen = null;
FlxTween.tween(FlxG.camera, {zoom: PlayState.defaultCameraZoom}, 2.5,
{
ease: FlxEase.quadInOut,
onComplete: function(twn:FlxTween)
{
Countdown.performCountdown(false);
}
});
});
});
}
}

View file

@ -0,0 +1,722 @@
package funkin.play.character;
import flixel.animation.FlxAnimationController;
import flixel.FlxCamera;
import flixel.FlxSprite;
import flixel.graphics.frames.FlxFrame;
import flixel.graphics.frames.FlxFramesCollection;
import flixel.math.FlxMath;
import flixel.math.FlxPoint.FlxCallbackPoint;
import flixel.math.FlxPoint;
import flixel.math.FlxRect;
import flixel.system.FlxAssets.FlxGraphicAsset;
import flixel.util.FlxColor;
import flixel.util.FlxDestroyUtil;
import funkin.graphics.adobeanimate.FlxAtlasSprite;
import funkin.modding.events.ScriptEvent;
import funkin.play.character.CharacterData.CharacterRenderType;
import openfl.display.BitmapData;
import openfl.display.BlendMode;
/**
* Individual animation data for an AnimateAtlasCharacter.
*/
typedef AnimateAtlasAnimation =
{
name:String,
prefix:String,
offset:Null<Array<Float>>,
loop:Bool,
}
/**
* An AnimateAtlasCharacter is a Character which is rendered by
* displaying an animation derived from an Adobe Animate texture atlas spritesheet file.
*
* BaseCharacter has game logic, AnimateAtlasCharacter has only rendering logic.
* KEEP THEM SEPARATE!
*/
class AnimateAtlasCharacter extends BaseCharacter
{
// BaseCharacter extends FlxSprite but we can't make it also extend FlxAtlasSprite UGH
// I basically copied the code from FlxSpriteGroup to make the FlxAtlasSprite a "child" of this class
var mainSprite:FlxAtlasSprite;
var _skipTransformChildren:Bool = false;
var animations:Map<String, AnimateAtlasAnimation> = new Map<String, AnimateAtlasAnimation>();
var currentAnimation:String;
public function new(id:String)
{
super(id, CharacterRenderType.AnimateAtlas);
}
override function initVars():Void
{
// this.flixelType = SPRITEGROUP;
// TODO: Make `animation` a stub that redirects calls to `mainSprite`?
animation = new FlxAnimationController(this);
offset = new FlxCallbackPoint(offsetCallback);
origin = new FlxCallbackPoint(originCallback);
scale = new FlxCallbackPoint(scaleCallback);
scrollFactor = new FlxCallbackPoint(scrollFactorCallback);
scale.set(1, 1);
scrollFactor.set(1, 1);
initMotionVars();
}
override function onCreate(event:ScriptEvent):Void
{
trace('Creating Animate Atlas character: ' + this.characterId);
var atlasSprite:FlxAtlasSprite = loadAtlasSprite();
setSprite(atlasSprite);
loadAnimations();
super.onCreate(event);
}
public override function playAnimation(name:String, restart:Bool = false, ?ignoreOther:Bool = false):Void
{
if ((!canPlayOtherAnims && !ignoreOther)) return;
currentAnimation = name;
var prefix:String = getAnimationData(name).prefix;
if (prefix == null) prefix = name;
this.mainSprite.playAnimation(prefix, restart, ignoreOther);
}
function loadAtlasSprite():FlxAtlasSprite
{
trace('[ATLASCHAR] Loading sprite atlas for ${characterId}.');
var sprite:FlxAtlasSprite = new FlxAtlasSprite(0, 0, Paths.animateAtlas(_data.assetPath, 'shared'));
sprite.onAnimationFinish.removeAll();
sprite.onAnimationFinish.add(this.onAnimationFinished);
return sprite;
}
override function onAnimationFinished(prefix:String):Void
{
super.onAnimationFinished(prefix);
if (getAnimationData() != null && getAnimationData().loop)
{
playAnimation(prefix, true, false);
}
else
{
this.mainSprite.cleanupAnimation(prefix);
}
}
function setSprite(sprite:FlxAtlasSprite):Void
{
trace('[ATLASCHAR] Applying sprite properties to ${characterId}');
this.mainSprite = sprite;
var feetPos:FlxPoint = feetPosition;
this.updateHitbox();
sprite.x = this.x;
sprite.y = this.y;
sprite.alpha *= alpha;
sprite.flipX = flipX;
sprite.flipY = flipY;
sprite.scrollFactor.copyFrom(scrollFactor);
sprite.cameras = _cameras; // _cameras instead of cameras because get_cameras() will not return null
if (clipRect != null) clipRectTransform(sprite, clipRect);
}
function loadAnimations():Void
{
trace('[ATLASCHAR] Loading ${_data.animations.length} animations for ${characterId}');
var animData:Array<AnimateAtlasAnimation> = cast _data.animations;
for (anim in animData)
{
animations.set(anim.name, anim);
}
}
public override function getCurrentAnimation():String
{
return this.mainSprite.getCurrentAnimation();
}
function getAnimationData(name:String = null):AnimateAtlasAnimation
{
if (name == null) name = getCurrentAnimation();
return animations.get(name);
}
//
//
// Code copied from FlxSpriteGroup
//
//
/**
* Handy function that allows you to quickly transform one property of sprites in this group at a time.
*
* @param callback Function to transform the sprites. Example:
* `function(sprite, v:Dynamic) { s.acceleration.x = v; s.makeGraphic(10,10,0xFF000000); }`
* @param value Value which will passed to lambda function.
*/
@:generic
public function transformChildren<V>(callback:FlxAtlasSprite->V->Void, value:V):Void
{
if (_skipTransformChildren || this.mainSprite == null) return;
callback(this.mainSprite, value);
}
/**
* Calls `kill()` on the group's members and then on the group itself.
* You can revive this group later via `revive()` after this.
*/
public override function kill():Void
{
_skipTransformChildren = true;
super.kill();
_skipTransformChildren = false;
this.mainSprite.kill();
}
/**
* Revives the group.
*/
public override function revive():Void
{
_skipTransformChildren = true;
super.revive(); // calls set_exists and set_alive
_skipTransformChildren = false;
this.mainSprite.revive();
}
/**
* **WARNING:** A destroyed `FlxBasic` can't be used anymore.
* It may even cause crashes if it is still part of a group or state.
* You may want to use `kill()` instead if you want to disable the object temporarily only and `revive()` it later.
*
* This function is usually not called manually (Flixel calls it automatically during state switches for all `add()`ed objects).
*
* Override this function to `null` out variables manually or call `destroy()` on class members if necessary.
* Don't forget to call `super.destroy()`!
*/
public override function destroy():Void
{
// normally don't have to destroy FlxPoints, but these are FlxCallbackPoints!
offset = FlxDestroyUtil.destroy(offset);
origin = FlxDestroyUtil.destroy(origin);
scale = FlxDestroyUtil.destroy(scale);
scrollFactor = FlxDestroyUtil.destroy(scrollFactor);
this.mainSprite = FlxDestroyUtil.destroy(this.mainSprite);
super.destroy();
}
/**
* Check and see if any sprite in this group is currently on screen.
*
* @param Camera Specify which game camera you want. If `null`, it will just grab the first global camera.
* @return Whether the object is on screen or not.
*/
public override function isOnScreen(?camera:FlxCamera):Bool
{
if (this.mainSprite != null && this.mainSprite.exists && this.mainSprite.visible && this.mainSprite.isOnScreen(camera)) return true;
return false;
}
/**
* Checks to see if a point in 2D world space overlaps any `FlxSprite` object from this group.
*
* @param Point The point in world space you want to check.
* @param InScreenSpace Whether to take scroll factors into account when checking for overlap.
* @param Camera Specify which game camera you want. If `null`, it will just grab the first global camera.
* @return Whether or not the point overlaps this group.
*/
public override function overlapsPoint(point:FlxPoint, inScreenSpace:Bool = false, camera:FlxCamera = null):Bool
{
var result:Bool = false;
result = this.mainSprite.overlapsPoint(point, inScreenSpace, camera);
return result;
}
/**
* Checks to see if a point in 2D world space overlaps any of FlxSprite object's current displayed pixels.
* This check is ALWAYS made in screen space, and always takes scroll factors into account.
*
* @param Point The point in world space you want to check.
* @param Mask Used in the pixel hit test to determine what counts as solid.
* @param Camera Specify which game camera you want. If `null`, it will just grab the first global camera.
* @return Whether or not the point overlaps this object.
*/
public override function pixelsOverlapPoint(point:FlxPoint, Mask:Int = 0xFF, Camera:FlxCamera = null):Bool
{
var result:Bool = false;
if (this.mainSprite != null && this.mainSprite.exists && this.mainSprite.visible)
{
result = this.mainSprite.pixelsOverlapPoint(point, Mask, Camera);
}
return result;
}
public override function update(elapsed:Float):Void
{
this.mainSprite.update(elapsed);
if (moves) updateMotion(elapsed);
}
public override function draw():Void
{
this.mainSprite.draw();
#if FLX_DEBUG
if (FlxG.debugger.drawDebug) drawDebug();
#end
}
inline function xTransform(sprite:FlxSprite, x:Float):Void
sprite.x += x; // addition
inline function yTransform(sprite:FlxSprite, y:Float):Void
sprite.y += y; // addition
inline function angleTransform(sprite:FlxSprite, angle:Float):Void
sprite.angle += angle; // addition
inline function alphaTransform(sprite:FlxSprite, alpha:Float):Void
{
if (sprite.alpha != 0 || alpha == 0)
{
sprite.alpha *= alpha; // multiplication
}
else
{
sprite.alpha = 1 / alpha; // direct set to avoid stuck sprites
}
}
inline function directAlphaTransform(sprite:FlxSprite, alpha:Float):Void
sprite.alpha = alpha; // direct set
inline function facingTransform(sprite:FlxSprite, facing:Int):Void
sprite.facing = facing;
inline function flipXTransform(sprite:FlxSprite, flipX:Bool):Void
sprite.flipX = flipX;
inline function flipYTransform(sprite:FlxSprite, flipY:Bool):Void
sprite.flipY = flipY;
inline function movesTransform(sprite:FlxSprite, moves:Bool):Void
sprite.moves = moves;
inline function pixelPerfectTransform(sprite:FlxSprite, pixelPerfect:Bool):Void
sprite.pixelPerfectRender = pixelPerfect;
inline function gColorTransform(sprite:FlxSprite, color:Int):Void
sprite.color = color;
inline function blendTransform(sprite:FlxSprite, blend:BlendMode):Void
sprite.blend = blend;
inline function immovableTransform(sprite:FlxSprite, immovable:Bool):Void
sprite.immovable = immovable;
inline function visibleTransform(sprite:FlxSprite, visible:Bool):Void
sprite.visible = visible;
inline function activeTransform(sprite:FlxSprite, active:Bool):Void
sprite.active = active;
inline function solidTransform(sprite:FlxSprite, solid:Bool):Void
sprite.solid = solid;
inline function aliveTransform(sprite:FlxSprite, alive:Bool):Void
sprite.alive = alive;
inline function existsTransform(sprite:FlxSprite, exists:Bool):Void
sprite.exists = exists;
inline function cameraTransform(sprite:FlxSprite, camera:FlxCamera):Void
sprite.camera = camera;
inline function camerasTransform(sprite:FlxSprite, cameras:Array<FlxCamera>):Void
sprite.cameras = cameras;
inline function offsetTransform(sprite:FlxSprite, offset:FlxPoint):Void
sprite.offset.copyFrom(offset);
inline function originTransform(sprite:FlxSprite, origin:FlxPoint):Void
sprite.origin.copyFrom(origin);
inline function scaleTransform(sprite:FlxSprite, scale:FlxPoint):Void
sprite.scale.copyFrom(scale);
inline function scrollFactorTransform(sprite:FlxSprite, scrollFactor:FlxPoint):Void
sprite.scrollFactor.copyFrom(scrollFactor);
inline function clipRectTransform(sprite:FlxSprite, clipRect:FlxRect):Void
{
if (clipRect == null)
{
sprite.clipRect = null;
}
else
{
sprite.clipRect = FlxRect.get(clipRect.x - sprite.x + x, clipRect.y - sprite.y + y, clipRect.width, clipRect.height);
}
}
inline function offsetCallback(offset:FlxPoint):Void
transformChildren(offsetTransform, offset);
inline function originCallback(origin:FlxPoint):Void
transformChildren(originTransform, origin);
inline function scaleCallback(scale:FlxPoint):Void
transformChildren(scaleTransform, scale);
inline function scrollFactorCallback(scrollFactor:FlxPoint):Void
transformChildren(scrollFactorTransform, scrollFactor);
override function set_camera(value:FlxCamera):FlxCamera
{
if (camera != value) transformChildren(cameraTransform, value);
return super.set_camera(value);
}
override function set_cameras(value:Array<FlxCamera>):Array<FlxCamera>
{
if (cameras != value) transformChildren(camerasTransform, value);
return super.set_cameras(value);
}
override function set_exists(value:Bool):Bool
{
if (exists != value) transformChildren(existsTransform, value);
return super.set_exists(value);
}
override function set_visible(value:Bool):Bool
{
if (exists && visible != value) transformChildren(visibleTransform, value);
return super.set_visible(value);
}
override function set_active(value:Bool):Bool
{
if (exists && active != value) transformChildren(activeTransform, value);
return super.set_active(value);
}
override function set_alive(value:Bool):Bool
{
if (alive != value) transformChildren(aliveTransform, value);
return super.set_alive(value);
}
override function set_x(value:Float):Float
{
if (!exists || x == value) return x; // early return (no need to transform)
transformChildren(xTransform, value - x); // offset
x = value;
return x;
}
override function set_y(value:Float):Float
{
if (exists && y != value) transformChildren(yTransform, value - y); // offset
y = value;
return y;
}
override function set_angle(value:Float):Float
{
if (exists && angle != value) transformChildren(angleTransform, value - angle); // offset
angle = value;
return angle;
}
override function set_alpha(value:Float):Float
{
value = FlxMath.bound(value, 0, 1);
if (exists && alpha != value)
{
transformChildren(directAlphaTransform, value);
}
alpha = value;
return alpha;
}
override function set_facing(value:Int):Int
{
if (exists && facing != value) transformChildren(facingTransform, value);
facing = value;
return facing;
}
override function set_flipX(value:Bool):Bool
{
if (exists && flipX != value) transformChildren(flipXTransform, value);
flipX = value;
return flipX;
}
override function set_flipY(value:Bool):Bool
{
if (exists && flipY != value) transformChildren(flipYTransform, value);
flipY = value;
return flipY;
}
override function set_moves(value:Bool):Bool
{
if (exists && moves != value) transformChildren(movesTransform, value);
moves = value;
return moves;
}
override function set_immovable(value:Bool):Bool
{
if (exists && immovable != value) transformChildren(immovableTransform, value);
immovable = value;
return immovable;
}
override function set_solid(value:Bool):Bool
{
if (exists && solid != value) transformChildren(solidTransform, value);
return super.set_solid(value);
}
override function set_color(value:Int):Int
{
if (exists && color != value) transformChildren(gColorTransform, value);
color = value;
return color;
}
override function set_blend(value:BlendMode):BlendMode
{
if (exists && blend != value) transformChildren(blendTransform, value);
blend = value;
return blend;
}
override function set_clipRect(rect:FlxRect):FlxRect
{
if (exists) transformChildren(clipRectTransform, rect);
return super.set_clipRect(rect);
}
override function set_pixelPerfectRender(value:Bool):Bool
{
if (exists && pixelPerfectRender != value) transformChildren(pixelPerfectTransform, value);
return super.set_pixelPerfectRender(value);
}
override function set_width(value:Float):Float
{
return value;
}
override function get_width():Float
{
if (this.mainSprite == null) return 0;
return this.mainSprite.width;
}
/**
* Returns the left-most position of the left-most member.
* If there are no members, x is returned.
*
* @since 5.0.0
* @return the left-most position of the left-most member
*/
public function findMinX():Float
{
return this.mainSprite == null ? x : findMinXHelper();
}
function findMinXHelper():Float
{
return this.mainSprite.x;
}
/**
* Returns the right-most position of the right-most member.
* If there are no members, x is returned.
*
* @since 5.0.0
* @return the right-most position of the right-most member
*/
public function findMaxX():Float
{
return this.mainSprite == null ? x : findMaxXHelper();
}
function findMaxXHelper():Float
{
return this.mainSprite.x + this.mainSprite.width;
}
/**
* This functionality isn't supported in SpriteGroup
*/
override function set_height(value:Float):Float
{
return value;
}
override function get_height():Float
{
if (this.mainSprite == null) return 0;
return this.mainSprite.height;
}
/**
* Returns the top-most position of the top-most member.
* If there are no members, y is returned.
*
* @since 5.0.0
* @return the top-most position of the top-most member
*/
public function findMinY():Float
{
return this.mainSprite == null ? y : findMinYHelper();
}
function findMinYHelper():Float
{
return this.mainSprite.y;
}
/**
* Returns the top-most position of the top-most member.
* If there are no members, y is returned.
*
* @since 5.0.0
* @return the bottom-most position of the bottom-most member
*/
public function findMaxY():Float
{
return this.mainSprite == null ? y : findMaxYHelper();
}
function findMaxYHelper():Float
{
return this.mainSprite.y + this.mainSprite.height;
}
/**
* This functionality isn't supported in SpriteGroup
* @return this sprite group
*/
public override function loadGraphicFromSprite(Sprite:FlxSprite):FlxSprite
{
#if FLX_DEBUG
throw "This function is not supported in FlxSpriteGroup";
#end
return this;
}
/**
* This functionality isn't supported in SpriteGroup
* @return this sprite group
*/
public override function loadGraphic(Graphic:FlxGraphicAsset, Animated:Bool = false, Width:Int = 0, Height:Int = 0, Unique:Bool = false,
?Key:String):FlxSprite
{
return this;
}
/**
* This functionality isn't supported in SpriteGroup
* @return this sprite group
*/
public override function loadRotatedGraphic(Graphic:FlxGraphicAsset, Rotations:Int = 16, Frame:Int = -1, AntiAliasing:Bool = false, AutoBuffer:Bool = false,
?Key:String):FlxSprite
{
#if FLX_DEBUG
throw "This function is not supported in FlxSpriteGroup";
#end
return this;
}
/**
* This functionality isn't supported in SpriteGroup
* @return this sprite group
*/
public override function makeGraphic(Width:Int, Height:Int, Color:Int = FlxColor.WHITE, Unique:Bool = false, ?Key:String):FlxSprite
{
#if FLX_DEBUG
throw "This function is not supported in FlxSpriteGroup";
#end
return this;
}
override function set_pixels(value:BitmapData):BitmapData
{
return value;
}
override function set_frame(value:FlxFrame):FlxFrame
{
return value;
}
override function get_pixels():BitmapData
{
return null;
}
/**
* Internal function to update the current animation frame.
*
* @param RunOnCpp Whether the frame should also be recalculated if we're on a non-flash target
*/
override inline function calcFrame(RunOnCpp:Bool = false):Void
{
// Nothing to do here
}
/**
* This functionality isn't supported in SpriteGroup
*/
override inline function resetHelpers():Void {}
/**
* This functionality isn't supported in SpriteGroup
*/
public override inline function stamp(Brush:FlxSprite, X:Int = 0, Y:Int = 0):Void {}
override function set_frames(Frames:FlxFramesCollection):FlxFramesCollection
{
return Frames;
}
/**
* This functionality isn't supported in SpriteGroup
*/
override inline function updateColorTransform():Void {}
}

View file

@ -4,6 +4,7 @@ import flixel.math.FlxPoint;
import funkin.modding.events.ScriptEvent;
import funkin.noteStuff.NoteBasic.NoteDir;
import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.play.character.CharacterData.CharacterRenderType;
import funkin.play.stage.Bopper;
/**
@ -62,13 +63,27 @@ class BaseCharacter extends Bopper
* The absolute position of the top-left of the character.
* @return
*/
public var cornerPosition(get, null):FlxPoint;
public var cornerPosition(get, set):FlxPoint;
function get_cornerPosition():FlxPoint
{
return new FlxPoint(x, y);
}
function set_cornerPosition(value:FlxPoint):FlxPoint
{
var xDiff:Float = value.x - this.x;
var yDiff:Float = value.y - this.y;
this.cameraFocusPoint.x += xDiff;
this.cameraFocusPoint.y += yDiff;
super.set_x(value.x);
super.set_y(value.y);
return value;
}
/**
* The absolute position of the character's feet, at the bottom-center of the sprite.
*/
@ -131,7 +146,7 @@ class BaseCharacter extends Bopper
return super.set_y(value);
}
public function new(id:String)
public function new(id:String, renderType:CharacterRenderType)
{
super();
this.characterId = id;
@ -141,6 +156,10 @@ class BaseCharacter extends Bopper
{
throw 'Could not find character data for characterId: $characterId';
}
else if (_data.renderType != renderType)
{
throw 'Render type mismatch for character ($characterId): expected ${renderType}, got ${_data.renderType}';
}
else
{
this.characterName = _data.name;
@ -234,6 +253,8 @@ class BaseCharacter extends Bopper
override function onCreate(event:ScriptEvent):Void
{
super.onCreate(event);
// Make sure we are playing the idle animation...
this.dance();
// ...then update the hitbox so that this.width and this.height are correct.
@ -306,14 +327,16 @@ class BaseCharacter extends Bopper
return;
}
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');
// This logic turns the idle animation into a "lead-in" animation.
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"))
if (getCurrentAnimation().startsWith('sing'))
{
// TODO: Rework this code (and all character animations ugh)
// such that the hold time is handled by padding frames,
@ -323,7 +346,7 @@ class BaseCharacter extends Bopper
holdTimer += event.elapsed;
var singTimeMs:Float = singTimeCrochet * (Conductor.crochet * 0.001); // x beats, to ms.
if (getCurrentAnimation().endsWith("miss")) singTimeMs *= 2; // makes it feel more awkward when you miss
if (getCurrentAnimation().endsWith('miss')) singTimeMs *= 2; // makes it feel more awkward when you miss
// Without this check here, the player character would only play the `sing` animation
// for one beat, as opposed to holding it as long as the player is holding the button.
@ -348,39 +371,31 @@ class BaseCharacter extends Bopper
/**
* Since no `onBeatHit` or `dance` calls happen in GameOverSubstate,
* this regularly gets called instead.
*
* @param force Force the deathLoop animation to play, even if `firstDeath` is still playing.
*/
public function playDeathAnimation(force:Bool = false):Void
{
if (force || (getCurrentAnimation().startsWith("firstDeath") && isAnimationFinished()))
if (force || (getCurrentAnimation().startsWith('firstDeath') && isAnimationFinished()))
{
playAnimation("deathLoop" + GameOverSubstate.animationSuffix);
playAnimation('deathLoop' + GameOverSubstate.animationSuffix);
}
}
override function dance(force:Bool = false)
override function dance(force:Bool = false):Void
{
// Prevent default dancing behavior.
if (debugMode) return;
if (isDead) return;
if (debugMode || isDead) return;
if (!force)
{
if (getCurrentAnimation().startsWith("sing"))
{
return;
}
if (["hey", "cheer"].contains(getCurrentAnimation()) && !isAnimationFinished())
{
return;
}
if (getCurrentAnimation().startsWith('sing')) return;
if (['hey', 'cheer'].contains(getCurrentAnimation()) && !isAnimationFinished()) return;
}
// Prevent dancing while another animation is playing.
if (!force && getCurrentAnimation().startsWith("sing"))
{
return;
}
if (!force && getCurrentAnimation().startsWith('sing')) return;
// Otherwise, fallback to the super dance() method, which handles playing the idle animation.
super.dance();
@ -537,15 +552,18 @@ class BaseCharacter extends Bopper
* @param miss If true, play the miss animation instead of the sing animation.
* @param suffix A suffix to append to the animation name, like `alt`.
*/
public function playSingAnimation(dir:NoteDir, miss:Bool = false, suffix:String = ""):Void
public function playSingAnimation(dir:NoteDir, ?miss:Bool = false, ?suffix:String = ''):Void
{
var anim:String = 'sing${dir.nameUpper}${miss ? 'miss' : ''}${suffix != "" ? '-${suffix}' : ''}';
var anim:String = 'sing${dir.nameUpper}${miss ? 'miss' : ''}${suffix != '' ? '-${suffix}' : ''}';
// restart even if already playing, because the character might sing the same note twice.
playAnimation(anim, true);
}
}
/**
* The type of a given character sprite. Defines its default behaviors.
*/
enum CharacterType
{
/**
@ -562,7 +580,8 @@ enum CharacterType
* - 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.
* - 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;

View file

@ -1,18 +1,14 @@
package funkin.play.character;
import flixel.util.typeLimit.OneOfTwo;
import funkin.modding.events.ScriptEvent;
import funkin.modding.events.ScriptEventDispatcher;
import funkin.play.character.BaseCharacter;
import funkin.play.character.MultiSparrowCharacter;
import funkin.play.character.PackerCharacter;
import funkin.play.character.ScriptedCharacter.ScriptedAnimateAtlasCharacter;
import funkin.play.character.ScriptedCharacter.ScriptedBaseCharacter;
import funkin.play.character.ScriptedCharacter.ScriptedMultiSparrowCharacter;
import funkin.play.character.ScriptedCharacter.ScriptedPackerCharacter;
import funkin.play.character.ScriptedCharacter.ScriptedSparrowCharacter;
import funkin.play.character.SparrowCharacter;
import funkin.util.VersionUtil;
import funkin.util.assets.DataAssets;
import funkin.util.VersionUtil;
import haxe.Json;
import openfl.utils.Assets;
@ -23,12 +19,12 @@ class CharacterDataParser
* Handle breaking changes by incrementing this value
* and adding migration to the `migrateStageData()` function.
*/
public static final CHARACTER_DATA_VERSION:String = "1.0.0";
public static final CHARACTER_DATA_VERSION:String = '1.0.0';
/**
* The current version rule check for the stage data format.
*/
public static final CHARACTER_DATA_VERSION_RULE:String = "1.0.x";
public static final CHARACTER_DATA_VERSION_RULE:String = '1.0.x';
static final characterCache:Map<String, CharacterData> = new Map<String, CharacterData>();
static final characterScriptedClass:Map<String, String> = new Map<String, String>();
@ -44,14 +40,13 @@ class CharacterDataParser
{
// Clear any stages that are cached if there were any.
clearCharacterCache();
trace("Loading character cache...");
trace('Loading character cache...');
//
// UNSCRIPTED CHARACTERS
//
var charIdList:Array<String> = DataAssets.listDataFilesInPath('characters/');
var unscriptedCharIds:Array<String> = charIdList.filter(function(charId:String):Bool
{
var unscriptedCharIds:Array<String> = charIdList.filter(function(charId:String):Bool {
return !characterCache.exists(charId);
});
trace(' Fetching data for ${unscriptedCharIds.length} characters...');
@ -85,8 +80,16 @@ class CharacterDataParser
trace(' Instantiating ${scriptedCharClassNames1.length} (Sparrow) scripted characters...');
for (charCls in scriptedCharClassNames1)
{
var character = ScriptedSparrowCharacter.init(charCls, DEFAULT_CHAR_ID);
characterScriptedClass.set(character.characterId, charCls);
try
{
var character:SparrowCharacter = ScriptedSparrowCharacter.init(charCls, DEFAULT_CHAR_ID);
characterScriptedClass.set(character.characterId, charCls);
}
catch (e)
{
trace(' FAILED to instantiate scripted Sparrow character: ${charCls}');
trace(e);
}
}
}
@ -96,8 +99,16 @@ class CharacterDataParser
trace(' Instantiating ${scriptedCharClassNames2.length} (Packer) scripted characters...');
for (charCls in scriptedCharClassNames2)
{
var character = ScriptedPackerCharacter.init(charCls, DEFAULT_CHAR_ID);
characterScriptedClass.set(character.characterId, charCls);
try
{
var character:PackerCharacter = ScriptedPackerCharacter.init(charCls, DEFAULT_CHAR_ID);
characterScriptedClass.set(character.characterId, charCls);
}
catch (e)
{
trace(' FAILED to instantiate scripted Packer character: ${charCls}');
trace(e);
}
}
}
@ -107,24 +118,46 @@ class CharacterDataParser
trace(' Instantiating ${scriptedCharClassNames3.length} (Multi-Sparrow) scripted characters...');
for (charCls in scriptedCharClassNames3)
{
var character = ScriptedMultiSparrowCharacter.init(charCls, DEFAULT_CHAR_ID);
if (character == null)
try
{
trace(' Failed to instantiate scripted character: ${charCls}');
continue;
var character:MultiSparrowCharacter = ScriptedMultiSparrowCharacter.init(charCls, DEFAULT_CHAR_ID);
characterScriptedClass.set(character.characterId, charCls);
}
catch (e)
{
trace(' FAILED to instantiate scripted Multi-Sparrow character: ${charCls}');
trace(e);
}
}
}
var scriptedCharClassNames4:Array<String> = ScriptedAnimateAtlasCharacter.listScriptClasses();
if (scriptedCharClassNames4.length > 0)
{
trace(' Instantiating ${scriptedCharClassNames4.length} (Animate Atlas) scripted characters...');
for (charCls in scriptedCharClassNames4)
{
try
{
var character:AnimateAtlasCharacter = ScriptedAnimateAtlasCharacter.init(charCls, DEFAULT_CHAR_ID);
characterScriptedClass.set(character.characterId, charCls);
}
catch (e)
{
trace(' FAILED to instantiate scripted Animate Atlas character: ${charCls}');
trace(e);
}
characterScriptedClass.set(character.characterId, charCls);
}
}
// NOTE: Only instantiate the ones not populated above.
// ScriptedBaseCharacter.listScriptClasses() will pick up scripts extending the other classes.
var scriptedCharClassNames:Array<String> = ScriptedBaseCharacter.listScriptClasses();
scriptedCharClassNames = scriptedCharClassNames.filter(function(charCls:String):Bool
{
scriptedCharClassNames = scriptedCharClassNames.filter(function(charCls:String):Bool {
return !(scriptedCharClassNames1.contains(charCls)
|| scriptedCharClassNames2.contains(charCls)
|| scriptedCharClassNames3.contains(charCls));
|| scriptedCharClassNames3.contains(charCls)
|| scriptedCharClassNames4.contains(charCls));
});
if (scriptedCharClassNames.length > 0)
@ -132,7 +165,7 @@ class CharacterDataParser
trace(' Instantiating ${scriptedCharClassNames.length} (Base) scripted characters...');
for (charCls in scriptedCharClassNames)
{
var character = ScriptedBaseCharacter.init(charCls, DEFAULT_CHAR_ID);
var character:BaseCharacter = ScriptedBaseCharacter.init(charCls, DEFAULT_CHAR_ID, Custom);
if (character == null)
{
trace(' Failed to instantiate scripted character: ${charCls}');
@ -149,83 +182,101 @@ class CharacterDataParser
trace(' Successfully loaded ${Lambda.count(characterCache)} stages.');
}
/**
* Fetches data for a character and returns a BaseCharacter instance,
* ready to be added to the scene.
* @param charId The character ID to fetch.
* @return The character instance, or null if the character was not found.
*/
public static function fetchCharacter(charId:String):Null<BaseCharacter>
{
if (charId == null || charId == '')
if (charId == null || charId == '' || !characterCache.exists(charId))
{
// Gracefully handle songs that don't use this character.
// Gracefully handle songs that don't use this character,
// or throw an error if the character is missing.
if (charId != null && charId != '') trace('Failed to build character, not found in cache: ${charId}');
return null;
}
if (characterCache.exists(charId))
var charData:CharacterData = characterCache.get(charId);
var charScriptClass:String = characterScriptedClass.get(charId);
var char:BaseCharacter;
if (charScriptClass != null)
{
var charData:CharacterData = characterCache.get(charId);
var charScriptClass:String = characterScriptedClass.get(charId);
var char:BaseCharacter;
if (charScriptClass != null)
switch (charData.renderType)
{
switch (charData.renderType)
{
case CharacterRenderType.MULTISPARROW:
char = ScriptedMultiSparrowCharacter.init(charScriptClass, charId);
case CharacterRenderType.SPARROW:
char = ScriptedSparrowCharacter.init(charScriptClass, charId);
case CharacterRenderType.PACKER:
char = ScriptedPackerCharacter.init(charScriptClass, charId);
default:
// We're going to assume that the script class does the rendering.
char = ScriptedBaseCharacter.init(charScriptClass, charId);
}
case CharacterRenderType.AnimateAtlas:
char = ScriptedAnimateAtlasCharacter.init(charScriptClass, charId);
case CharacterRenderType.MultiSparrow:
char = ScriptedMultiSparrowCharacter.init(charScriptClass, charId);
case CharacterRenderType.Sparrow:
char = ScriptedSparrowCharacter.init(charScriptClass, charId);
case CharacterRenderType.Packer:
char = ScriptedPackerCharacter.init(charScriptClass, charId);
default:
// We're going to assume that the script class does the rendering.
char = ScriptedBaseCharacter.init(charScriptClass, charId, CharacterRenderType.Custom);
}
else
{
switch (charData.renderType)
{
case CharacterRenderType.MULTISPARROW:
char = new MultiSparrowCharacter(charId);
case CharacterRenderType.SPARROW:
char = new SparrowCharacter(charId);
case CharacterRenderType.PACKER:
char = new PackerCharacter(charId);
default:
trace('[WARN] Creating character with undefined renderType ${charData.renderType}');
char = new BaseCharacter(charId);
}
}
trace('Successfully instantiated character: ${charId}');
// Call onCreate only in the fetchCharacter() function, not at application initialization.
ScriptEventDispatcher.callEvent(char, new ScriptEvent(ScriptEvent.CREATE));
return char;
}
else
{
trace('Failed to build character, not found in cache: ${charId}');
switch (charData.renderType)
{
case CharacterRenderType.AnimateAtlas:
char = new AnimateAtlasCharacter(charId);
case CharacterRenderType.MultiSparrow:
char = new MultiSparrowCharacter(charId);
case CharacterRenderType.Sparrow:
char = new SparrowCharacter(charId);
case CharacterRenderType.Packer:
char = new PackerCharacter(charId);
default:
trace('[WARN] Creating character with undefined renderType ${charData.renderType}');
char = new BaseCharacter(charId, CharacterRenderType.Custom);
}
}
if (char == null)
{
trace('Failed to instantiate character: ${charId}');
return null;
}
trace('Successfully instantiated character: ${charId}');
// Call onCreate only in the fetchCharacter() function, not at application initialization.
ScriptEventDispatcher.callEvent(char, new ScriptEvent(ScriptEvent.CREATE));
return char;
}
/**
* Fetches just the character data for a character.
* @param charId The character ID to fetch.
* @return The character data, or null if the character was not found.
*/
public static function fetchCharacterData(charId:String):Null<CharacterData>
{
if (characterCache.exists(charId))
{
return characterCache.get(charId);
}
else
{
return null;
}
if (characterCache.exists(charId)) return characterCache.get(charId);
return null;
}
/**
* Lists all the valid character IDs.
* @return An array of character IDs.
*/
public static function listCharacterIds():Array<String>
{
return characterCache.keys().array();
}
/**
* Clears the character data cache.
*/
static function clearCharacterCache():Void
{
if (characterCache != null)
@ -239,7 +290,7 @@ class CharacterDataParser
}
/**
* Load a character's JSON file, parse its data, and return it.
* Load a character's JSON file and parse its data.
*
* @param charId The character to load.
* @return The character data, or null if validation failed.
@ -258,7 +309,7 @@ class CharacterDataParser
var charFilePath:String = Paths.json('characters/${charPath}');
var rawJson = Assets.getText(charFilePath).trim();
while (!StringTools.endsWith(rawJson, "}"))
while (!StringTools.endsWith(rawJson, '}'))
{
rawJson = rawJson.substr(0, rawJson.length - 1);
}
@ -266,7 +317,7 @@ class CharacterDataParser
return rawJson;
}
static function migrateCharacterData(rawJson:String, charId:String)
static function migrateCharacterData(rawJson:String, charId:String):Null<CharacterData>
{
// If you update the character data format in a breaking way,
// handle migration here by checking the `version` value.
@ -298,13 +349,13 @@ class CharacterDataParser
static final DEFAULT_FRAMERATE:Int = 24;
static final DEFAULT_ISPIXEL:Bool = false;
static final DEFAULT_LOOP:Bool = false;
static final DEFAULT_NAME:String = "Untitled Character";
static final DEFAULT_NAME:String = 'Untitled Character';
static final DEFAULT_OFFSETS:Array<Float> = [0, 0];
static final DEFAULT_HEALTHICON_OFFSETS:Array<Int> = [0, 25];
static final DEFAULT_RENDERTYPE:CharacterRenderType = CharacterRenderType.SPARROW;
static final DEFAULT_RENDERTYPE:CharacterRenderType = CharacterRenderType.Sparrow;
static final DEFAULT_SCALE:Float = 1;
static final DEFAULT_SCROLL:Array<Float> = [0, 0];
static final DEFAULT_STARTINGANIM:String = "idle";
static final DEFAULT_STARTINGANIM:String = 'idle';
/**
* Set unspecified parameters to their defaults.
@ -317,7 +368,7 @@ class CharacterDataParser
{
if (input == null)
{
// trace('ERROR: Could not parse character data for "${id}".');
trace('ERROR: Could not parse character data for "${id}".');
return null;
}
@ -471,20 +522,40 @@ class CharacterDataParser
}
}
/**
* Describes the available rendering types for a character.
*/
enum abstract CharacterRenderType(String) from String to String
{
var SPARROW = 'sparrow';
var PACKER = 'packer';
var MULTISPARROW = 'multisparrow';
// TODO: FlxSpine?
// https://api.haxeflixel.com/flixel/addons/editors/spine/FlxSpine.html
// TODO: Aseprite?
// https://lib.haxe.org/p/openfl-aseprite/
// TODO: Animate?
// https://lib.haxe.org/p/flxanimate
// TODO: REDACTED
/**
* Renders the character using a single spritesheet and XML data.
*/
public var Sparrow = 'sparrow';
/**
* Renders the character using a single spritesheet and TXT data.
*/
public var Packer = 'packer';
/**
* Renders the character using multiple spritesheets and XML data.
*/
public var MultiSparrow = 'multisparrow';
/**
* Renders the character using a spritesheet of symbols and JSON data.
*/
public var AnimateAtlas = 'animateatlas';
/**
* Renders the character using a custom method.
*/
public var Custom = 'custom';
}
/**
* The JSON data schema used to define a character.
*/
typedef CharacterData =
{
/**
@ -580,6 +651,9 @@ typedef CharacterData =
var flipX:Null<Bool>;
};
/**
* The JSON data schema used to define the health icon for a character.
*/
typedef HealthIconData =
{
/**

View file

@ -3,6 +3,7 @@ package funkin.play.character;
import flixel.graphics.frames.FlxFramesCollection;
import funkin.modding.events.ScriptEvent;
import funkin.util.assets.FlxAnimationUtil;
import funkin.play.character.CharacterData.CharacterRenderType;
/**
* For some characters which use Sparrow atlases, the spritesheets need to be split
@ -37,7 +38,7 @@ class MultiSparrowCharacter extends BaseCharacter
public function new(id:String)
{
super(id);
super(id, CharacterRenderType.MultiSparrow);
}
override function onCreate(event:ScriptEvent):Void
@ -48,7 +49,7 @@ class MultiSparrowCharacter extends BaseCharacter
super.onCreate(event);
}
function buildSprites()
function buildSprites():Void
{
buildSpritesheets();
buildAnimations();
@ -63,8 +64,11 @@ class MultiSparrowCharacter extends BaseCharacter
}
}
function buildSpritesheets()
function buildSpritesheets():Void
{
// TODO: This currently works by creating like 5 frame collections and switching between them.
// It would be better to refactor this to simply concatenate the frame collections together.
// Build the list of asset paths to use.
// Ignore nulls and duplicates.
var assetList = [_data.assetPath];

View file

@ -1,9 +1,9 @@
package funkin.play.character;
import funkin.modding.events.ScriptEvent;
import flixel.graphics.frames.FlxFramesCollection;
import funkin.modding.events.ScriptEvent;
import funkin.play.character.CharacterData.CharacterRenderType;
import funkin.util.assets.FlxAnimationUtil;
import funkin.play.character.BaseCharacter.CharacterType;
/**
* A PackerCharacter is a Character which is rendered by
@ -13,7 +13,7 @@ class PackerCharacter extends BaseCharacter
{
public function new(id:String)
{
super(id);
super(id, CharacterRenderType.Packer);
}
override function onCreate(event:ScriptEvent):Void
@ -26,7 +26,7 @@ class PackerCharacter extends BaseCharacter
super.onCreate(event);
}
function loadSpritesheet()
function loadSpritesheet():Void
{
trace('[PACKERCHAR] Loading spritesheet ${_data.assetPath} for ${characterId}');
@ -51,7 +51,7 @@ class PackerCharacter extends BaseCharacter
this.setScale(_data.scale);
}
function loadAnimations()
function loadAnimations():Void
{
trace('[PACKERCHAR] Loading ${_data.animations.length} animations for ${characterId}');

View file

@ -1,10 +1,5 @@
package funkin.play.character;
import funkin.play.character.MultiSparrowCharacter;
import funkin.play.character.PackerCharacter;
import funkin.play.character.SparrowCharacter;
import polymod.hscript.HScriptedClass;
/**
* A script that can be tied to a BaseCharacter, which persists across states.
* Create a scripted class that extends BaseCharacter to use this.
@ -13,7 +8,7 @@ import polymod.hscript.HScriptedClass;
* and can't use one of the built-in render modes.
*/
@:hscriptClass
class ScriptedBaseCharacter extends BaseCharacter implements HScriptedClass {}
class ScriptedBaseCharacter extends BaseCharacter implements polymod.hscript.HScriptedClass {}
/**
* A script that can be tied to a SparrowCharacter, which persists across states.
@ -21,7 +16,7 @@ class ScriptedBaseCharacter extends BaseCharacter implements HScriptedClass {}
* then call `super('charId')` in the constructor to use this.
*/
@:hscriptClass
class ScriptedSparrowCharacter extends SparrowCharacter implements HScriptedClass {}
class ScriptedSparrowCharacter extends SparrowCharacter implements polymod.hscript.HScriptedClass {}
/**
* A script that can be tied to a MultiSparrowCharacter, which persists across states.
@ -29,7 +24,7 @@ class ScriptedSparrowCharacter extends SparrowCharacter implements HScriptedClas
* then call `super('charId')` in the constructor to use this.
*/
@:hscriptClass
class ScriptedMultiSparrowCharacter extends MultiSparrowCharacter implements HScriptedClass {}
class ScriptedMultiSparrowCharacter extends MultiSparrowCharacter implements polymod.hscript.HScriptedClass {}
/**
* A script that can be tied to a PackerCharacter, which persists across states.
@ -37,4 +32,12 @@ class ScriptedMultiSparrowCharacter extends MultiSparrowCharacter implements HSc
* then call `super('charId')` in the constructor to use this.
*/
@:hscriptClass
class ScriptedPackerCharacter extends PackerCharacter implements HScriptedClass {}
class ScriptedPackerCharacter extends PackerCharacter implements polymod.hscript.HScriptedClass {}
/**
* A script that can be tied to an AnimateAtlasCharacter, which persists across states.
* Create a scripted class that extends AnimateAtlasCharacter,
* then call `super('charId')` in the constructor to use this.
*/
@:hscriptClass
class ScriptedAnimateAtlasCharacter extends AnimateAtlasCharacter implements polymod.hscript.HScriptedClass {}

View file

@ -3,6 +3,7 @@ package funkin.play.character;
import funkin.modding.events.ScriptEvent;
import funkin.util.assets.FlxAnimationUtil;
import flixel.graphics.frames.FlxFramesCollection;
import funkin.play.character.CharacterData.CharacterRenderType;
/**
* A SparrowCharacter is a Character which is rendered by
@ -15,7 +16,7 @@ class SparrowCharacter extends BaseCharacter
{
public function new(id:String)
{
super(id);
super(id, CharacterRenderType.Sparrow);
}
override function onCreate(event:ScriptEvent):Void

View file

@ -0,0 +1,125 @@
package funkin.play.cutscene;
import hxcodec.flixel.FlxVideoSprite;
import hxcodec.flixel.FlxCutsceneState;
import flixel.FlxSprite;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
import flixel.util.FlxColor;
import flixel.util.FlxTimer;
/**
* Static methods for playing cutscenes in the PlayState.
* TODO: Un-hardcode this shit!!!!!1!
*/
class VanillaCutscenes
{
/**
* Well, well, well, what have we got here?
*/
public static function playUghCutscene():Void
{
playVideoCutscene('music/ughCutscene.mp4');
}
/**
* Nice bars for an ugly, boring teenager!
*/
public static function playGunsCutscene():Void
{
playVideoCutscene('music/gunsCutscene.mp4');
}
/**
* Don't you have a school to shoot up?
*/
public static function playStressCutscene():Void
{
playVideoCutscene('music/stressCutscene.mp4');
}
static var blackScreen:FlxSprite;
/**
* Plays a cutscene from a video file, then starts the countdown once the video is done.
* TODO: Cutscene is currently skipped on native platforms.
*/
static function playVideoCutscene(path:String):Void
{
// Tell PlayState to stop the song until the video is done.
PlayState.isInCutscene = true;
PlayState.instance.camHUD.visible = false;
// Display a black screen to hide the game while the video is playing.
blackScreen = new FlxSprite(-200, -200).makeGraphic(FlxG.width * 2, FlxG.height * 2, FlxColor.BLACK);
blackScreen.scrollFactor.set(0, 0);
blackScreen.cameras = [PlayState.instance.camCutscene];
PlayState.instance.add(blackScreen);
#if html5
// Video displays OVER the FlxState.
vid = new FlxVideo(path);
vid.finishCallback = finishCutscene;
#else
// Video displays OVER the FlxState.
vid = new FlxVideoSprite(0, 0);
vid.cameras = [PlayState.instance.camCutscene];
PlayState.instance.add(vid);
vid.playVideo(Paths.file(path), false);
vid.onEndReached.add(finishCutscene.bind(0.5));
#end
}
static var vid:#if html5 FlxVideo #else FlxVideoSprite #end;
/**
* Does the cleanup to start the countdown after the video is done.
* Gets called immediately if the video can't be played.
*/
public static function finishCutscene(?transitionTime:Float = 2.5):Void
{
trace('ALERT: Finish cutscene called!');
#if html5
#else
vid.stop();
PlayState.instance.remove(vid);
#end
PlayState.instance.camHUD.visible = true;
FlxTween.tween(blackScreen, {alpha: 0}, transitionTime,
{
ease: FlxEase.quadInOut,
onComplete: function(twn:FlxTween) {
PlayState.instance.remove(blackScreen);
blackScreen = null;
}
});
FlxTween.tween(FlxG.camera, {zoom: PlayState.defaultCameraZoom}, transitionTime,
{
ease: FlxEase.quadInOut,
onComplete: function(twn:FlxTween) {
PlayState.instance.startCountdown();
}
});
}
/**
* FNF corruption mod???
*/
public static function playHorrorStartCutscene():Void
{
PlayState.isInCutscene = true;
PlayState.instance.camHUD.visible = false;
blackScreen = new FlxSprite(-200, -200).makeGraphic(FlxG.width * 2, FlxG.height * 2, FlxColor.BLACK);
blackScreen.scrollFactor.set(0, 0);
PlayState.instance.add(blackScreen);
new FlxTimer().start(0.1, _ -> finishCutscene(2.5));
}
}

View file

@ -47,17 +47,17 @@ class FocusCameraSongEvent extends SongEvent
super('FocusCamera');
}
public override function handleEvent(data:SongEventData)
public override function handleEvent(data:SongEventData):Void
{
// Does nothing if there is no PlayState camera or stage.
if (PlayState.instance == null || PlayState.instance.currentStage == null) return;
var posX = data.getFloat('x');
var posX:Null<Float> = data.getFloat('x');
if (posX == null) posX = 0.0;
var posY = data.getFloat('y');
var posY:Null<Float> = data.getFloat('y');
if (posY == null) posY = 0.0;
var char = data.getInt('char');
var char:Null<Int> = data.getInt('char');
if (char == null) char = cast data.value;
@ -65,29 +65,45 @@ class FocusCameraSongEvent extends SongEvent
{
case -1: // Position
trace('Focusing camera on static position.');
var xTarget = posX;
var yTarget = posY;
var xTarget:Float = posX;
var yTarget:Float = posY;
PlayState.instance.cameraFollowPoint.setPosition(xTarget, yTarget);
case 0: // Boyfriend
// Focus the camera on the player.
if (PlayState.instance.currentStage.getBoyfriend() == null)
{
trace('No BF to focus on.');
return;
}
trace('Focusing camera on player.');
var xTarget = PlayState.instance.currentStage.getBoyfriend().cameraFocusPoint.x + posX;
var yTarget = PlayState.instance.currentStage.getBoyfriend().cameraFocusPoint.y + posY;
var xTarget:Float = PlayState.instance.currentStage.getBoyfriend().cameraFocusPoint.x + posX;
var yTarget:Float = PlayState.instance.currentStage.getBoyfriend().cameraFocusPoint.y + posY;
PlayState.instance.cameraFollowPoint.setPosition(xTarget, yTarget);
case 1: // Dad
// Focus the camera on the dad.
if (PlayState.instance.currentStage.getDad() == null)
{
trace('No dad to focus on.');
return;
}
trace('Focusing camera on dad.');
var xTarget = PlayState.instance.currentStage.getDad().cameraFocusPoint.x + posX;
var yTarget = PlayState.instance.currentStage.getDad().cameraFocusPoint.y + posY;
trace(PlayState.instance.currentStage.getDad());
var xTarget:Float = PlayState.instance.currentStage.getDad().cameraFocusPoint.x + posX;
var yTarget:Float = PlayState.instance.currentStage.getDad().cameraFocusPoint.y + posY;
PlayState.instance.cameraFollowPoint.setPosition(xTarget, yTarget);
case 2: // Girlfriend
// Focus the camera on the girlfriend.
if (PlayState.instance.currentStage.getGirlfriend() == null)
{
trace('No GF to focus on.');
return;
}
trace('Focusing camera on girlfriend.');
var xTarget = PlayState.instance.currentStage.getGirlfriend().cameraFocusPoint.x + posX;
var yTarget = PlayState.instance.currentStage.getGirlfriend().cameraFocusPoint.y + posY;
var xTarget:Float = PlayState.instance.currentStage.getGirlfriend().cameraFocusPoint.x + posX;
var yTarget:Float = PlayState.instance.currentStage.getGirlfriend().cameraFocusPoint.y + posY;
PlayState.instance.cameraFollowPoint.setPosition(xTarget, yTarget);
default:
@ -97,7 +113,7 @@ class FocusCameraSongEvent extends SongEvent
public override function getTitle():String
{
return "Focus Camera";
return 'Focus Camera';
}
/**

View file

@ -1,5 +1,8 @@
package funkin.play.scoring;
/**
* Which system to use when scoring and judging notes.
*/
enum abstract ScoringSystem(String)
{
/**
@ -19,9 +22,6 @@ enum abstract ScoringSystem(String)
* Scores the player based on the offset based on timing, represented by a sigmoid function.
*/
var PBOT1;
// WIFE1
// WIFE3
}
/**
@ -35,161 +35,229 @@ class Scoring
* @param scoringSystem The scoring system to use.
* @return The score the note receives.
*/
public static function scoreNote(msTiming:Float, scoringSystem:ScoringSystem = PBOT1)
public static function scoreNote(msTiming:Float, scoringSystem:ScoringSystem = PBOT1):Int
{
switch (scoringSystem)
return switch (scoringSystem)
{
case LEGACY:
return scoreNote_LEGACY(msTiming);
case WEEK7:
return scoreNote_WEEK7(msTiming);
case PBOT1:
return scoreNote_PBOT1(msTiming);
case LEGACY: scoreNoteLEGACY(msTiming);
case WEEK7: scoreNoteWEEK7(msTiming);
case PBOT1: scoreNotePBOT1(msTiming);
default:
trace('ERROR: Unknown scoring system: ' + scoringSystem);
return 0;
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.
* @param scoringSystem The scoring system to use.
* @return The judgement the note receives.
*/
public static function judgeNote(msTiming:Float, scoringSystem:ScoringSystem = PBOT1):String
{
switch (scoringSystem)
return switch (scoringSystem)
{
case LEGACY:
return judgeNote_LEGACY(msTiming);
case WEEK7:
return judgeNote_WEEK7(msTiming);
case PBOT1:
return judgeNote_PBOT1(msTiming);
case LEGACY: judgeNoteLEGACY(msTiming);
case WEEK7: judgeNoteWEEK7(msTiming);
case PBOT1: judgeNotePBOT1(msTiming);
default:
trace('ERROR: Unknown scoring system: ' + scoringSystem);
return 'miss';
'miss';
}
}
/**
* The maximum score received.
* The maximum score a note can receive.
*/
public static var PBOT1_MAX_SCORE = 350;
public static final PBOT1_MAX_SCORE:Int = 500;
/**
* The minimum score received.
* The offset of the sigmoid curve for the scoring function.
*/
public static var PBOT1_MIN_SCORE = 0;
public static final PBOT1_SCORING_OFFSET:Float = 54.99;
/**
* The slope of the sigmoid curve for the scoring function.
*/
public static final PBOT1_SCORING_SLOPE:Float = 0.080;
/**
* The minimum score a note can receive while still being considered a hit.
*/
public static final PBOT1_MIN_SCORE:Float = 9.0;
/**
* The score a note receives when it is missed.
*/
public static final PBOT1_MISS_SCORE:Int = 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.
*/
public static final PBOT1_PERFECT_THRESHOLD:Float = 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
* The threshold at which a note hit is considered missed.
* `160ms`
*/
public static final PBOT1_MISS_THRESHOLD:Float = 160.0;
// 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;
/**
* The time within which a note is considered to have been hit with the Killer judgement.
* `~7.5% of the hit window, or 12.5ms`
*/
public static final PBOT1_KILLER_THRESHOLD:Float = 12.5;
static function scoreNote_PBOT1(msTiming:Float):Int
/**
* The time within which a note is considered to have been hit with the Sick judgement.
* `~25% of the hit window, or 45ms`
*/
public static final PBOT1_SICK_THRESHOLD:Float = 45.0;
/**
* The time within which a note is considered to have been hit with the Good judgement.
* `~55% of the hit window, or 90ms`
*/
public static final PBOT1_GOOD_THRESHOLD:Float = 90.0;
/**
* The time within which a note is considered to have been hit with the Bad judgement.
* `~85% of the hit window, or 135ms`
*/
public static final PBOT1_BAD_THRESHOLD:Float = 135.0;
/**
* The time within which a note is considered to have been hit with the Shit judgement.
* `100% of the hit window, or 160ms`
*/
public static final PBOT1_SHIT_THRESHOLD:Float = 160.0;
static function scoreNotePBOT1(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 absTiming:Float = Math.abs(msTiming);
var score = Std.int(PBOT1_MAX_SCORE * factor);
return switch (absTiming)
{
case(_ > PBOT1_MISS_THRESHOLD) => true:
PBOT1_MISS_SCORE;
case(_ < PBOT1_PERFECT_THRESHOLD) => true:
PBOT1_MAX_SCORE;
default:
var factor:Float = 1.0 - (1.0 / (1.0 + Math.exp(-PBOT1_SCORING_SLOPE * (absTiming - PBOT1_SCORING_OFFSET))));
var score:Int = Std.int(PBOT1_MAX_SCORE * factor + PBOT1_MIN_SCORE);
return score;
score;
}
}
static function judgeNote_PBOT1(msTiming:Float):String
static function judgeNotePBOT1(msTiming:Float):String
{
return judgeNote_WEEK7(msTiming);
var absTiming:Float = Math.abs(msTiming);
return switch (absTiming)
{
case(_ < PBOT1_KILLER_THRESHOLD) => true:
'killer';
case(_ < PBOT1_SICK_THRESHOLD) => true:
'sick';
case(_ < PBOT1_GOOD_THRESHOLD) => true:
'good';
case(_ < PBOT1_BAD_THRESHOLD) => true:
'bad';
case(_ < PBOT1_SHIT_THRESHOLD) => true:
'shit';
default:
'miss';
}
}
/**
* 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)
public static final 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.
* The threshold at which a note is considered a "Sick" hit rather than another judgement.
* Represented as a percentage of the total hit window.
*/
public static var LEGACY_BAD_THRESHOLD:Float = 0.9;
public static final LEGACY_SICK_THRESHOLD:Float = 0.2;
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;
/**
* The threshold at which a note is considered a "Good" hit rather than another judgement.
* Represented as a percentage of the total hit window.
*/
public static final LEGACY_GOOD_THRESHOLD:Float = 0.75;
static function scoreNote_LEGACY(msTiming:Float):Int
/**
* The threshold at which a note is considered a "Bad" hit rather than another judgement.
* Represented as a percentage of the total hit window.
*/
public static final LEGACY_BAD_THRESHOLD:Float = 0.9;
/**
* The score a note receives when hit within the Shit threshold, rather than a miss.
* Represented as a percentage of the total hit window.
*/
public static final LEGACY_SHIT_THRESHOLD:Float = 1.0;
/**
* The score a note receives when hit within the Sick threshold.
*/
public static final LEGACY_SICK_SCORE:Int = 350;
/**
* The score a note receives when hit within the Good threshold.
*/
public static final LEGACY_GOOD_SCORE:Int = 200;
/**
* The score a note receives when hit within the Bad threshold.
*/
public static final LEGACY_BAD_SCORE:Int = 100;
/**
* The score a note receives when hit within the Shit threshold.
*/
public static final LEGACY_SHIT_SCORE:Int = 50;
static function scoreNoteLEGACY(msTiming:Float):Int
{
var absTiming = Math.abs(msTiming);
if (absTiming < LEGACY_HIT_WINDOW * LEGACY_SICK_THRESHOLD)
var absTiming:Float = Math.abs(msTiming);
return switch (absTiming)
{
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;
case(_ < LEGACY_HIT_WINDOW * LEGACY_SICK_THRESHOLD) => true:
LEGACY_SICK_SCORE;
case(_ < LEGACY_HIT_WINDOW * LEGACY_GOOD_THRESHOLD) => true:
LEGACY_GOOD_SCORE;
case(_ < LEGACY_HIT_WINDOW * LEGACY_BAD_THRESHOLD) => true:
LEGACY_BAD_SCORE;
case(_ < LEGACY_HIT_WINDOW * LEGACY_SHIT_THRESHOLD) => true:
LEGACY_SHIT_SCORE;
default:
0;
}
}
static function judgeNote_LEGACY(msTiming:Float):String
static function judgeNoteLEGACY(msTiming:Float):String
{
var absTiming = Math.abs(msTiming);
if (absTiming < LEGACY_HIT_WINDOW * LEGACY_SICK_THRESHOLD)
var absTiming:Float = Math.abs(msTiming);
return switch (absTiming)
{
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';
case(_ < LEGACY_HIT_WINDOW * LEGACY_SICK_THRESHOLD) => true:
'sick';
case(_ < LEGACY_HIT_WINDOW * LEGACY_GOOD_THRESHOLD) => true:
'good';
case(_ < LEGACY_HIT_WINDOW * LEGACY_BAD_THRESHOLD) => true:
'bad';
case(_ < LEGACY_HIT_WINDOW * LEGACY_SHIT_THRESHOLD) => true:
'shit';
default:
'miss';
}
}
@ -197,19 +265,34 @@ class Scoring
* 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 final WEEK7_HIT_WINDOW:Float = 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;
public static final WEEK7_BAD_THRESHOLD:Float = 0.8; // 80% of the hit window, or ~125ms
public static final WEEK7_GOOD_THRESHOLD:Float = 0.55; // 55% of the hit window, or ~91ms
public static final WEEK7_SICK_THRESHOLD:Float = 0.2; // 20% of the hit window, or ~33ms
public static final WEEK7_SHIT_SCORE:Int = 50;
public static final WEEK7_BAD_SCORE:Int = 100;
public static final WEEK7_GOOD_SCORE:Int = 200;
public static final WEEK7_SICK_SCORE:Int = 350;
static function scoreNote_WEEK7(msTiming:Float):Int
static function scoreNoteWEEK7(msTiming:Float):Int
{
var absTiming = Math.abs(msTiming);
var absTiming:Float = Math.abs(msTiming);
return switch (absTiming)
{
case(_ < WEEK7_HIT_WINDOW * WEEK7_SICK_THRESHOLD) => true:
LEGACY_SICK_SCORE;
case(_ < WEEK7_HIT_WINDOW * WEEK7_GOOD_THRESHOLD) => true:
LEGACY_GOOD_SCORE;
case(_ < WEEK7_HIT_WINDOW * WEEK7_BAD_THRESHOLD) => true:
LEGACY_BAD_SCORE;
case(_ < WEEK7_HIT_WINDOW) => true:
LEGACY_SHIT_SCORE;
default:
0;
}
if (absTiming < WEEK7_HIT_WINDOW * WEEK7_SICK_THRESHOLD)
{
return WEEK7_SICK_SCORE;
@ -232,7 +315,7 @@ class Scoring
}
}
static function judgeNote_WEEK7(msTiming:Float):String
static function judgeNoteWEEK7(msTiming:Float):String
{
var absTiming = Math.abs(msTiming);
if (absTiming < WEEK7_HIT_WINDOW * WEEK7_SICK_THRESHOLD)

View file

@ -18,9 +18,9 @@ class SongDataParser
*/
static final songCache:Map<String, Song> = new Map<String, Song>();
static final DEFAULT_SONG_ID = 'UNKNOWN';
static final SONG_DATA_PATH = 'songs/';
static final SONG_DATA_SUFFIX = '-metadata.json';
static final DEFAULT_SONG_ID:String = 'UNKNOWN';
static final SONG_DATA_PATH:String = 'songs/';
static final SONG_DATA_SUFFIX:String = '-metadata.json';
/**
* Parses and preloads the game's song metadata and scripts when the game starts.
@ -30,7 +30,7 @@ class SongDataParser
public static function loadSongCache():Void
{
clearSongCache();
trace("Loading song cache...");
trace('Loading song cache...');
//
// SCRIPTED SONGS
@ -54,12 +54,10 @@ class SongDataParser
//
// UNSCRIPTED SONGS
//
var songIdList:Array<String> = DataAssets.listDataFilesInPath(SONG_DATA_PATH, SONG_DATA_SUFFIX).map(function(songDataPath:String):String
{
var songIdList:Array<String> = DataAssets.listDataFilesInPath(SONG_DATA_PATH, SONG_DATA_SUFFIX).map(function(songDataPath:String):String {
return songDataPath.split('/')[0];
});
var unscriptedSongIds:Array<String> = songIdList.filter(function(songId:String):Bool
{
var unscriptedSongIds:Array<String> = songIdList.filter(function(songId:String):Bool {
return !songCache.exists(songId);
});
trace(' Instantiating ${unscriptedSongIds.length} non-scripted songs...');
@ -67,7 +65,7 @@ class SongDataParser
{
try
{
var song = new Song(songId);
var song:Song = new Song(songId);
if (song != null)
{
trace(' Loaded song data: ${song.songId}');
@ -88,6 +86,8 @@ class SongDataParser
/**
* Retrieves a particular song from the cache.
* @param songId The ID of the song to retrieve.
* @return The song, or null if it was not found.
*/
public static function fetchSong(songId:String):Null<Song>
{
@ -331,7 +331,7 @@ typedef RawSongNoteData =
abstract SongNoteData(RawSongNoteData)
{
public function new(time:Float, data:Int, length:Float = 0, kind:String = "")
public function new(time:Float, data:Int, length:Float = 0, kind:String = '')
{
this =
{

View file

@ -39,7 +39,7 @@ class Bopper extends FlxSprite implements IPlayStateScriptedClass
* Add a suffix to the `idle` animation (or `danceLeft` and `danceRight` animations)
* that this bopper will play.
*/
public var idleSuffix(default, set):String = "";
public var idleSuffix(default, set):String = '';
/**
* Whether this bopper should bop every beat. By default it's true, but when used
@ -59,7 +59,7 @@ class Bopper extends FlxSprite implements IPlayStateScriptedClass
*/
public var globalOffsets(default, set):Array<Float> = [0, 0];
function set_globalOffsets(value:Array<Float>)
function set_globalOffsets(value:Array<Float>):Array<Float>
{
if (globalOffsets == null) globalOffsets = [0, 0];
if (globalOffsets == value) return value;
@ -69,14 +69,15 @@ class Bopper extends FlxSprite implements IPlayStateScriptedClass
this.x += xDiff;
this.y += yDiff;
return animOffsets = value;
globalOffsets = value;
return value;
}
var animOffsets(default, set):Array<Float> = [0, 0];
public var originalPosition:FlxPoint = new FlxPoint(0, 0);
function set_animOffsets(value:Array<Float>)
function set_animOffsets(value:Array<Float>):Array<Float>
{
if (animOffsets == null) animOffsets = [0, 0];
if (animOffsets == value) return value;
@ -101,8 +102,11 @@ class Bopper extends FlxSprite implements IPlayStateScriptedClass
super();
this.danceEvery = danceEvery;
this.animation.callback = this.onAnimationFrame;
this.animation.finishCallback = this.onAnimationFinished;
if (this.animation != null)
{
this.animation.callback = this.onAnimationFrame;
this.animation.finishCallback = this.onAnimationFinished;
}
}
/**
@ -281,8 +285,7 @@ class Bopper extends FlxSprite implements IPlayStateScriptedClass
applyAnimationOffsets(correctName);
canPlayOtherAnims = false;
forceAnimationTimer.start(duration, (timer) ->
{
forceAnimationTimer.start(duration, (timer) -> {
canPlayOtherAnims = true;
}, 1);
}

View file

@ -111,7 +111,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass
* 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.
*/
function buildStage()
function buildStage():Void
{
trace('Building stage for display: ${this.stageId}');
@ -140,9 +140,9 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass
// Initalize sprite frames.
switch (dataProp.animType)
{
case "packer":
case 'packer':
propSprite.frames = Paths.getPackerAtlas(dataProp.assetPath);
default: // "sparrow"
default: // 'sparrow'
propSprite.frames = Paths.getSparrowAtlas(dataProp.assetPath);
}
}
@ -186,7 +186,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass
switch (dataProp.animType)
{
case "packer":
case 'packer':
for (propAnim in dataProp.animations)
{
propSprite.animation.add(propAnim.name, propAnim.frameIndices);
@ -196,7 +196,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass
cast(propSprite, Bopper).setAnimationOffsets(propAnim.name, propAnim.offsets[0], propAnim.offsets[1]);
}
}
default: // "sparrow"
default: // 'sparrow'
FlxAnimationUtil.addAtlasAnimations(propSprite, dataProp.animations);
if (Std.isOfType(propSprite, Bopper))
{
@ -268,8 +268,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass
public function setShader(shader:FlxShader)
{
forEachAlive(function(prop:FlxSprite)
{
forEachAlive(function(prop:FlxSprite) {
prop.shader = shader;
});
}
@ -298,7 +297,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass
/**
* Used by the PlayState to add a character to the stage.
*/
public function addCharacter(character:BaseCharacter, charType:CharacterType)
public function addCharacter(character:BaseCharacter, charType:CharacterType):Void
{
if (character == null) return;
@ -321,16 +320,16 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass
switch (charType)
{
case BF:
this.characters.set("bf", character);
this.characters.set('bf', character);
charData = _data.characters.bf;
character.flipX = !character.getDataFlipX();
character.initHealthIcon(false);
case GF:
this.characters.set("gf", character);
this.characters.set('gf', character);
charData = _data.characters.gf;
character.flipX = character.getDataFlipX();
case DAD:
this.characters.set("dad", character);
this.characters.set('dad', character);
charData = _data.characters.dad;
character.flipX = character.getDataFlipX();
character.initHealthIcon(true);
@ -410,11 +409,11 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass
{
if (pop)
{
var boyfriend:BaseCharacter = getCharacter("bf");
var boyfriend:BaseCharacter = getCharacter('bf');
// Remove the character from the stage.
this.remove(boyfriend);
this.characters.remove("bf");
this.characters.remove('bf');
return boyfriend;
}

View file

@ -0,0 +1,48 @@
package funkin.ui.animDebugShit;
import flixel.FlxG;
import funkin.graphics.adobeanimate.FlxAtlasSprite;
/**
* A simple test of FlxAnimate.
* Delete this later?
*/
class FlxAnimateTest extends MusicBeatState
{
var sprite:FlxAtlasSprite;
public function new()
{
super();
this.bgColor = 0xFF999999;
}
public override function create():Void
{
super.create();
sprite = new FlxAtlasSprite(0, 0, 'shared:assets/shared/images/characters/tankman');
add(sprite);
sprite.playAnimation('idle');
}
public override function update(elapsed:Float):Void
{
super.update(elapsed);
if (FlxG.keys.justPressed.SPACE) sprite.playAnimation('idle');
if (FlxG.keys.justPressed.W) sprite.playAnimation('singUP');
if (FlxG.keys.justPressed.A) sprite.playAnimation('singLEFT');
if (FlxG.keys.justPressed.S) sprite.playAnimation('singDOWN');
if (FlxG.keys.justPressed.D) sprite.playAnimation('singRIGHT');
if (FlxG.keys.justPressed.J) sprite.playAnimation('hehPrettyGood');
if (FlxG.keys.justPressed.K) sprite.playAnimation('ugh');
}
}