1
0
Fork 0
mirror of https://github.com/ninjamuffin99/Funkin.git synced 2024-12-26 15:07:14 +00:00

First WIP of scripted characters (NOT WORKING)

This commit is contained in:
Eric Myllyoja 2022-03-17 01:40:08 -04:00
parent 7c0cb9c69c
commit 1a85b4a1ce
17 changed files with 832 additions and 148 deletions

View file

@ -1,5 +1,6 @@
package funkin;
import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.modding.module.ModuleHandler;
import funkin.play.stage.StageData;
import funkin.charting.ChartingState;
@ -122,7 +123,7 @@ class InitState extends FlxTransitionableState
FlxTransitionableState.skipNextTransIn = true;
StageDataParser.loadStageCache();
CharacterDataParser.loadCharacterCache();
ModuleHandler.loadModuleCache();
#if song

View file

@ -4,7 +4,4 @@ import flixel.FlxSprite;
import funkin.modding.IHook;
@:hscriptClass
class ScriptedFlxSprite extends FlxSprite implements IHook
{
// No body needed for this class, it's magic ;)
}
class ScriptedFlxSprite extends FlxSprite implements IHook {}

View file

@ -4,7 +4,4 @@ import flixel.group.FlxSpriteGroup;
import funkin.modding.IHook;
@:hscriptClass
class ScriptedFlxSpriteGroup extends FlxSpriteGroup implements IHook
{
// No body needed for this class, it's magic ;)
}
class ScriptedFlxSpriteGroup extends FlxSpriteGroup implements IHook {}

View file

@ -3,7 +3,4 @@ package funkin.modding.module;
import funkin.modding.IHook;
@:hscriptClass
class ScriptedModule extends Module implements IHook
{
// No body needed for this class, it's magic ;)
}
class ScriptedModule extends Module implements IHook {}

View file

@ -0,0 +1,56 @@
package funkin.play;
typedef AnimationData =
{
/**
* The name for the animation.
* This should match the animation name queried by the game;
* for example, characters need animations with names `idle`, `singDOWN`, `singUPmiss`, etc.
*/
var name:String;
/**
* The prefix for the frames of the animation as defined by the XML file.
* This will may or may not differ from the `name` of the animation,
* depending on how your animator organized their FLA or whatever.
*/
var prefix:String;
/**
* Offset the character's position by this amount when playing this animation.
* @default [0, 0]
*/
var offsets:Null<Array<Float>>;
/**
* Whether the animation should loop when it finishes.
* @default false
*/
var looped:Null<Bool>;
/**
* Whether the animation's sprites should be flipped horizontally.
* @default false
*/
var flipX:Null<Bool>;
/**
* Whether the animation's sprites should be flipped vertically.
* @default false
*/
var flipY:Null<Bool>;
/**
* The frame rate of the animation.
* @default 24
*/
var frameRate:Null<Int>;
/**
* If you want this animation to use only certain frames of an animation with a given prefix,
* select them here.
* @example [0, 1, 2, 3] (use only the first four frames)
* @default [] (all frames)
*/
var frameIndices:Null<Array<Int>>;
}

View file

@ -1,6 +1,6 @@
package funkin.play;
import funkin.play.Strumline.StrumlineArrow;
import funkin.play.character.CharacterBase;
import flixel.addons.effects.FlxTrail;
import flixel.addons.transition.FlxTransitionableState;
import flixel.FlxCamera;
@ -27,8 +27,10 @@ import funkin.modding.events.ScriptEventDispatcher;
import funkin.modding.IHook;
import funkin.modding.module.ModuleHandler;
import funkin.Note;
import funkin.play.character.CharacterData;
import funkin.play.stage.Stage;
import funkin.play.stage.StageData;
import funkin.play.Strumline.StrumlineArrow;
import funkin.play.Strumline.StrumlineStyle;
import funkin.Section.SwagSection;
import funkin.SongLoad.SwagSong;
@ -126,8 +128,11 @@ class PlayState extends MusicBeatState implements IHook
/**
* An empty FlxObject contained in the scene.
* The current gameplay camera will be centered on this object. Tween its position to move the camera smoothly.
*
* NOTE: This must be an FlxObject, not an FlxPoint, because it needs to be added to the scene.
* Once it's added to the scene, the camera can be configured to follow it.
*/
public var cameraFollowPoint:FlxObject;
public var cameraFollowPoint:FlxObject = new FlxObject(0, 0, 1, 1);
/**
* PRIVATE INSTANCE VARIABLES
@ -240,7 +245,6 @@ class PlayState extends MusicBeatState implements IHook
var songScore:Int = 0;
var doof:DialogueBox;
var grpNoteSplashes:FlxTypedGroup<NoteSplash>;
var camPos:FlxPoint;
var comboPopUps:PopUpStuff;
var perfectMode:Bool = false;
var previousFrameTime:Int = 0;
@ -315,6 +319,14 @@ class PlayState extends MusicBeatState implements IHook
initDiscord();
#end
// Configure camera follow point.
if (previousCameraFollowPoint != null)
{
cameraFollowPoint.setPosition(previousCameraFollowPoint.x, previousCameraFollowPoint.y);
previousCameraFollowPoint = null;
}
add(cameraFollowPoint);
comboPopUps = new PopUpStuff();
add(comboPopUps);
@ -328,16 +340,6 @@ class PlayState extends MusicBeatState implements IHook
generateSong();
cameraFollowPoint = new FlxObject(0, 0, 1, 1);
cameraFollowPoint.setPosition(camPos.x, camPos.y);
if (previousCameraFollowPoint != null)
{
cameraFollowPoint = previousCameraFollowPoint;
previousCameraFollowPoint = null;
}
add(cameraFollowPoint);
resetCamera();
FlxG.worldBounds.set(0, 0, FlxG.width, FlxG.height);
@ -467,7 +469,11 @@ class PlayState extends MusicBeatState implements IHook
function initCharacters()
{
// all dis is shitty, redo later for stage shit
//
// GIRLFRIEND
//
// TODO: Tie the GF version to the song data, not the stage ID or the current player.
var gfVersion:String = 'gf';
switch (currentStageId)
@ -483,35 +489,34 @@ class PlayState extends MusicBeatState implements IHook
}
if (currentSong.player1 == "pico")
{
gfVersion = "nene";
}
if (currentSong.song.toLowerCase() == 'stress')
gfVersion = 'pico-speaker';
var gf = new Character(400, 130, gfVersion);
gf.scrollFactor.set(0.95, 0.95);
switch (gfVersion)
var girlfriend:Character = new Character(350, -70, gfVersion);
girlfriend.scrollFactor.set(0.95, 0.95);
if (gfVersion == 'pico-speaker')
{
case 'pico-speaker':
gf.x -= 50;
gf.y -= 200;
girlfriend.x -= 50;
girlfriend.y -= 200;
}
//
// DAD
//
var dad = new Character(100, 100, currentSong.player2);
camPos = new FlxPoint(dad.getGraphicMidpoint().x, dad.getGraphicMidpoint().y);
cameraFollowPoint.setPosition(dad.getGraphicMidpoint().x, dad.getGraphicMidpoint().y);
switch (currentSong.player2)
{
case 'gf':
dad.setPosition(gf.x, gf.y);
gf.visible = false;
dad.setPosition(girlfriend.x, girlfriend.y);
girlfriend.visible = false;
if (isStoryMode)
{
camPos.x += 600;
cameraFollowPoint.x += 600;
tweenCamIn();
}
case "spooky":
@ -521,25 +526,40 @@ class PlayState extends MusicBeatState implements IHook
case 'monster-christmas':
dad.y += 130;
case 'dad':
camPos.x += 400;
cameraFollowPoint.x += 400;
case 'pico':
camPos.x += 600;
cameraFollowPoint.x += 600;
dad.y += 300;
case 'parents-christmas':
dad.x -= 500;
case 'senpai' | 'senpai-angry':
dad.x += 150;
dad.y += 360;
camPos.set(dad.getGraphicMidpoint().x + 300, dad.getGraphicMidpoint().y);
cameraFollowPoint.setPosition(dad.getGraphicMidpoint().x + 300, dad.getGraphicMidpoint().y);
case 'spirit':
dad.x -= 150;
dad.y += 100;
camPos.set(dad.getGraphicMidpoint().x + 300, dad.getGraphicMidpoint().y);
cameraFollowPoint.setPosition(dad.getGraphicMidpoint().x + 300, dad.getGraphicMidpoint().y);
case 'tankman':
dad.y += 180;
}
var boyfriend = new Boyfriend(770, 450, currentSong.player1);
if (currentSong.player1 == "pico")
{
dad.x -= 100;
dad.y -= 100;
}
//
// BOYFRIEND
//
var boyfriend:CharacterBase;
switch (currentSong.player1)
{
default:
boyfriend = CharacterDataParser.fetchCharacter(currentSong.player1);
boyfriend.characterType = CharacterType.BF;
}
// REPOSITIONING PER STAGE
switch (currentStageId)
@ -550,8 +570,8 @@ class PlayState extends MusicBeatState implements IHook
evilTrail.zIndex = 190;
add(evilTrail);
case "tank":
gf.y += 10;
gf.x -= 30;
girlfriend.y += 10;
girlfriend.x -= 30;
boyfriend.x += 40;
boyfriend.y += 0;
dad.y += 60;
@ -559,8 +579,8 @@ class PlayState extends MusicBeatState implements IHook
if (gfVersion != 'pico-speaker')
{
gf.x -= 170;
gf.y -= 75;
girlfriend.x -= 170;
girlfriend.y -= 75;
}
}
@ -568,16 +588,16 @@ class PlayState extends MusicBeatState implements IHook
{
// We're using Eric's stage handler.
// Characters get added to the stage, not the main scene.
currentStage.addCharacter(gf, GF);
currentStage.addCharacter(boyfriend, BF);
currentStage.addCharacter(dad, DAD);
currentStage.addCharacterOld(girlfriend, GF);
currentStage.addCharacterOld(dad, DAD);
// Redo z-indexes.
currentStage.refresh();
}
else
{
add(gf);
add(girlfriend);
add(dad);
add(boyfriend);
}
@ -612,6 +632,7 @@ class PlayState extends MusicBeatState implements IHook
// Reload the stages in cache. This might cause a lag spike but who cares this is a debug utility.
StageDataParser.loadStageCache();
CharacterDataParser.loadCharacterCache();
ModuleHandler.loadModuleCache();
// Reload the level. This should use new data from the assets folder.
@ -685,8 +706,6 @@ class PlayState extends MusicBeatState implements IHook
senpaiEvil.screenCenter();
senpaiEvil.x += senpaiEvil.width / 5;
cameraFollowPoint.setPosition(camPos.x, camPos.y);
if (currentSong.song.toLowerCase() == 'roses' || currentSong.song.toLowerCase() == 'thorns')
{
remove(black);
@ -1664,7 +1683,7 @@ class PlayState extends MusicBeatState implements IHook
&& PlayState.instance.currentStage.getBoyfriend().animation.curAnim.name.startsWith('sing')
&& !PlayState.instance.currentStage.getBoyfriend().animation.curAnim.name.endsWith('miss'))
{
PlayState.instance.currentStage.getBoyfriend().playAnim('idle');
PlayState.instance.currentStage.getBoyfriend().playAnimation('idle');
}
}
@ -1695,7 +1714,7 @@ class PlayState extends MusicBeatState implements IHook
vocals.volume = 0;
FlxG.sound.play(Paths.soundRandom('missnote', 1, 3), FlxG.random.float(0.1, 0.2));
currentStage.getBoyfriend().playAnim('sing' + direction.nameUpper + 'miss', true);
currentStage.getBoyfriend().playAnimation('sing' + direction.nameUpper + 'miss', true);
}
function goodNoteHit(note:Note):Void
@ -1708,7 +1727,7 @@ class PlayState extends MusicBeatState implements IHook
popUpScore(note.data.strumTime, note);
}
currentStage.getBoyfriend().playAnim('sing' + note.dirNameUpper, true);
currentStage.getBoyfriend().playAnimation('sing' + note.dirNameUpper, true);
playerStrumline.getArrow(note.data.noteData).playAnimation('confirm', true);
@ -1800,7 +1819,7 @@ class PlayState extends MusicBeatState implements IHook
if (curBeat % 2 == 0)
{
if (currentStage.getBoyfriend().animation != null && !currentStage.getBoyfriend().animation.curAnim.name.startsWith("sing"))
currentStage.getBoyfriend().playAnim('idle');
currentStage.getBoyfriend().playAnimation('idle');
if (currentStage.getDad().animation != null && !currentStage.getDad().animation.curAnim.name.startsWith("sing"))
currentStage.getDad().dance();
}
@ -1812,7 +1831,7 @@ class PlayState extends MusicBeatState implements IHook
if (curBeat % 8 == 7 && currentSong.song == 'Bopeebo')
{
currentStage.getBoyfriend().playAnim('hey', true);
currentStage.getBoyfriend().playAnimation('hey', true);
}
if (curBeat % 16 == 15
@ -1821,7 +1840,7 @@ class PlayState extends MusicBeatState implements IHook
&& curBeat > 16
&& curBeat < 48)
{
currentStage.getBoyfriend().playAnim('hey', true);
currentStage.getBoyfriend().playAnimation('hey', true);
currentStage.getDad().playAnim('cheer', true);
}
}

View file

@ -1,9 +0,0 @@
package funkin.play.character;
enum CharacterType
{
BF;
GF;
DAD;
OTHER;
}

View file

@ -0,0 +1,140 @@
package funkin.play.character;
import funkin.modding.events.ScriptEvent;
import funkin.modding.events.ScriptEvent.UpdateScriptEvent;
import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.Note.NoteDir;
import funkin.modding.events.ScriptEvent.NoteScriptEvent;
import funkin.play.stage.Bopper;
/**
* A Character is a stage prop which bops to the music as well as controlled by the strumlines.
*
* Remember: The character's origin is at its FEET. (horizontal center, vertical bottom)
*/
class CharacterBase extends Bopper
{
public var characterId(default, null):String;
public var characterName(default, null):String;
/**
* Whether the player is an active character (Boyfriend) or not.
*/
public var characterType:CharacterType = OTHER;
public var attachedStrumlines(default, null):Array<Int>;
final _data:CharacterData;
/**
* Tracks how long, in seconds, the character has been playing the current `sing` animation.
* This is used to ensure that characters play the `sing` animations for at least one beat,
* preventing them from reverting to the `idle` animation between notes.
*/
public var holdTimer:Float = 0;
final singTimeCrochet:Float;
public function new(id:String)
{
super();
this.characterId = id;
this.attachedStrumlines = [];
_data = CharacterDataParser.parseCharacterData(this.characterId);
if (_data == null)
{
throw 'Could not find character data for characterId: $characterId';
}
else
{
this.characterName = _data.name;
this.singTimeCrochet = _data.singTime;
}
}
public override function onUpdate(event:UpdateScriptEvent):Void
{
super.onUpdate(event);
// Handle character note hold time.
holdTimer += event.elapsed;
var singTimeMs:Float = singTimeCrochet * Conductor.crochet;
// 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.
var shouldStopSinging:Bool = (this.characterType == BF) ? !isHoldingNote() : true;
if (holdTimer > singTimeMs && shouldStopSinging)
{
holdTimer = 0;
dance();
}
}
/**
* Returns true if the player is holding a note.
* Used when determing whether a the player character should revert to the `idle` animation.
* On non-player characters, this should be ignored.
*/
function isHoldingNote(player:Int = 1):Bool
{
// Returns true if at least one of LEFT, DOWN, UP, or RIGHT is being held.
switch (player)
{
case 1:
return [
PlayerSettings.player1.controls.NOTE_LEFT,
PlayerSettings.player1.controls.NOTE_DOWN,
PlayerSettings.player1.controls.NOTE_UP,
PlayerSettings.player1.controls.NOTE_RIGHT,
].contains(true);
case 2:
return [
PlayerSettings.player2.controls.NOTE_LEFT,
PlayerSettings.player2.controls.NOTE_DOWN,
PlayerSettings.player2.controls.NOTE_UP,
PlayerSettings.player2.controls.NOTE_RIGHT,
].contains(true);
}
return false;
}
/**
* Every time a note is hit, check if the note is from the same strumline.
* If it is, then play the sing animation.
*/
public override function onNoteHit(event:NoteScriptEvent)
{
super.onNoteHit(event);
// If event.note is from the same strumline as this character, then sing.
// if (this.attachedStrumlines.indexOf(event.note.strumline) != -1)
// {
// this.playSingAnimation(event.note.dir, false, note.alt);
// }
}
public override function onDestroy(event:ScriptEvent):Void
{
this.characterType = OTHER;
}
/**
* Play the appropriate singing animation, for the given note direction.
* @param dir The direction of the note.
* @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`.
*/
function playSingAnimation(dir:NoteDir, miss:Bool = false, suffix:String = ""):Void
{
var anim:String = 'sing${dir.nameUpper}${miss ? 'miss' : ''}${suffix != "" ? '-${suffix}' : ''}';
playAnimation(anim, true);
}
}
enum CharacterType
{
BF;
DAD;
GF;
OTHER;
}

View file

@ -0,0 +1,414 @@
package funkin.play.character;
import openfl.Assets;
import haxe.Json;
import funkin.play.character.render.PackerCharacter;
import funkin.play.character.render.SparrowCharacter;
import funkin.util.assets.DataAssets;
import funkin.play.character.CharacterBase;
import funkin.play.character.ScriptedCharacter.ScriptedSparrowCharacter;
import funkin.play.character.ScriptedCharacter.ScriptedPackerCharacter;
import flixel.util.typeLimit.OneOfTwo;
using StringTools;
class CharacterDataParser
{
/**
* The current version string for the stage data format.
* Handle breaking changes by incrementing this value
* and adding migration to the `migrateStageData()` function.
*/
public static final CHARACTER_DATA_VERSION:String = "1.0";
static final characterCache:Map<String, CharacterBase> = new Map<String, CharacterBase>();
static final DEFAULT_CHAR_ID:String = 'UNKNOWN';
/**
* Parses and preloads the game's stage data and scripts when the game starts.
*
* If you want to force stages to be reloaded, you can just call this function again.
*/
public static function loadCharacterCache():Void
{
// Clear any stages that are cached if there were any.
clearCharacterCache();
trace("[CHARDATA] Loading character cache...");
//
// SCRIPTED CHARACTERS
//
// Generic (Sparrow) characters
var scriptedCharClassNames:Array<String> = ScriptedCharacter.listScriptClasses();
trace(' Instantiating ${scriptedCharClassNames.length} scripted characters...');
for (charCls in scriptedCharClassNames)
{
_storeChar(ScriptedCharacter.init(charCls, DEFAULT_CHAR_ID), charCls);
}
// Sparrow characters
scriptedCharClassNames = ScriptedSparrowCharacter.listScriptClasses();
if (scriptedCharClassNames.length > 0)
{
trace(' Instantiating ${scriptedCharClassNames.length} scripted characters (SPARROW)...');
for (charCls in scriptedCharClassNames)
{
_storeChar(ScriptedSparrowCharacter.init(charCls, DEFAULT_CHAR_ID), charCls);
}
}
// // Packer characters
// scriptedCharClassNames = ScriptedPackerCharacter.listScriptClasses();
// if (scriptedCharClassNames.length > 0)
// {
// trace(' Instantiating ${scriptedCharClassNames.length} scripted characters (PACKER)...');
// for (charCls in scriptedCharClassNames)
// {
// _storeChar(ScriptedPackerCharacter.init(charCls, DEFAULT_CHAR_ID), charCls);
// }
// }
// TODO: Add more character types.
//
// UNSCRIPTED STAGES
//
var charIdList:Array<String> = DataAssets.listDataFilesInPath('characters/');
var unscriptedCharIds:Array<String> = charIdList.filter(function(charId:String):Bool
{
return !characterCache.exists(charId);
});
trace(' Instantiating ${unscriptedCharIds.length} non-scripted characters...');
for (charId in unscriptedCharIds)
{
var char:CharacterBase = null;
try
{
var charData:CharacterData = parseCharacterData(charId);
if (charData != null)
{
switch (charData.renderType)
{
case CharacterRenderType.PACKER:
char = new PackerCharacter(charId);
case CharacterRenderType.SPARROW:
// default
char = new SparrowCharacter(charId);
default:
trace(' Failed to instantiate character: ${charId} (Bad render type ${charData.renderType})');
}
}
if (char != null)
{
trace(' Loaded character data: ${char.characterName}');
characterCache.set(charId, char);
}
}
catch (e)
{
// Assume error was already logged.
continue;
}
}
trace(' Successfully loaded ${Lambda.count(characterCache)} stages.');
}
static function _storeChar(char:CharacterBase, charCls:String):Void
{
if (char != null)
{
trace(' Loaded scripted character: ${char.characterName}');
// Disable the rendering logic for stage until it's loaded.
// Note that kill() =/= destroy()
char.kill();
// Then store it.
characterCache.set(char.characterId, char);
}
else
{
trace(' Failed to instantiate scripted character class: ${charCls}');
}
}
public static function fetchCharacter(charId:String):Null<CharacterBase>
{
if (characterCache.exists(charId))
{
trace('[CHARDATA] Successfully fetch stage: ${charId}');
var character:CharacterBase = characterCache.get(charId);
character.revive();
return character;
}
else
{
trace('[CHARDATA] Failed to fetch character, not found in cache: ${charId}');
return null;
}
}
static function clearCharacterCache():Void
{
if (characterCache != null)
{
for (char in characterCache)
{
char.destroy();
}
characterCache.clear();
}
}
/**
* Load a character's JSON file, parse its data, and return it.
*
* @param charId The character to load.
* @return The character data, or null if validation failed.
*/
public static function parseCharacterData(charId:String):Null<CharacterData>
{
var rawJson:String = loadCharacterFile(charId);
var charData:CharacterData = migrateCharacterData(rawJson, charId);
return validateCharacterData(charId, charData);
}
static function loadCharacterFile(charPath:String):String
{
var charFilePath:String = Paths.json('characters/${charPath}');
var rawJson = Assets.getText(charFilePath).trim();
while (!rawJson.endsWith("}"))
{
rawJson = rawJson.substr(0, rawJson.length - 1);
}
return rawJson;
}
static function migrateCharacterData(rawJson:String, charId:String)
{
// If you update the character data format in a breaking way,
// handle migration here by checking the `version` value.
try
{
var charData:CharacterData = cast Json.parse(rawJson);
return charData;
}
catch (e)
{
trace(' Error parsing data for character: ${charId}');
trace(' ${e}');
return null;
}
}
static final DEFAULT_NAME:String = "Untitled Character";
static final DEFAULT_RENDERTYPE:CharacterRenderType = CharacterRenderType.SPARROW;
static final DEFAULT_STARTINGANIM:String = "idle";
static final DEFAULT_SCROLL:Array<Float> = [0, 0];
static final DEFAULT_ISPIXEL:Bool = false;
static final DEFAULT_DANCEEVERY:Int = 1;
static final DEFAULT_FRAMERATE:Int = 24;
static final DEFAULT_FLIPX:Bool = false;
static final DEFAULT_SCALE:Float = 1;
static final DEFAULT_FLIPY:Bool = false;
static final DEFAULT_LOOP:Bool = false;
static final DEFAULT_FRAMEINDICES:Array<Int> = [];
/**
* Set unspecified parameters to their defaults.
* If the parameter is mandatory, print an error message.
* @param id
* @param input
* @return The validated character data
*/
static function validateCharacterData(id:String, input:CharacterData):Null<CharacterData>
{
if (input == null)
{
trace('[CHARDATA] ERROR: Could not parse character data for "${id}".');
return null;
}
if (input.version == null)
{
trace('[CHARDATA] ERROR: Could not load character data for "$id": missing version');
return null;
}
if (input.version == CHARACTER_DATA_VERSION)
{
trace('[CHARDATA] ERROR: Could not load character data for "$id": bad/outdated version (got ${input.version}, expected ${CHARACTER_DATA_VERSION})');
return null;
}
if (input.name == null)
{
trace('[CHARDATA] WARN: Character data for "$id" missing name');
input.name = DEFAULT_NAME;
}
if (input.renderType == null)
{
input.renderType = DEFAULT_RENDERTYPE;
}
if (input.assetPath == null)
{
trace('[CHARDATA] ERROR: Could not load character data for "$id": missing assetPath');
return null;
}
if (input.startingAnimation == null)
{
input.startingAnimation = DEFAULT_STARTINGANIM;
}
if (input.scale == null)
{
input.scale = DEFAULT_SCALE;
}
if (input.isPixel == null)
{
input.isPixel = DEFAULT_ISPIXEL;
}
if (input.danceEvery == null)
{
input.danceEvery = DEFAULT_DANCEEVERY;
}
if (input.animations == null || input.animations.length == 0)
{
trace('[CHARDATA] ERROR: Could not load character data for "$id": missing animations');
input.animations = [];
}
if (input.animations.length == 0 && input.startingAnimation != null)
{
return null;
}
for (inputAnimation in input.animations)
{
if (inputAnimation.name == null)
{
trace('[CHARDATA] ERROR: Could not load character data for "$id": missing animation name for prop "${input.name}"');
return null;
}
if (inputAnimation.frameRate == null)
{
inputAnimation.frameRate = DEFAULT_FRAMERATE;
}
if (inputAnimation.frameIndices == null)
{
inputAnimation.frameIndices = DEFAULT_FRAMEINDICES;
}
if (inputAnimation.looped == null)
{
inputAnimation.looped = DEFAULT_LOOP;
}
if (inputAnimation.flipX == null)
{
inputAnimation.flipX = DEFAULT_FLIPX;
}
if (inputAnimation.flipY == null)
{
inputAnimation.flipY = DEFAULT_FLIPY;
}
}
// All good!
return input;
}
}
enum abstract CharacterRenderType(String) from String to String
{
var SPARROW = 'sparrow';
var PACKER = 'packer';
// TODO: Aesprite?
// TODO: Animate?
// TODO: Experimental...
}
typedef CharacterData =
{
/**
* The sematic version of the chart data format.
*/
var version:String;
/**
* The readable name of the character.
*/
var name:String;
/**
* The type of rendering system to use for the character.
* @default sparrow
*/
var renderType:CharacterRenderType;
/**
* Behavior varies by render type:
* - SPARROW: Path to retrieve both the spritesheet and the XML data from.
* - PACKER: Path to retrieve both the spritsheet and the TXT data from.
*/
var assetPath:String;
/**
* Either the scale of the graphic as a float, or the [w, h] scale as an array of two floats.
* Pro tip: On pixel-art levels, save the sprites small and set this value to 6 or so to save memory.
* @default 1
*/
var scale:OneOfTwo<Float, Array<Float>>;
/**
* Setting this to true disables anti-aliasing for the character.
* @default false
*/
var isPixel:Null<Bool>;
/**
* The frequency at which the character will play its idle animation, in beats.
* Increasing this number will make the character dance less often.
*
* @default 1
*/
var danceEvery:Null<Int>;
/**
* The minimum duration that a character will play a note animation for, in beats.
* If this number is too low, you may see the character start playing the idle animation between notes.
* If this number is too high, you may see the the character play the sing animation for too long after the notes are gone.
*
* Examples:
* - Daddy Dearest uses a value of `1.525`.
* @default 1.0
*/
var singTime:Null<Float>;
/**
* An optional array of animations which the character can play.
*/
var animations:Array<AnimationData>;
/**
* If animations are used, this is the name of the animation to play first.
* @default idle
*/
var startingAnimation:Null<String>;
};

View file

@ -0,0 +1,14 @@
package funkin.play.character;
import funkin.play.character.render.PackerCharacter;
import funkin.play.character.render.SparrowCharacter;
import funkin.modding.IHook;
@:hscriptClass
class ScriptedCharacter extends SparrowCharacter implements IHook {}
@:hscriptClass
class ScriptedSparrowCharacter extends SparrowCharacter implements IHook {}
@:hscriptClass
class ScriptedPackerCharacter extends PackerCharacter implements IHook {}

View file

@ -0,0 +1,15 @@
package funkin.play.character.render;
import funkin.play.character.CharacterBase.CharacterType;
/**
* A PackerCharacter is a Character which is rendered by
* displaying an animation derived from a Packer spritesheet file.
*/
class PackerCharacter extends CharacterBase
{
public function new(id:String)
{
super(id);
}
}

View file

@ -0,0 +1,15 @@
package funkin.play.character.render;
import funkin.play.character.CharacterBase.CharacterType;
/**
* A SparrowCharacter is a Character which is rendered by
* displaying an animation derived from a SparrowV2 atlas spritesheet file.
*/
class SparrowCharacter extends CharacterBase
{
public function new(id:String)
{
super(id);
}
}

View file

@ -16,6 +16,7 @@ class Bopper extends FlxSprite implements IPlayStateScriptedClass
{
/**
* The bopper plays the dance animation once every `danceEvery` beats.
* Set to 0 to disable idle animation.
*/
public var danceEvery:Int = 1;
@ -31,11 +32,19 @@ class Bopper extends FlxSprite implements IPlayStateScriptedClass
/**
* Set this value to define an additional horizontal offset to this sprite's position.
*/
public var xOffset:Float = 0;
public var xOffset(default, set):Float = 0;
override function set_x(value:Float):Float
{
this.x = value + this.xOffset;
this.x = this.xOffset + value;
return this.x;
}
function set_xOffset(value:Float):Float
{
var diff = value - this.xOffset;
this.xOffset = value;
this.x += diff;
return value;
}
@ -55,7 +64,15 @@ class Bopper extends FlxSprite implements IPlayStateScriptedClass
override function set_y(value:Float):Float
{
this.y = value + this.yOffset;
this.y = this.yOffset + value;
return this.y;
}
function set_yOffset(value:Float):Float
{
var diff = value - this.yOffset;
this.yOffset = value;
this.y += diff;
return value;
}
@ -73,7 +90,7 @@ class Bopper extends FlxSprite implements IPlayStateScriptedClass
function update_shouldAlternate():Void
{
if (this.animation.getByName('danceLeft') != null)
if (hasAnimation('danceLeft'))
{
this.shouldAlternate = true;
}
@ -84,7 +101,7 @@ class Bopper extends FlxSprite implements IPlayStateScriptedClass
*/
public function onBeatHit(event:SongTimeScriptEvent):Void
{
if (event.beat % danceEvery == 0)
if (danceEvery > 0 && event.beat % danceEvery == 0)
{
dance();
}
@ -109,20 +126,42 @@ class Bopper extends FlxSprite implements IPlayStateScriptedClass
{
if (hasDanced)
{
this.animation.play('danceRight$idleSuffix');
playAnimation('danceRight$idleSuffix');
}
else
{
this.animation.play('danceLeft$idleSuffix');
playAnimation('danceLeft$idleSuffix');
}
hasDanced = !hasDanced;
}
else
{
this.animation.play('idle$idleSuffix');
playAnimation('idle$idleSuffix');
}
}
public function hasAnimation(id:String):Bool
{
return this.animation.getByName(id) != null;
}
/*
* @param AnimName The string name of the animation you want to play.
* @param Force Whether to force the animation to restart.
*/
public function playAnimation(name:String, force:Bool = false):Void
{
this.animation.play(name, force, false, 0);
}
/**
* Returns the name of the animation that is currently playing.
*/
public function getCurrentAnimation():String
{
return this.animation.curAnim.name;
}
public function onScriptEvent(event:ScriptEvent) {}
public function onCreate(event:ScriptEvent) {}

View file

@ -4,7 +4,4 @@ import funkin.modding.IHook;
@:hscriptClass
@:keep
class ScriptedBopper extends Bopper implements IHook
{
// No body needed for this class, it's magic ;)
}
class ScriptedBopper extends Bopper implements IHook {}

View file

@ -3,7 +3,4 @@ package funkin.play.stage;
import funkin.modding.IHook;
@:hscriptClass
class ScriptedStage extends Stage implements IHook
{
// No body needed for this class, it's magic ;)
}
class ScriptedStage extends Stage implements IHook {}

View file

@ -1,5 +1,6 @@
package funkin.play.stage;
import funkin.play.character.CharacterBase;
import funkin.modding.events.ScriptEventDispatcher;
import funkin.modding.events.ScriptEvent;
import funkin.modding.events.ScriptEvent.CountdownScriptEvent;
@ -10,7 +11,7 @@ import flixel.group.FlxSpriteGroup;
import flixel.math.FlxPoint;
import flixel.util.FlxSort;
import funkin.modding.IHook;
import funkin.play.character.Character.CharacterType;
import funkin.play.character.CharacterBase.CharacterType;
import funkin.play.stage.StageData.StageDataParser;
import funkin.util.SortUtil;
@ -29,7 +30,8 @@ class Stage extends FlxSpriteGroup implements IHook implements IPlayStateScripte
public var camZoom:Float = 1.0;
var namedProps:Map<String, FlxSprite> = new Map<String, FlxSprite>();
var characters:Map<String, Character> = new Map<String, Character>();
var characters:Map<String, CharacterBase> = new Map<String, CharacterBase>();
var charactersOld:Map<String, Character> = new Map<String, Character>();
var boppers:Array<Bopper> = new Array<Bopper>();
/**
@ -149,12 +151,12 @@ class Stage extends FlxSpriteGroup implements IHook implements IPlayStateScripte
{
if (propAnim.frameIndices.length == 0)
{
propSprite.animation.addByPrefix(propAnim.name, propAnim.prefix, propAnim.frameRate, propAnim.loop, propAnim.flipX,
propSprite.animation.addByPrefix(propAnim.name, propAnim.prefix, propAnim.frameRate, propAnim.looped, propAnim.flipX,
propAnim.flipY);
}
else
{
propSprite.animation.addByIndices(propAnim.name, propAnim.prefix, propAnim.frameIndices, "", propAnim.frameRate, propAnim.loop,
propSprite.animation.addByIndices(propAnim.name, propAnim.prefix, propAnim.frameIndices, "", propAnim.frameRate, propAnim.looped,
propAnim.flipX, propAnim.flipY);
}
}
@ -234,7 +236,7 @@ class Stage extends FlxSpriteGroup implements IHook implements IPlayStateScripte
/**
* Used by the PlayState to add a character to the stage.
*/
public function addCharacter(character:Character, charType:CharacterType)
public function addCharacter(character:CharacterBase, charType:CharacterType)
{
// Apply position and z-index.
switch (charType)
@ -255,7 +257,38 @@ class Stage extends FlxSpriteGroup implements IHook implements IPlayStateScripte
character.x = _data.characters.dad.position[0];
character.y = _data.characters.dad.position[1];
default:
this.characters.set(character.curCharacter, character);
this.characters.set(character.characterId, character);
}
// Add the character to the scene.
this.add(character);
}
/**
* Used by the PlayState to add a character to the stage.
*/
public function addCharacterOld(character:Character, charType:CharacterType)
{
// Apply position and z-index.
switch (charType)
{
case BF:
this.charactersOld.set("bf", character);
character.zIndex = _data.characters.bf.zIndex;
character.x = _data.characters.bf.position[0];
character.y = _data.characters.bf.position[1];
case GF:
this.charactersOld.set("gf", character);
character.zIndex = _data.characters.gf.zIndex;
character.x = _data.characters.gf.position[0];
character.y = _data.characters.gf.position[1];
case DAD:
this.charactersOld.set("dad", character);
character.zIndex = _data.characters.dad.zIndex;
character.x = _data.characters.dad.position[0];
character.y = _data.characters.dad.position[1];
default:
this.charactersOld.set(character.curCharacter, character);
}
// Add the character to the scene.
@ -265,24 +298,25 @@ class Stage extends FlxSpriteGroup implements IHook implements IPlayStateScripte
/**
* Retrieves a given character from the stage.
*/
public function getCharacter(id:String):Character
public function getCharacter(id:String):CharacterBase
{
return this.characters.get(id);
}
public function getBoyfriend():Character
public function getBoyfriend():CharacterBase
{
return getCharacter('bf');
// return this.charactersOld.get('bf');
}
public function getGirlfriend():Character
{
return getCharacter('gf');
return this.charactersOld.get('gf');
}
public function getDad():Character
{
return getCharacter('dad');
return this.charactersOld.get('dad');
}
/**

View file

@ -1,9 +1,9 @@
package funkin.play.stage;
import openfl.Assets;
import flixel.util.typeLimit.OneOfTwo;
import funkin.util.assets.DataAssets;
import haxe.Json;
import flixel.util.typeLimit.OneOfTwo;
import openfl.Assets;
using StringTools;
@ -194,12 +194,18 @@ class StageDataParser
return null;
}
if (input.version != STAGE_DATA_VERSION)
if (input.version == null)
{
trace('[STAGEDATA] ERROR: Could not load stage data for "$id": missing version');
return null;
}
if (input.version != STAGE_DATA_VERSION)
{
trace('[STAGEDATA] ERROR: Could not load stage data for "$id": bad/outdated version (got ${input.version}, expected ${STAGE_DATA_VERSION})');
return null;
}
if (input.name == null)
{
trace('[STAGEDATA] WARN: Stage data for "$id" missing name');
@ -301,9 +307,9 @@ class StageDataParser
inputAnimation.frameIndices = DEFAULT_FRAMEINDICES;
}
if (inputAnimation.loop == null)
if (inputAnimation.looped == null)
{
inputAnimation.loop = true;
inputAnimation.looped = true;
}
if (inputAnimation.flipX == null)
@ -432,7 +438,7 @@ typedef StageDataProp =
* An optional array of animations which the prop can play.
* @default Prop has no animations.
*/
var animations:Array<StageDataPropAnimation>;
var animations:Array<AnimationData>;
/**
* If animations are used, this is the name of the animation to play first.
@ -448,52 +454,6 @@ typedef StageDataProp =
var animType:String;
};
typedef StageDataPropAnimation =
{
/**
* The name of the animation.
*/
var name:String;
/**
* The common beginning of image names in atlas for this animation's frames.
* For example, if the frames are named "test0001.png", "test0002.png", etc., use "test".
*/
var prefix:String;
/**
* If you want this animation to use only certain frames of an animation with a given prefix,
* select them here.
* @example [0, 1, 2, 3] (use only the first four frames)
* @default [] (all frames)
*/
var frameIndices:Array<Int>;
/**
* The speed of the animation in frames per second.
* @default 24
*/
var frameRate:Null<Int>;
/**
* Whether the animation should loop.
* @default false
*/
var loop:Null<Bool>;
/**
* Whether to flip the sprite horizontally while animating.
* @default false
*/
var flipX:Null<Bool>;
/**
* Whether to flip the sprite vertically while animating.
* @default false
*/
var flipY:Null<Bool>;
};
typedef StageDataCharacter =
{
/**
@ -505,5 +465,6 @@ typedef StageDataCharacter =
/**
* The position to render the character at.
*/ position:Array<Float>
*/
position:Array<Float>
};