1
0
Fork 0
mirror of https://github.com/ninjamuffin99/Funkin.git synced 2025-03-21 17:39:20 +00:00

Merge branch 'rewrite/master' into input-offsets

This commit is contained in:
Cameron Taylor 2024-01-31 00:39:35 -05:00
commit 0f29f2f7a6
95 changed files with 3249 additions and 1625 deletions

View file

@ -3,9 +3,9 @@ description: "sets up haxe shit, using HMM!"
runs:
using: "composite"
steps:
- uses: krdlab/setup-haxe@v1.5.1
- uses: funkincrew/ci-haxe@v3
with:
haxe-version: 4.3.1
haxe-version: 4.3.3
- name: Config haxelib
run: |
haxelib config
@ -19,7 +19,7 @@ runs:
shell: bash
- name: dependency install cache
id: cache-hmm
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: .haxelib
key: ${{ runner.os }}-hmm-${{ hashFiles('**/hmm.json') }}

View file

@ -13,32 +13,32 @@ inputs:
runs:
using: "composite"
steps:
- name: Install butler Windows
if: runner.os == 'Windows'
run: |
curl -L -o butler.zip https://broth.itch.ovh/butler/windows-amd64/LATEST/archive/default
7z x butler.zip
./butler -v
shell: bash
- name: Install butler Mac
if: runner.os == 'macOS'
run: |
curl -L -o butler.zip https://broth.itch.ovh/butler/darwin-amd64/LATEST/archive/default
unzip butler.zip
./butler -V
shell: bash
- name: Install butler Linux
if: runner.os == 'Linux'
run: |
curl -L -o butler.zip https://broth.itch.ovh/butler/linux-amd64/LATEST/archive/default
unzip butler.zip
chmod +x butler
./butler -V
shell: bash
- name: Upload game to itch.io
env:
BUTLER_API_KEY: ${{inputs.butler-key}}
run: |
./butler login
./butler push ${{inputs.build-dir}} ninja-muffin24/funkin-secret:${{inputs.target}}-${GITHUB_REF_NAME}
shell: bash
- name: Install butler Windows
if: runner.os == 'Windows'
run: |
curl -L -o butler.zip https://broth.itch.ovh/butler/windows-amd64/LATEST/archive/default
7z x butler.zip
./butler -v
shell: bash
- name: Install butler Mac
if: runner.os == 'macOS'
run: |
curl -L -o butler.zip https://broth.itch.ovh/butler/darwin-amd64/LATEST/archive/default
unzip butler.zip
./butler -V
shell: bash
- name: Install butler Linux
if: runner.os == 'Linux'
run: |
curl -L -o butler.zip https://broth.itch.ovh/butler/linux-amd64/LATEST/archive/default
unzip butler.zip
chmod +x butler
./butler -V
shell: bash
- name: Upload game to itch.io
env:
BUTLER_API_KEY: ${{inputs.butler-key}}
run: |
./butler login
./butler push ${{inputs.build-dir}} ninja-muffin24/funkin-secret:${{inputs.target}}-${GITHUB_REF_NAME}
shell: bash

View file

@ -4,59 +4,33 @@ on:
push:
jobs:
check_date:
runs-on: [self-hosted, linux]
container: ubuntu:latest
name: Check latest commit
outputs:
should_run: ${{ steps.should_run.outputs.should_run }}
steps:
- name: ensure git cli is installed
run: apt update && apt install sudo git -y
- uses: actions/checkout@v4
with:
submodules: 'recursive'
fetch-depth: 0
token: ${{ secrets.GH_RO_PAT }}
- name: check whether submodules exist
run: |
git config --global --add safe.directory $GITHUB_WORKSPACE
# debug output
echo gh=${{ github.sha }}
echo head=$(git rev-parse HEAD)
echo art=$(git -C art rev-parse HEAD)
echo assets=$(git -C assets rev-parse HEAD)
# checks if HEAD commit hash in submodules is diff from current repo, and therefore exists
test $(git rev-parse HEAD) != $(git -C art rev-parse HEAD)
test $(git rev-parse HEAD) != $(git -C assets rev-parse HEAD)
- id: should_run
continue-on-error: true
name: check latest commit is less than a day
if: ${{ github.event_name == 'schedule' }}
run: test -z $(git rev-list --after="24 hours" ${{ github.sha }}) && echo "::set-output name=should_run::false"
create-nightly-html5:
needs: check_date
if: ${{ needs.check_date.outputs.should_run != 'false'}}
runs-on: [self-hosted, linux]
container: ubuntu:latest
container: ubuntu:23.10
steps:
- name: prepare container
run: |
apt update
apt install sudo git curl unzip -y
echo $GITHUB_WORKSPACE
git config --global --add safe.directory $GITHUB_WORKSPACE
- uses: actions/checkout@v4
- name: get token from gh app
uses: actions/create-github-app-token@v1
id: app_token
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.APP_PEM }}
owner: ${{ github.repository_owner }}
- name: checkout repo
uses: funkincrew/ci-checkout@v6
with:
submodules: 'recursive'
fetch-depth: 0
token: ${{ secrets.GH_RO_PAT }}
token: ${{ steps.app_token.outputs.token }}
- uses: ./.github/actions/setup-haxeshit
- name: Build game
- name: gather game dependencies
run: |
sudo apt-get install -y libx11-dev xorg-dev libgl-dev libxi-dev libxext-dev libasound2-dev libxinerama-dev libxrandr-dev libgl1-mesa-dev
- name: build game
run: |
haxelib run lime build html5 -release --times
ls
- uses: ./.github/actions/upload-itch
@ -65,32 +39,34 @@ jobs:
build-dir: export/release/html5/bin
target: html5
create-nightly-win:
needs: check_date
if: ${{ needs.check_date.outputs.should_run != 'false'}}
runs-on: windows-latest
permissions:
contents: write
actions: write
steps:
- uses: actions/checkout@v4
- name: get token from gh app
uses: actions/create-github-app-token@v1
id: app_token
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.APP_PEM }}
owner: ${{ github.repository_owner }}
- name: checkout repo
uses: funkincrew/ci-checkout@v6
with:
submodules: 'recursive'
fetch-depth: 0
token: ${{ secrets.GH_RO_PAT }}
token: ${{ steps.app_token.outputs.token }}
- uses: ./.github/actions/setup-haxeshit
- name: Make HXCPP cache dir
run: |
mkdir -p ${{ runner.temp }}\hxcpp_cache
- name: Restore build cache
id: cache-build-win
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: |
.haxelib
export
${{ runner.temp }}\hxcpp_cache
key: ${{ runner.os }}-build-win-${{ github.ref_name }}-${{ hashFiles('**/hmm.json') }}
- name: Build game
- name: build game
run: |
haxelib run lime build windows -release -DNO_REDIRECT_ASSETS_FOLDER
dir
@ -101,20 +77,81 @@ jobs:
butler-key: ${{ secrets.BUTLER_API_KEY }}
build-dir: export/release/windows/bin
target: win
create-nightly-mac:
runs-on: [self-hosted, macos]
steps:
- name: prepare container
run: |
git config --global --add safe.directory $GITHUB_WORKSPACE
- name: get token from gh app
uses: actions/create-github-app-token@v1
id: app_token
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.APP_PEM }}
owner: ${{ github.repository_owner }}
- name: checkout repo
uses: funkincrew/ci-checkout@v6
with:
submodules: 'recursive'
token: ${{ steps.app_token.outputs.token }}
- uses: ./.github/actions/setup-haxeshit
- name: Make HXCPP cache dir
run: |
mkdir -p ${{ runner.temp }}/hxcpp_cache
- name: restore build cache
id: cache-build-win
uses: actions/cache@v4
with:
path: |
.haxelib
export
${{ runner.temp }}/hxcpp_cache
key: ${{ runner.os }}-build-mac-${{ github.ref_name }}-${{ hashFiles('**/hmm.json') }}
- name: Build game
run: |
haxelib run lime build macos -release --times
ls
env:
HXCPP_COMPILE_CACHE: "${{ runner.temp }}/hxcpp_cache"
- uses: ./.github/actions/upload-itch
with:
butler-key: ${{ secrets.BUTLER_API_KEY}}
build-dir: export/release/macos/bin
target: macos
# test-unit-win:
# needs: create-nightly-win
# runs-on: windows-latest
# permissions:
# contents: write
# actions: write
# steps:
# - uses: actions/checkout@v4
# - name: get token from gh app
# uses: actions/create-github-app-token@v1
# id: app_token
# with:
# submodules: 'recursive'
# fetch-depth: 0
# token: ${{ secrets.GH_RO_PAT }}
# app-id: ${{ vars.APP_ID }}
# private-key: ${{ secrets.APP_PEM }}
# owner: ${{ github.repository_owner }}
# - name: checkout repo
# uses: funkincrew/ci-checkout@v6
# with:
# submodules: 'recursive'
# token: ${{ steps.app_token.outputs.token }}
# - name: Make HXCPP cache dir
# run: |
# mkdir -p ${{ runner.temp }}\hxcpp_cache
# - name: Restore build cache
# id: cache-build-win
# uses: actions/cache@v4
# with:
# path: |
# .haxelib
# export
# ${{ runner.temp }}\hxcpp_cache
# key: ${{ runner.os }}-test-win-${{ github.ref_name }}-${{ hashFiles('**/hmm.json') }}
# - uses: ./.github/actions/setup-haxeshit
# - name: Run unit tests
# run: |
# cd ./tests/unit/
# ./start-win-native.bat
# env:
# HXCPP_COMPILE_CACHE: "${{ runner.temp }}\\hxcpp_cache"

View file

@ -111,6 +111,7 @@
<haxelib name="tink_json" /> <!-- JSON parsing (DEPRECATED) -->
<haxelib name="thx.semver" /> <!-- Version string handling -->
<haxelib name="hmm" /> <!-- Read library version data at compile time so it can be baked into logs -->
<haxelib name="hxcpp-debug-server" if="desktop debug" /> <!-- VSCode debug support -->
<!--Disable the Flixel core focus lost screen-->
@ -130,8 +131,8 @@
<haxedef name="message.reporting" value="pretty" />
<!-- _________________________________ Custom _______________________________ -->
<!-- Disable trace() calls in release builds to bump up performance. -->
<haxeflag name="--no-traces" unless="debug" />
<!-- Disable trace() calls in release builds to bump up performance.
<haxeflag name="- -no-traces" unless="debug" />-->
<!-- HScript relies heavily on Reflection, which means we can't use DCE. -->
<haxeflag name="-dce no" />
<!-- Ensure all Funkin' classes are available at runtime. -->

View file

@ -11,7 +11,7 @@
"name": "flixel",
"type": "git",
"dir": null,
"ref": "a83738673e7edbf8acba3a1426af284dfe6719fe",
"ref": "07c6018008801972d12275690fc144fcc22e3de6",
"url": "https://github.com/FunkinCrew/flixel"
},
{
@ -37,7 +37,7 @@
"name": "flxanimate",
"type": "git",
"dir": null,
"ref": "d7c5621be742e2c98d523dfe5af7528835eaff1e",
"ref": "9bacdd6ea39f5e3a33b0f5dfb7bc583fe76060d4",
"url": "https://github.com/FunkinCrew/flxanimate"
},
{
@ -54,14 +54,14 @@
"name": "haxeui-core",
"type": "git",
"dir": null,
"ref": "5086e59e7551d775ed4d1fb0188e31de22d1312b",
"ref": "5b2d5b8e7e470cf637953e1369c80a1f42016a75",
"url": "https://github.com/haxeui/haxeui-core"
},
{
"name": "haxeui-flixel",
"type": "git",
"dir": null,
"ref": "2b9cff727999b53ed292b1675ac1c9089ac77600",
"ref": "e9f880522e27134b29df4067f82df7d7e5237b70",
"url": "https://github.com/haxeui/haxeui-flixel"
},
{
@ -107,7 +107,7 @@
"name": "lime",
"type": "git",
"dir": null,
"ref": "737b86f121cdc90358d59e2e527934f267c94a2c",
"ref": "fff39ba6fc64969cd51987ef7491d9345043dc5d",
"url": "https://github.com/FunkinCrew/lime"
},
{
@ -149,7 +149,7 @@
"name": "polymod",
"type": "git",
"dir": null,
"ref": "80d1d309803c1b111866524f9769325e3b8b0b1b",
"ref": "cb11a95d0159271eb3587428cf4b9602e46dc469",
"url": "https://github.com/larsiusprime/polymod"
},
{

View file

@ -21,7 +21,6 @@ abstract Tallies(RawTallies)
bad: 0,
good: 0,
sick: 0,
killer: 0,
totalNotes: 0,
totalNotesHit: 0,
maxCombo: 0,
@ -43,7 +42,6 @@ typedef RawTallies =
var bad:Int;
var good:Int;
var sick:Int;
var killer:Int;
var maxCombo:Int;
var isNewHighscore:Bool;

View file

@ -20,11 +20,11 @@ import openfl.display.BitmapData;
import funkin.data.level.LevelRegistry;
import funkin.data.notestyle.NoteStyleRegistry;
import funkin.data.event.SongEventRegistry;
import funkin.data.stage.StageRegistry;
import funkin.play.cutscene.dialogue.ConversationDataParser;
import funkin.play.cutscene.dialogue.DialogueBoxDataParser;
import funkin.play.cutscene.dialogue.SpeakerDataParser;
import funkin.data.song.SongRegistry;
import funkin.play.stage.StageData.StageDataParser;
import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.modding.module.ModuleHandler;
import funkin.ui.title.TitleState;
@ -217,8 +217,9 @@ class InitState extends FlxState
ConversationDataParser.loadConversationCache();
DialogueBoxDataParser.loadDialogueBoxCache();
SpeakerDataParser.loadSpeakerCache();
StageDataParser.loadStageCache();
StageRegistry.instance.loadEntries();
CharacterDataParser.loadCharacterCache();
ModuleHandler.buildModuleCallbacks();
ModuleHandler.loadModuleCache();

View file

@ -107,6 +107,26 @@ class FunkinSound extends FlxSound
return this;
}
/**
* Called when the user clicks to focus on the window.
*/
override function onFocus():Void
{
if (!_alreadyPaused && this._shouldPlay)
{
resume();
}
}
/**
* Called when the user tabs away from the window.
*/
override function onFocusLost():Void
{
_alreadyPaused = _paused;
pause();
}
public override function resume():FunkinSound
{
if (this._time < 0)

View file

@ -132,6 +132,12 @@ class SoundGroup extends FlxTypedGroup<FunkinSound>
});
}
public override function destroy()
{
stop();
super.destroy();
}
/**
* Remove all sounds from the group.
*/

View file

@ -8,8 +8,7 @@ class PolygonVisGroup extends FlxTypedGroup<PolygonSpectogram>
{
public var playerVis:PolygonSpectogram;
public var opponentVis:PolygonSpectogram;
var instVis:PolygonSpectogram;
public var instVis:PolygonSpectogram;
public function new()
{
@ -51,6 +50,43 @@ class PolygonVisGroup extends FlxTypedGroup<PolygonSpectogram>
instVis = vis;
}
public function clearPlayerVis():Void
{
if (playerVis != null)
{
remove(playerVis);
playerVis.destroy();
playerVis = null;
}
}
public function clearOpponentVis():Void
{
if (opponentVis != null)
{
remove(opponentVis);
opponentVis.destroy();
opponentVis = null;
}
}
public function clearInstVis():Void
{
if (instVis != null)
{
remove(instVis);
instVis.destroy();
instVis = null;
}
}
public function clearAllVis():Void
{
clearPlayerVis();
clearOpponentVis();
clearInstVis();
}
/**
* Overrides the add function to add a visualizer to the group.
* @param vis The visualizer to add.

View file

View file

View file

View file

@ -15,7 +15,7 @@ abstract SongEventSchema(SongEventSchemaRaw)
}
@:arrayAccess
public inline function getByName(name:String):SongEventSchemaField
public function getByName(name:String):SongEventSchemaField
{
for (field in this)
{
@ -41,6 +41,32 @@ abstract SongEventSchema(SongEventSchemaRaw)
{
return this[k] = v;
}
public function stringifyFieldValue(name:String, value:Dynamic):String
{
var field:SongEventSchemaField = getByName(name);
if (field == null) return 'Unknown';
switch (field.type)
{
case SongEventFieldType.STRING:
return Std.string(value);
case SongEventFieldType.INTEGER:
return Std.string(value);
case SongEventFieldType.FLOAT:
return Std.string(value);
case SongEventFieldType.BOOL:
return Std.string(value);
case SongEventFieldType.ENUM:
for (key in field.keys.keys())
{
if (field.keys.get(key) == value) return key;
}
return Std.string(value);
default:
return 'Unknown';
}
}
}
typedef SongEventSchemaRaw = Array<SongEventSchemaField>;

View file

@ -7,9 +7,9 @@ import funkin.ui.story.ScriptedLevel;
class LevelRegistry extends BaseRegistry<Level, LevelData>
{
/**
* The current version string for the stage data format.
* The current version string for the level data format.
* Handle breaking changes by incrementing this value
* and adding migration to the `migrateStageData()` function.
* and adding migration to the `migrateLevelData()` function.
*/
public static final LEVEL_DATA_VERSION:thx.semver.Version = "1.0.0";

View file

@ -1,6 +1,7 @@
package funkin.data.song;
import funkin.data.event.SongEventRegistry;
import funkin.play.event.SongEvent;
import funkin.data.event.SongEventSchema;
import funkin.data.song.SongRegistry;
import thx.semver.Version;
@ -38,10 +39,11 @@ class SongMetadata implements ICloneable<SongMetadata>
public var looped:Bool;
/**
* Instrumental and vocal offsets. Optional, defaults to 0.
* Instrumental and vocal offsets.
* Defaults to an empty SongOffsets object.
*/
@:optional
public var offsets:SongOffsets;
public var offsets:Null<SongOffsets>;
/**
* Data relating to the song's gameplay.
@ -93,7 +95,7 @@ class SongMetadata implements ICloneable<SongMetadata>
result.version = this.version;
result.timeFormat = this.timeFormat;
result.divisions = this.divisions;
result.offsets = this.offsets.clone();
result.offsets = this.offsets != null ? this.offsets.clone() : new SongOffsets(); // if no song offsets found (aka null), so just create new ones
result.timeChanges = this.timeChanges.deepClone();
result.looped = this.looped;
result.playData = this.playData.clone();
@ -701,6 +703,11 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
}
}
public inline function getHandler():Null<SongEvent>
{
return SongEventRegistry.getEvent(this.event);
}
public inline function getSchema():Null<SongEventSchema>
{
return SongEventRegistry.getEventSchema(this.event);
@ -751,6 +758,39 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
return this.value == null ? null : cast Reflect.field(this.value, key);
}
public function buildTooltip():String
{
var eventHandler = getHandler();
var eventSchema = getSchema();
if (eventSchema == null) return 'Unknown Event: ${this.event}';
var result = '${eventHandler.getTitle()}';
var defaultKey = eventSchema.getFirstField()?.name;
var valueStruct:haxe.DynamicAccess<Dynamic> = valueAsStruct(defaultKey);
for (pair in valueStruct.keyValueIterator())
{
var key = pair.key;
var value = pair.value;
var title = eventSchema.getByName(key)?.title ?? 'UnknownField';
if (eventSchema.stringifyFieldValue(key, value) != null) trace(eventSchema.stringifyFieldValue(key, value));
var valueStr = eventSchema.stringifyFieldValue(key, value) ?? 'UnknownValue';
result += '\n- ${title}: ${valueStr}';
}
return result;
}
public function clone():SongEventData
{
return new SongEventData(this.time, this.event, this.value);
}
@:op(A == B)
public function op_equals(other:SongEventData):Bool
{
@ -826,7 +866,13 @@ class SongNoteDataRaw implements ICloneable<SongNoteDataRaw>
@:alias("l")
@:default(0)
@:optional
public var length:Float;
public var length(default, set):Float;
function set_length(value:Float):Float
{
_stepLength = null;
return length = value;
}
/**
* The kind of the note.
@ -883,6 +929,11 @@ class SongNoteDataRaw implements ICloneable<SongNoteDataRaw>
return _stepTime = Conductor.instance.getTimeInSteps(this.time);
}
/**
* The length of the note, if applicable, in steps.
* Calculated from the length and the BPM.
* Cached for performance. Set to `null` to recalculate.
*/
@:jignored
var _stepLength:Null<Float> = null;
@ -907,9 +958,14 @@ class SongNoteDataRaw implements ICloneable<SongNoteDataRaw>
}
else
{
var lengthMs:Float = Conductor.instance.getStepTimeInMs(value) - this.time;
var endStep:Float = getStepTime() + value;
var endMs:Float = Conductor.instance.getStepTimeInMs(endStep);
var lengthMs:Float = endMs - this.time;
this.length = lengthMs;
}
// Recalculate the step length next time it's requested.
_stepLength = null;
}
@ -980,6 +1036,10 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw
@:op(A == B)
public function op_equals(other:SongNoteData):Bool
{
// Handle the case where one value is null.
if (this == null) return other == null;
if (other == null) return false;
if (this.kind == '')
{
if (other.kind != '' && other.kind != 'normal') return false;
@ -995,6 +1055,10 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw
@:op(A != B)
public function op_notEquals(other:SongNoteData):Bool
{
// Handle the case where one value is null.
if (this == null) return other == null;
if (other == null) return false;
if (this.kind == '')
{
if (other.kind != '' && other.kind != 'normal') return true;
@ -1010,24 +1074,32 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw
@:op(A > B)
public function op_greaterThan(other:SongNoteData):Bool
{
if (other == null) return false;
return this.time > other.time;
}
@:op(A < B)
public function op_lessThan(other:SongNoteData):Bool
{
if (other == null) return false;
return this.time < other.time;
}
@:op(A >= B)
public function op_greaterThanOrEquals(other:SongNoteData):Bool
{
if (other == null) return false;
return this.time >= other.time;
}
@:op(A <= B)
public function op_lessThanOrEquals(other:SongNoteData):Bool
{
if (other == null) return false;
return this.time <= other.time;
}

View file

@ -273,7 +273,7 @@ class SongDataUtils
}
/**
* Filter a list of notes to only include notes whose data is within the given range.
* Filter a list of notes to only include notes whose data is within the given range, inclusive.
*/
public static function getNotesInDataRange(notes:Array<SongNoteData>, start:Int, end:Int):Array<SongNoteData>
{

View file

@ -12,6 +12,7 @@ import funkin.util.VersionUtil;
using funkin.data.song.migrator.SongDataMigrator;
@:nullSafety
class SongRegistry extends BaseRegistry<Song, SongMetadata>
{
/**
@ -31,7 +32,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
public static final SONG_MUSIC_DATA_VERSION_RULE:thx.semver.VersionRule = "2.0.x";
public static var DEFAULT_GENERATEDBY(get, null):String;
public static var DEFAULT_GENERATEDBY(get, never):String;
static function get_DEFAULT_GENERATEDBY():String
{
@ -88,7 +89,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
{
try
{
var entry:Song = createEntry(entryId);
var entry:Null<Song> = createEntry(entryId);
if (entry != null)
{
trace(' Loaded entry data: ${entry}');
@ -126,7 +127,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
var parser = new json2object.JsonParser<SongMetadata>();
parser.ignoreUnknownVariables = false;
parser.ignoreUnknownVariables = true;
switch (loadEntryMetadataFile(id, variation))
{
@ -149,7 +150,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
var parser = new json2object.JsonParser<SongMetadata>();
parser.ignoreUnknownVariables = false;
parser.ignoreUnknownVariables = true;
parser.fromJson(contents, fileName);
if (parser.errors.length > 0)
@ -209,7 +210,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
var parser = new json2object.JsonParser<SongMetadata_v2_1_0>();
parser.ignoreUnknownVariables = false;
parser.ignoreUnknownVariables = true;
switch (loadEntryMetadataFile(id, variation))
{
@ -231,7 +232,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
var parser = new json2object.JsonParser<SongMetadata_v2_0_0>();
parser.ignoreUnknownVariables = false;
parser.ignoreUnknownVariables = true;
switch (loadEntryMetadataFile(id, variation))
{
@ -251,7 +252,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
function parseEntryMetadataRaw_v2_1_0(contents:String, ?fileName:String = 'raw'):Null<SongMetadata>
{
var parser = new json2object.JsonParser<SongMetadata_v2_1_0>();
parser.ignoreUnknownVariables = false;
parser.ignoreUnknownVariables = true;
parser.fromJson(contents, fileName);
if (parser.errors.length > 0)
@ -265,7 +266,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
function parseEntryMetadataRaw_v2_0_0(contents:String, ?fileName:String = 'raw'):Null<SongMetadata>
{
var parser = new json2object.JsonParser<SongMetadata_v2_0_0>();
parser.ignoreUnknownVariables = false;
parser.ignoreUnknownVariables = true;
parser.fromJson(contents, fileName);
if (parser.errors.length > 0)
@ -346,7 +347,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
var parser = new json2object.JsonParser<SongChartData>();
parser.ignoreUnknownVariables = false;
parser.ignoreUnknownVariables = true;
switch (loadEntryChartFile(id, variation))
{
@ -369,7 +370,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
var parser = new json2object.JsonParser<SongChartData>();
parser.ignoreUnknownVariables = false;
parser.ignoreUnknownVariables = true;
parser.fromJson(contents, fileName);
if (parser.errors.length > 0)
@ -455,7 +456,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
{
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
var entryStr:Null<String> = loadEntryMetadataFile(id, variation)?.contents;
var entryVersion:thx.semver.Version = VersionUtil.getVersionFromJSON(entryStr);
var entryVersion:Null<thx.semver.Version> = VersionUtil.getVersionFromJSON(entryStr);
return entryVersion;
}
@ -463,7 +464,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
{
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
var entryStr:Null<String> = loadEntryChartFile(id, variation)?.contents;
var entryVersion:thx.semver.Version = VersionUtil.getVersionFromJSON(entryStr);
var entryVersion:Null<thx.semver.Version> = VersionUtil.getVersionFromJSON(entryStr);
return entryVersion;
}

View file

View file

@ -0,0 +1,199 @@
package funkin.data.stage;
import funkin.data.animation.AnimationData;
@:nullSafety
class StageData
{
/**
* The sematic version number of the stage data JSON format.
* Supports fancy comparisons like NPM does it's neat.
*/
@:default(funkin.data.stage.StageRegistry.STAGE_DATA_VERSION)
public var version:String;
public var name:String = 'Unknown';
public var props:Array<StageDataProp> = [];
public var characters:StageDataCharacters;
@:default(1.0)
@:optional
public var cameraZoom:Null<Float>;
public function new()
{
this.version = StageRegistry.STAGE_DATA_VERSION;
this.characters = makeDefaultCharacters();
}
function makeDefaultCharacters():StageDataCharacters
{
return {
bf:
{
zIndex: 0,
position: [0, 0],
cameraOffsets: [-100, -100]
},
dad:
{
zIndex: 0,
position: [0, 0],
cameraOffsets: [100, -100]
},
gf:
{
zIndex: 0,
position: [0, 0],
cameraOffsets: [0, 0]
}
};
}
/**
* Convert this StageData into a JSON string.
*/
public function serialize(pretty:Bool = true):String
{
var writer = new json2object.JsonWriter<StageData>();
return writer.write(this, pretty ? ' ' : null);
}
}
typedef StageDataCharacters =
{
var bf:StageDataCharacter;
var dad:StageDataCharacter;
var gf:StageDataCharacter;
};
typedef StageDataProp =
{
/**
* The name of the prop for later lookup by scripts.
* Optional; if unspecified, the prop can't be referenced by scripts.
*/
@:optional
var name:String;
/**
* The asset used to display the prop.
* NOTE: As of Stage data v1.0.1, you can also use a color here to create a rectangle, like "#ff0000".
* In this case, the `scale` property will be used to determine the size of the prop.
*/
var assetPath:String;
/**
* The position of the prop as an [x, y] array of two floats.
*/
var position:Array<Float>;
/**
* A number determining the stack order of the prop, relative to other props and the characters in the stage.
* Props with lower numbers render below those with higher numbers.
* This is just like CSS, it isn't hard.
* @default 0
*/
@:optional
@:default(0)
var zIndex:Int;
/**
* If set to true, anti-aliasing will be forcibly disabled on the sprite.
* This prevents blurry images on pixel-art levels.
* @default false
*/
@:optional
@:default(false)
var isPixel:Bool;
/**
* Either the scale of the prop as a float, or the [w, h] scale as an array of two floats.
* Pro tip: On pixel-art levels, save the sprite small and set this value to 6 or so to save memory.
*/
@:jcustomparse(funkin.data.DataParse.eitherFloatOrFloats)
@:jcustomwrite(funkin.data.DataWrite.eitherFloatOrFloats)
@:optional
var scale:haxe.ds.Either<Float, Array<Float>>;
/**
* The alpha of the prop, as a float.
* @default 1.0
*/
@:optional
@:default(1.0)
var alpha:Float;
/**
* If not zero, this prop will play an animation every X beats of the song.
* This requires animations to be defined. If `danceLeft` and `danceRight` are defined,
* they will alternated between, otherwise the `idle` animation will be used.
*
* @default 0
*/
@:default(0)
@:optional
var danceEvery:Int;
/**
* How much the prop scrolls relative to the camera. Used to create a parallax effect.
* Represented as an [x, y] array of two floats.
* [1, 1] means the prop moves 1:1 with the camera.
* [0.5, 0.5] means the prop half as much as the camera.
* [0, 0] means the prop is not moved.
* @default [0, 0]
*/
@:optional
@:default([0, 0])
var scroll:Array<Float>;
/**
* An optional array of animations which the prop can play.
* @default Prop has no animations.
*/
@:optional
@:default([])
var animations:Array<AnimationData>;
/**
* If animations are used, this is the name of the animation to play first.
* @default Don't play an animation.
*/
@:optional
var startingAnimation:Null<String>;
/**
* The animation type to use.
* Options: "sparrow", "packer"
* @default "sparrow"
*/
@:default("sparrow")
@:optional
var animType:String;
};
typedef StageDataCharacter =
{
/**
* A number determining the stack order of the character, relative to props and other characters in the stage.
* Again, just like CSS.
* @default 0
*/
@:optional
@:default(0)
var zIndex:Int;
/**
* The position to render the character at.
*/
@:optional
@:default([0, 0])
var position:Array<Float>;
/**
* The camera offsets to apply when focusing on the character on this stage.
* @default [-100, -100] for BF, [100, -100] for DAD/OPPONENT, [0, 0] for GF
*/
@:optional
var cameraOffsets:Array<Float>;
};

View file

@ -0,0 +1,103 @@
package funkin.data.stage;
import funkin.data.stage.StageData;
import funkin.play.stage.Stage;
import funkin.play.stage.ScriptedStage;
class StageRegistry extends BaseRegistry<Stage, StageData>
{
/**
* 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 STAGE_DATA_VERSION:thx.semver.Version = "1.0.1";
public static final STAGE_DATA_VERSION_RULE:thx.semver.VersionRule = "1.0.x";
public static final instance:StageRegistry = new StageRegistry();
public function new()
{
super('STAGE', 'stages', STAGE_DATA_VERSION_RULE);
}
/**
* Read, parse, and validate the JSON data and produce the corresponding data object.
*/
public function parseEntryData(id:String):Null<StageData>
{
// JsonParser does not take type parameters,
// otherwise this function would be in BaseRegistry.
var parser = new json2object.JsonParser<StageData>();
parser.ignoreUnknownVariables = false;
switch (loadEntryFile(id))
{
case {fileName: fileName, contents: contents}:
parser.fromJson(contents, fileName);
default:
return null;
}
if (parser.errors.length > 0)
{
printErrors(parser.errors, id);
return null;
}
return parser.value;
}
/**
* Parse and validate the JSON data and produce the corresponding data object.
*
* NOTE: Must be implemented on the implementation class.
* @param contents The JSON as a string.
* @param fileName An optional file name for error reporting.
*/
public function parseEntryDataRaw(contents:String, ?fileName:String):Null<StageData>
{
var parser = new json2object.JsonParser<StageData>();
parser.ignoreUnknownVariables = false;
parser.fromJson(contents, fileName);
if (parser.errors.length > 0)
{
printErrors(parser.errors, fileName);
return null;
}
return parser.value;
}
function createScriptedEntry(clsName:String):Stage
{
return ScriptedStage.init(clsName, "unknown");
}
function getScriptedClassNames():Array<String>
{
return ScriptedStage.listScriptClasses();
}
/**
* A list of all the stages from the base game, in order.
* TODO: Should this be hardcoded?
*/
public function listBaseGameStageIds():Array<String>
{
return [
"mainStage", "spookyMansion", "phillyTrain", "limoRide", "mallXmas", "mallEvil", "school", "schoolEvil", "tankmanBattlefield", "phillyStreets",
"phillyBlazin",
];
}
/**
* A list of all installed story weeks that are not from the base game.
*/
public function listModdedStageIds():Array<String>
{
return listEntryIds().filter(function(id:String):Bool {
return listBaseGameStageIds().indexOf(id) == -1;
});
}
}

View file

@ -0,0 +1,53 @@
package funkin.graphics;
import flixel.FlxSprite;
import flixel.util.FlxColor;
import flixel.graphics.FlxGraphic;
/**
* An FlxSprite with additional functionality.
*/
class FunkinSprite extends FlxSprite
{
/**
* @param x Starting X position
* @param y Starting Y position
*/
public function new(?x:Float = 0, ?y:Float = 0)
{
super(x, y);
}
/**
* Acts similarly to `makeGraphic`, but with improved memory usage,
* at the expense of not being able to paint onto the sprite.
*
* @param width The target width of the sprite.
* @param height The target height of the sprite.
* @param color The color to fill the sprite with.
*/
public function makeSolidColor(width:Int, height:Int, color:FlxColor = FlxColor.WHITE):FunkinSprite
{
var graphic:FlxGraphic = FlxG.bitmap.create(2, 2, color, false, 'solid#${color.toHexString(true, false)}');
frames = graphic.imageFrame;
scale.set(width / 2, height / 2);
updateHitbox();
return this;
}
/**
* Ensure scale is applied when cloning a sprite.
* The default `clone()` method acts kinda weird TBH.
* @return A clone of this sprite.
*/
public override function clone():FunkinSprite
{
var result = new FunkinSprite(this.x, this.y);
result.frames = this.frames;
result.scale.set(this.scale.x, this.scale.y);
result.updateHitbox();
return result;
}
}

View file

@ -4,11 +4,12 @@ import funkin.util.macro.ClassMacro;
import funkin.modding.module.ModuleHandler;
import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.data.song.SongData;
import funkin.play.stage.StageData;
import funkin.data.stage.StageData;
import polymod.Polymod;
import polymod.backends.PolymodAssets.PolymodAssetType;
import polymod.format.ParseRules.TextFileFormat;
import funkin.data.event.SongEventRegistry;
import funkin.data.stage.StageRegistry;
import funkin.util.FileUtil;
import funkin.data.level.LevelRegistry;
import funkin.data.notestyle.NoteStyleRegistry;
@ -275,7 +276,7 @@ class PolymodHandler
ConversationDataParser.loadConversationCache();
DialogueBoxDataParser.loadDialogueBoxCache();
SpeakerDataParser.loadSpeakerCache();
StageDataParser.loadStageCache();
StageRegistry.instance.loadEntries();
CharacterDataParser.loadCharacterCache();
ModuleHandler.loadModuleCache();
}

View file

@ -7,6 +7,7 @@ import flixel.sound.FlxSound;
import funkin.ui.story.StoryMenuState;
import flixel.util.FlxColor;
import flixel.util.FlxTimer;
import funkin.graphics.FunkinSprite;
import funkin.ui.MusicBeatSubState;
import funkin.modding.events.ScriptEvent;
import funkin.modding.events.ScriptEventDispatcher;
@ -22,6 +23,12 @@ import funkin.play.character.BaseCharacter;
*/
class GameOverSubState extends MusicBeatSubState
{
/**
* The currently active GameOverSubState.
* There should be only one GameOverSubState in existance at a time, we can use a singleton.
*/
public static var instance:GameOverSubState = null;
/**
* Which alternate animation on the character to use.
* You can set this via script.
@ -87,6 +94,13 @@ class GameOverSubState extends MusicBeatSubState
override public function create()
{
if (instance != null)
{
// TODO: Do something in this case? IDK.
trace('WARNING: GameOverSubState instance already exists. This should not happen.');
}
instance = this;
super.create();
//
@ -94,11 +108,12 @@ class GameOverSubState extends MusicBeatSubState
//
// Add a black background to the screen.
var bg = new FlxSprite().makeGraphic(FlxG.width, FlxG.height, FlxColor.BLACK);
var bg = new FunkinSprite().makeSolidColor(FlxG.width * 2, FlxG.height * 2, FlxColor.BLACK);
// We make this transparent so that we can see the stage underneath during debugging,
// but it's normally opaque.
bg.alpha = transparent ? 0.25 : 1.0;
bg.scrollFactor.set();
bg.screenCenter();
add(bg);
// Pluck Boyfriend from the PlayState and place him (in the same position) in the GameOverSubState.
@ -220,6 +235,7 @@ class GameOverSubState extends MusicBeatSubState
playJeffQuote();
// Start music at lower volume
startDeathMusic(0.2, false);
boyfriend.playAnimation('deathLoop' + animationSuffix);
}
default:
// Start music at normal volume once the initial death animation finishes.
@ -280,10 +296,10 @@ class GameOverSubState extends MusicBeatSubState
*/
function startDeathMusic(?startingVolume:Float = 1, force:Bool = false):Void
{
var musicPath = Paths.music('gameOver' + musicSuffix);
var musicPath = Paths.music('gameplay/gameover/gameOver' + musicSuffix);
if (isEnding)
{
musicPath = Paths.music('gameOverEnd' + musicSuffix);
musicPath = Paths.music('gameplay/gameover/gameOverEnd' + musicSuffix);
}
if (!gameOverMusic.playing || force)
{
@ -303,7 +319,7 @@ class GameOverSubState extends MusicBeatSubState
public static function playBlueBalledSFX()
{
blueballed = true;
FlxG.sound.play(Paths.sound('fnf_loss_sfx' + blueBallSuffix));
FlxG.sound.play(Paths.sound('gameplay/gameover/fnf_loss_sfx' + blueBallSuffix));
}
var playingJeffQuote:Bool = false;
@ -326,6 +342,11 @@ class GameOverSubState extends MusicBeatSubState
}
});
}
public override function toString():String
{
return "GameOverSubState";
}
}
typedef GameOverParams =

View file

@ -50,11 +50,11 @@ import funkin.play.notes.SustainTrail;
import funkin.play.scoring.Scoring;
import funkin.play.song.Song;
import funkin.data.song.SongRegistry;
import funkin.data.stage.StageRegistry;
import funkin.data.song.SongData.SongEventData;
import funkin.data.song.SongData.SongNoteData;
import funkin.data.song.SongData.SongCharacterData;
import funkin.play.stage.Stage;
import funkin.play.stage.StageData.StageDataParser;
import funkin.ui.transition.LoadingState;
import funkin.play.components.PopUpStuff;
import funkin.ui.options.PreferencesMenu;
@ -1353,7 +1353,8 @@ class PlayState extends MusicBeatSubState
*/
function loadStage(id:String):Void
{
currentStage = StageDataParser.fetchStage(id);
currentStage = StageRegistry.instance.fetchEntry(id);
currentStage.revive(); // Stages are killed and props destroyed when the PlayState is destroyed to save memory.
if (currentStage != null)
{
@ -1791,7 +1792,64 @@ class PlayState extends MusicBeatSubState
{
if (playerStrumline?.notes?.members == null || opponentStrumline?.notes?.members == null) return;
opponentStrumline.processNotes(null, dispatchEvent);
// Process notes on the opponent's side.
for (note in opponentStrumline.notes.members)
{
if (note == null) continue;
// TODO: Does this properly account for offsets?
var hitWindowStart = note.strumTime - Constants.HIT_WINDOW_MS;
var hitWindowCenter = note.strumTime;
var hitWindowEnd = note.strumTime + Constants.HIT_WINDOW_MS;
if (Conductor.instance.songPosition > hitWindowEnd)
{
if (note.hasMissed) continue;
note.tooEarly = false;
note.mayHit = false;
note.hasMissed = true;
if (note.holdNoteSprite != null) note.holdNoteSprite.missedNote = true;
}
else if (Conductor.instance.songPosition > hitWindowCenter)
{
if (note.hasBeenHit) continue;
// Call an event to allow canceling the note hit.
// NOTE: This is what handles the character animations!
var event:NoteScriptEvent = new NoteScriptEvent(NOTE_HIT, note, 0, true);
dispatchEvent(event);
// Calling event.cancelEvent() skips all the other logic! Neat!
if (event.eventCanceled) continue;
// Command the opponent to hit the note on time.
// NOTE: This is what handles the strumline and cleaning up the note itself!
opponentStrumline.hitNote(note);
if (note.holdNoteSprite != null)
{
opponentStrumline.playNoteHoldCover(note.holdNoteSprite);
}
}
else if (Conductor.instance.songPosition > hitWindowStart)
{
if (note.hasBeenHit || note.hasMissed) continue;
note.tooEarly = false;
note.mayHit = true;
note.hasMissed = false;
if (note.holdNoteSprite != null) note.holdNoteSprite.missedNote = false;
}
else
{
note.tooEarly = true;
note.mayHit = false;
note.hasMissed = false;
if (note.holdNoteSprite != null) note.holdNoteSprite.missedNote = false;
}
}
// Process hold notes on the opponent's side.
for (holdNote in opponentStrumline.holdNotes.members)
@ -1808,11 +1866,77 @@ class PlayState extends MusicBeatSubState
}
}
// TODO: Potential penalty for dropping a hold note?
// if (holdNote.missedNote && !holdNote.handledMiss) { holdNote.handledMiss = true; }
if (holdNote.missedNote && !holdNote.handledMiss)
{
// When the opponent drops a hold note.
holdNote.handledMiss = true;
// We dropped a hold note.
// Mute vocals and play miss animation, but don't penalize.
vocals.opponentVolume = 0;
currentStage.getOpponent().playSingAnimation(holdNote.noteData.getDirection(), true);
}
}
playerStrumline.processNotes(onNoteMiss, dispatchEvent);
// Process notes on the player's side.
for (note in playerStrumline.notes.members)
{
if (note == null) continue;
if (note.hasBeenHit)
{
note.tooEarly = false;
note.mayHit = false;
note.hasMissed = false;
continue;
}
var hitWindowStart = note.strumTime - Constants.HIT_WINDOW_MS;
var hitWindowCenter = note.strumTime;
var hitWindowEnd = note.strumTime + Constants.HIT_WINDOW_MS;
if (Conductor.instance.songPosition > hitWindowEnd)
{
note.tooEarly = false;
note.mayHit = false;
note.hasMissed = true;
if (note.holdNoteSprite != null) note.holdNoteSprite.missedNote = true;
}
else if (Conductor.instance.songPosition > hitWindowStart)
{
note.tooEarly = false;
note.mayHit = true;
note.hasMissed = false;
if (note.holdNoteSprite != null) note.holdNoteSprite.missedNote = false;
}
else
{
note.tooEarly = true;
note.mayHit = false;
note.hasMissed = false;
if (note.holdNoteSprite != null) note.holdNoteSprite.missedNote = false;
}
// This becomes true when the note leaves the hit window.
// It might still be on screen.
if (note.hasMissed && !note.handledMiss)
{
// Call an event to allow canceling the note miss.
// NOTE: This is what handles the character animations!
var event:NoteScriptEvent = new NoteScriptEvent(NOTE_MISS, note, 0, true);
dispatchEvent(event);
// Calling event.cancelEvent() skips all the other logic! Neat!
if (event.eventCanceled) continue;
// Judge the miss.
// NOTE: This is what handles the scoring.
trace('Missed note! ${note.noteData}');
onNoteMiss(note);
note.handledMiss = true;
}
}
// Process hold notes on the player's side.
// This handles scoring so we don't need it on the opponent's side.
@ -1828,8 +1952,15 @@ class PlayState extends MusicBeatSubState
songScore += Std.int(Constants.SCORE_HOLD_BONUS_PER_SECOND * elapsed);
}
// TODO: Potential penalty for dropping a hold note?
// if (holdNote.missedNote && !holdNote.handledMiss) { holdNote.handledMiss = true; }
if (holdNote.missedNote && !holdNote.handledMiss)
{
// The player dropped a hold note.
holdNote.handledMiss = true;
// Mute vocals and play miss animation, but don't penalize.
vocals.playerVolume = 0;
currentStage.getBoyfriend().playSingAnimation(holdNote.noteData.getDirection(), true);
}
}
}
@ -1921,8 +2052,6 @@ class PlayState extends MusicBeatSubState
trace('Hit note! ${targetNote.noteData}');
goodNoteHit(targetNote, input);
targetNote.visible = false;
targetNote.kill();
notesInDirection.remove(targetNote);
// Play the strumline animation.
@ -1954,15 +2083,8 @@ class PlayState extends MusicBeatSubState
// Calling event.cancelEvent() skips all the other logic! Neat!
if (event.eventCanceled) return;
Highscore.tallies.combo++;
Highscore.tallies.totalNotesHit++;
if (Highscore.tallies.combo > Highscore.tallies.maxCombo) Highscore.tallies.maxCombo = Highscore.tallies.combo;
popUpScore(note, input);
playerStrumline.hitNote(note);
if (note.isHoldNote && note.holdNoteSprite != null)
{
playerStrumline.playNoteHoldCover(note.holdNoteSprite);
@ -1978,8 +2100,6 @@ class PlayState extends MusicBeatSubState
function onNoteMiss(note:NoteSprite):Void
{
// a MISS is when you let a note scroll past you!!
Highscore.tallies.missed++;
var event:NoteScriptEvent = new NoteScriptEvent(NOTE_MISS, note, Highscore.tallies.combo, true);
dispatchEvent(event);
// Calling event.cancelEvent() skips all the other logic! Neat!
@ -2027,8 +2147,11 @@ class PlayState extends MusicBeatSubState
}
vocals.playerVolume = 0;
Highscore.tallies.missed++;
if (Highscore.tallies.combo != 0)
{
// Break the combo.
Highscore.tallies.combo = comboPopUps.displayCombo(0);
}
@ -2164,8 +2287,10 @@ class PlayState extends MusicBeatSubState
vocals.playerVolume = 1;
// Calculate the input latency (do this as late as possible).
var inputLatencyMs:Float = haxe.Int64.toInt(PreciseInputManager.getCurrentTimestamp() - input.timestamp) / 1000.0 / 1000.0;
trace('Input: ${daNote.noteData.getDirectionName()} pressed ${inputLatencyMs}ms ago!');
// trace('Compare: ${PreciseInputManager.getCurrentTimestamp()} - ${input.timestamp}');
var inputLatencyNs:Int64 = PreciseInputManager.getCurrentTimestamp() - input.timestamp;
var inputLatencyMs:Float = inputLatencyNs.toFloat() / Constants.NS_PER_MS;
// trace('Input: ${daNote.noteData.getDirectionName()} pressed ${inputLatencyMs}ms ago!');
// Get the offset and compensate for input latency.
// Round inward (trim remainder) for consistency.
@ -2174,29 +2299,51 @@ class PlayState extends MusicBeatSubState
var score = Scoring.scoreNote(noteDiff, PBOT1);
var daRating = Scoring.judgeNote(noteDiff, PBOT1);
if (daRating == 'miss')
{
// If daRating is 'miss', that means we made a mistake and should not continue.
trace('[WARNING] popUpScore judged a note as a miss!');
// TODO: Remove this.
comboPopUps.displayRating('miss');
return;
}
var isComboBreak = false;
switch (daRating)
{
case 'killer':
Highscore.tallies.killer += 1;
health += Constants.HEALTH_KILLER_BONUS;
case 'sick':
Highscore.tallies.sick += 1;
health += Constants.HEALTH_SICK_BONUS;
isComboBreak = Constants.JUDGEMENT_SICK_COMBO_BREAK;
case 'good':
Highscore.tallies.good += 1;
health += Constants.HEALTH_GOOD_BONUS;
isComboBreak = Constants.JUDGEMENT_GOOD_COMBO_BREAK;
case 'bad':
Highscore.tallies.bad += 1;
health += Constants.HEALTH_BAD_BONUS;
isComboBreak = Constants.JUDGEMENT_BAD_COMBO_BREAK;
case 'shit':
Highscore.tallies.shit += 1;
health += Constants.HEALTH_SHIT_BONUS;
case 'miss':
Highscore.tallies.missed += 1;
health -= Constants.HEALTH_MISS_PENALTY;
isComboBreak = Constants.JUDGEMENT_SHIT_COMBO_BREAK;
}
if (daRating == "sick" || daRating == "killer")
if (isComboBreak)
{
// Break the combo, but don't increment tallies.misses.
Highscore.tallies.combo = comboPopUps.displayCombo(0);
}
else
{
Highscore.tallies.combo++;
Highscore.tallies.totalNotesHit++;
if (Highscore.tallies.combo > Highscore.tallies.maxCombo) Highscore.tallies.maxCombo = Highscore.tallies.combo;
}
playerStrumline.hitNote(daNote, !isComboBreak);
if (daRating == "sick")
{
playerStrumline.playNoteSplash(daNote.noteData.getDirection());
}
@ -2332,7 +2479,6 @@ class PlayState extends MusicBeatSubState
score: songScore,
tallies:
{
killer: Highscore.tallies.killer,
sick: Highscore.tallies.sick,
good: Highscore.tallies.good,
bad: Highscore.tallies.bad,
@ -2383,7 +2529,6 @@ class PlayState extends MusicBeatSubState
tallies:
{
// TODO: Sum up the values for the whole level!
killer: 0,
sick: 0,
good: 0,
bad: 0,

View file

@ -9,6 +9,7 @@ import flixel.math.FlxMath;
import flixel.math.FlxPoint.FlxCallbackPoint;
import flixel.math.FlxPoint;
import flixel.math.FlxRect;
import funkin.graphics.FunkinSprite;
import flixel.system.FlxAssets.FlxGraphicAsset;
import flixel.util.FlxColor;
import flixel.util.FlxDestroyUtil;
@ -621,7 +622,7 @@ class AnimateAtlasCharacter extends BaseCharacter
* This functionality isn't supported in SpriteGroup
* @return this sprite group
*/
public override function loadGraphicFromSprite(Sprite:FlxSprite):FlxSprite
public override function loadGraphicFromSprite(Sprite:FlxSprite):FunkinSprite
{
#if FLX_DEBUG
throw "This function is not supported in FlxSpriteGroup";

View file

@ -1,5 +1,6 @@
package funkin.play.character;
import flixel.graphics.frames.FlxAtlasFrames;
import flixel.graphics.frames.FlxFramesCollection;
import funkin.modding.events.ScriptEvent;
import funkin.util.assets.FlxAnimationUtil;
@ -7,35 +8,17 @@ import funkin.play.character.CharacterData.CharacterRenderType;
/**
* For some characters which use Sparrow atlases, the spritesheets need to be split
* into multiple files. This character renderer handles by showing the appropriate sprite.
* into multiple files. This character renderer concatenates these together into a single sprite.
*
* Examples in base game include BF Holding GF (most of the sprites are in one file
* but the death animation is in a separate file).
* Only example I can think of in mods is Tricky (which has a separate file for each animation).
*
* BaseCharacter has game logic, SparrowCharacter has only rendering logic.
* BaseCharacter has game logic, MultiSparrowCharacter has only rendering logic.
* KEEP THEM SEPARATE!
*
* TODO: Rewrite this to use a single frame collection.
* @see https://github.com/HaxeFlixel/flixel/issues/2587#issuecomment-1179620637
*/
class MultiSparrowCharacter extends BaseCharacter
{
/**
* The actual group which holds all spritesheets this character uses.
*/
var members:Map<String, FlxFramesCollection> = new Map<String, FlxFramesCollection>();
/**
* A map between animation names and what frame collection the animation should use.
*/
var animAssetPath:Map<String, String> = new Map<String, String>();
/**
* The current frame collection being used.
*/
var activeMember:String;
public function new(id:String)
{
super(id, CharacterRenderType.MultiSparrow);
@ -51,7 +34,7 @@ class MultiSparrowCharacter extends BaseCharacter
function buildSprites():Void
{
buildSpritesheets();
buildSpritesheet();
buildAnimations();
if (_data.isPixel)
@ -66,95 +49,49 @@ class MultiSparrowCharacter extends BaseCharacter
}
}
function buildSpritesheets():Void
function buildSpritesheet():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];
var assetList = [];
for (anim in _data.animations)
{
if (anim.assetPath != null && !assetList.contains(anim.assetPath))
{
assetList.push(anim.assetPath);
}
animAssetPath.set(anim.name, anim.assetPath);
}
// Load the Sparrow atlas for each path and store them in the members map.
var texture:FlxAtlasFrames = Paths.getSparrowAtlas(_data.assetPath, 'shared');
if (texture == null)
{
trace('Multi-Sparrow atlas could not load PRIMARY texture: ${_data.assetPath}');
}
else
{
trace('Creating multi-sparrow atlas: ${_data.assetPath}');
texture.parent.destroyOnNoUse = false;
}
for (asset in assetList)
{
var texture:FlxFramesCollection = Paths.getSparrowAtlas(asset, 'shared');
var subTexture:FlxAtlasFrames = Paths.getSparrowAtlas(asset, 'shared');
// If we don't do this, the unused textures will be removed as soon as they're loaded.
if (texture == null)
if (subTexture == null)
{
trace('Multi-Sparrow atlas could not load texture: ${asset}');
trace('Multi-Sparrow atlas could not load subtexture: ${asset}');
}
else
{
trace('Adding multi-sparrow atlas: ${asset}');
texture.parent.destroyOnNoUse = false;
members.set(asset, texture);
trace('Concatenating multi-sparrow atlas: ${asset}');
subTexture.parent.destroyOnNoUse = false;
}
texture.addAtlas(subTexture);
}
// Use the default frame collection to start.
loadFramesByAssetPath(_data.assetPath);
}
/**
* Replace this sprite's animation frames with the ones at this asset path.
*/
function loadFramesByAssetPath(assetPath:String):Void
{
if (_data.assetPath == null)
{
trace('[ERROR] Multi-Sparrow character has no default asset path!');
return;
}
if (assetPath == null)
{
// trace('Asset path is null, falling back to default. This is normal!');
loadFramesByAssetPath(_data.assetPath);
return;
}
if (this.activeMember == assetPath)
{
// trace('Already using this asset path: ${assetPath}');
return;
}
if (members.exists(assetPath))
{
// Switch to a new set of sprites.
// trace('Loading frames from asset path: ${assetPath}');
this.frames = members.get(assetPath);
this.activeMember = assetPath;
this.setScale(_data.scale);
}
else
{
trace('[WARN] MultiSparrow character ${characterId} could not find asset path: ${assetPath}');
}
}
/**
* Replace this sprite's animation frames with the ones needed to play this animation.
*/
function loadFramesByAnimName(animName)
{
if (animAssetPath.exists(animName))
{
loadFramesByAssetPath(animAssetPath.get(animName));
}
else
{
trace('[WARN] MultiSparrow character ${characterId} could not find animation: ${animName}');
}
this.frames = texture;
this.setScale(_data.scale);
}
function buildAnimations()
@ -164,7 +101,6 @@ class MultiSparrowCharacter extends BaseCharacter
// We need to swap to the proper frame collection before adding the animations, I think?
for (anim in _data.animations)
{
loadFramesByAnimName(anim.name);
FlxAnimationUtil.addAtlasAnimation(this, anim);
if (anim.offsets == null)
@ -187,37 +123,6 @@ class MultiSparrowCharacter extends BaseCharacter
// unless we're forcing a new animation.
if (!this.canPlayOtherAnims && !ignoreOther) return;
loadFramesByAnimName(name);
super.playAnimation(name, restart, ignoreOther, reverse);
}
override function set_frames(value:FlxFramesCollection):FlxFramesCollection
{
// DISABLE THIS SO WE DON'T DESTROY OUR HARD WORK
// WE WILL MAKE SURE TO LOAD THE PROPER SPRITESHEET BEFORE PLAYING AN ANIM
// if (animation != null)
// {
// animation.destroyAnimations();
// }
if (value != null)
{
graphic = value.parent;
this.frames = value;
this.frame = value.getByIndex(0);
// this.numFrames = value.numFrames;
resetHelpers();
this.bakedRotationAngle = 0;
this.animation.frameIndex = 0;
graphicLoaded();
}
else
{
this.frames = null;
this.frame = null;
this.graphic = null;
}
return this.frames;
}
}

View file

@ -4,6 +4,7 @@ import funkin.data.song.SongData.SongNoteData;
import funkin.play.notes.notestyle.NoteStyle;
import flixel.graphics.frames.FlxAtlasFrames;
import flixel.FlxSprite;
import funkin.graphics.shaders.HSVShader;
class NoteSprite extends FlxSprite
{
@ -11,6 +12,8 @@ class NoteSprite extends FlxSprite
public var holdNoteSprite:SustainTrail;
var hsvShader:HSVShader;
/**
* The time at which the note should be hit, in milliseconds.
*/
@ -102,6 +105,8 @@ class NoteSprite extends FlxSprite
this.strumTime = strumTime;
this.direction = direction;
this.hsvShader = new HSVShader();
if (this.strumTime < 0) this.strumTime = 0;
setupNoteGraphic(noteStyle);
@ -116,16 +121,57 @@ class NoteSprite extends FlxSprite
setGraphicSize(Strumline.STRUMLINE_SIZE);
updateHitbox();
this.shader = hsvShader;
}
#if FLX_DEBUG
/**
* Call this to override how debug bounding boxes are drawn for this sprite.
*/
public override function drawDebugOnCamera(camera:flixel.FlxCamera):Void
{
if (!camera.visible || !camera.exists || !isOnScreen(camera)) return;
var gfx = beginDrawDebug(camera);
var rect = getBoundingBox(camera);
trace('note sprite bounding box: ' + rect.x + ', ' + rect.y + ', ' + rect.width + ', ' + rect.height);
gfx.lineStyle(2, 0xFFFF66FF, 0.5); // thickness, color, alpha
gfx.drawRect(rect.x, rect.y, rect.width, rect.height);
gfx.lineStyle(2, 0xFFFFFF66, 0.5); // thickness, color, alpha
gfx.drawRect(rect.x, rect.y + rect.height / 2, rect.width, 1);
endDrawDebug(camera);
}
#end
public function desaturate():Void
{
this.hsvShader.saturation = 0.2;
}
public function setHue(hue:Float):Void
{
this.hsvShader.hue = hue;
}
public override function revive():Void
{
super.revive();
this.visible = true;
this.alpha = 1.0;
this.active = false;
this.tooEarly = false;
this.hasBeenHit = false;
this.mayHit = false;
this.hasMissed = false;
this.hsvShader.hue = 1.0;
this.hsvShader.saturation = 1.0;
this.hsvShader.value = 1.0;
}
public override function kill():Void

View file

@ -397,7 +397,7 @@ class Strumline extends FlxSpriteGroup
// Update rendering of notes.
for (note in notes.members)
{
if (note == null || !note.alive || note.hasBeenHit) continue;
if (note == null || !note.alive) continue;
var vwoosh:Bool = note.holdNoteSprite == null;
// Set the note's position.
@ -424,7 +424,7 @@ class Strumline extends FlxSpriteGroup
playStatic(holdNote.noteDirection);
holdNote.missedNote = true;
holdNote.visible = true;
holdNote.alpha = 0.0;
holdNote.alpha = 0.0; // Completely hide the dropped hold note.
}
}
@ -465,10 +465,6 @@ class Strumline extends FlxSpriteGroup
var yOffset:Float = (holdNote.fullSustainLength - holdNote.sustainLength) * Constants.PIXELS_PER_MS;
trace('yOffset: ' + yOffset);
trace('holdNote.fullSustainLength: ' + holdNote.fullSustainLength);
trace('holdNote.sustainLength: ' + holdNote.sustainLength);
var vwoosh:Bool = false;
if (Preferences.downscroll)
@ -612,11 +608,24 @@ class Strumline extends FlxSpriteGroup
this.noteData.insertionSort(compareNoteData.bind(FlxSort.ASCENDING));
}
public function hitNote(note:NoteSprite):Void
/**
* @param note The note to hit.
* @param removeNote True to remove the note immediately, false to make it transparent and let it move offscreen.
*/
public function hitNote(note:NoteSprite, removeNote:Bool = true):Void
{
playConfirm(note.direction);
note.hasBeenHit = true;
killNote(note);
if (removeNote)
{
killNote(note);
}
else
{
note.alpha = 0.5;
note.desaturate();
}
if (note.holdNoteSprite != null)
{

View file

@ -47,6 +47,11 @@ class SustainTrail extends FlxSprite
*/
public var missedNote:Bool = false;
/**
* Set to `true` after handling additional logic for missing notes.
*/
public var handledMiss:Bool = false;
// maybe BlendMode.MULTIPLY if missed somehow, drawTriangles does not support!
/**
@ -82,6 +87,9 @@ class SustainTrail extends FlxSprite
public var isPixel:Bool;
var graphicWidth:Float = 0;
var graphicHeight:Float = 0;
/**
* Normally you would take strumTime:Float, noteData:Int, sustainLength:Float, parentNote:Note (?)
* @param NoteData
@ -110,8 +118,8 @@ class SustainTrail extends FlxSprite
zoom *= 0.7;
// CALCULATE SIZE
width = graphic.width / 8 * zoom; // amount of notes * 2
height = sustainHeight(sustainLength, getScrollSpeed());
graphicWidth = graphic.width / 8 * zoom; // amount of notes * 2
graphicHeight = sustainHeight(sustainLength, getScrollSpeed());
// instead of scrollSpeed, PlayState.SONG.speed
flipY = Preferences.downscroll;
@ -148,12 +156,21 @@ class SustainTrail extends FlxSprite
if (sustainLength == s) return s;
height = sustainHeight(s, getScrollSpeed());
graphicHeight = sustainHeight(s, getScrollSpeed());
this.sustainLength = s;
updateClipping();
updateHitbox();
return this.sustainLength;
}
public override function updateHitbox():Void
{
width = graphicWidth;
height = graphicHeight;
offset.set(0, 0);
origin.set(width * 0.5, height * 0.5);
}
/**
* Sets up new vertex and UV data to clip the trail.
* If flipY is true, top and bottom bounds swap places.
@ -161,7 +178,7 @@ class SustainTrail extends FlxSprite
*/
public function updateClipping(songTime:Float = 0):Void
{
var clipHeight:Float = FlxMath.bound(sustainHeight(sustainLength - (songTime - strumTime), getScrollSpeed()), 0, height);
var clipHeight:Float = FlxMath.bound(sustainHeight(sustainLength - (songTime - strumTime), getScrollSpeed()), 0, graphicHeight);
if (clipHeight <= 0.1)
{
visible = false;
@ -178,10 +195,10 @@ class SustainTrail extends FlxSprite
// ===HOLD VERTICES==
// Top left
vertices[0 * 2] = 0.0; // Inline with left side
vertices[0 * 2 + 1] = flipY ? clipHeight : height - clipHeight;
vertices[0 * 2 + 1] = flipY ? clipHeight : graphicHeight - clipHeight;
// Top right
vertices[1 * 2] = width;
vertices[1 * 2] = graphicWidth;
vertices[1 * 2 + 1] = vertices[0 * 2 + 1]; // Inline with top left vertex
// Bottom left
@ -197,7 +214,7 @@ class SustainTrail extends FlxSprite
}
// Bottom right
vertices[3 * 2] = width;
vertices[3 * 2] = graphicWidth;
vertices[3 * 2 + 1] = vertices[2 * 2 + 1]; // Inline with bottom left vertex
// ===HOLD UVs===
@ -233,7 +250,7 @@ class SustainTrail extends FlxSprite
// Bottom left
vertices[6 * 2] = vertices[2 * 2]; // Inline with left side
vertices[6 * 2 + 1] = flipY ? (graphic.height * (-bottomClip + endOffset) * zoom) : (height + graphic.height * (bottomClip - endOffset) * zoom);
vertices[6 * 2 + 1] = flipY ? (graphic.height * (-bottomClip + endOffset) * zoom) : (graphicHeight + graphic.height * (bottomClip - endOffset) * zoom);
// Bottom right
vertices[7 * 2] = vertices[3 * 2]; // Inline with right side
@ -277,6 +294,10 @@ class SustainTrail extends FlxSprite
getScreenPosition(_point, camera).subtractPoint(offset);
camera.drawTriangles(processedGraphic, vertices, indices, uvtData, null, _point, blend, true, antialiasing);
}
#if FLX_DEBUG
if (FlxG.debugger.drawDebug) drawDebug();
#end
}
public override function kill():Void
@ -305,6 +326,7 @@ class SustainTrail extends FlxSprite
hitNote = false;
missedNote = false;
handledMiss = false;
}
override public function destroy():Void

View file

@ -158,8 +158,8 @@ class Scoring
return switch (absTiming)
{
case(_ < PBOT1_KILLER_THRESHOLD) => true:
'killer';
// case(_ < PBOT1_KILLER_THRESHOLD) => true:
// 'killer';
case(_ < PBOT1_SICK_THRESHOLD) => true:
'sick';
case(_ < PBOT1_GOOD_THRESHOLD) => true:

View file

@ -174,7 +174,10 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
difficulty.timeChanges = metadata.timeChanges;
difficulty.looped = metadata.looped;
difficulty.generatedBy = metadata.generatedBy;
difficulty.offsets = metadata.offsets;
difficulty.offsets = metadata?.offsets ?? new SongOffsets();
difficulty.difficultyRating = metadata.playData.ratings.get(diffId) ?? 0;
difficulty.album = metadata.playData.album;
difficulty.stage = metadata.playData.stage;
difficulty.noteStyle = metadata.playData.noteStyle;
@ -232,6 +235,7 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
difficulty.timeChanges = metadata.timeChanges;
difficulty.looped = metadata.looped;
difficulty.generatedBy = metadata.generatedBy;
difficulty.offsets = metadata?.offsets ?? new SongOffsets();
difficulty.stage = metadata.playData.stage;
difficulty.noteStyle = metadata.playData.noteStyle;
@ -405,6 +409,9 @@ class SongDifficulty
public var scrollSpeed:Float = Constants.DEFAULT_SCROLLSPEED;
public var difficultyRating:Int = 0;
public var album:Null<String> = null;
public function new(song:Song, diffId:String, variation:String)
{
this.song = song;

View file

@ -5,13 +5,16 @@ import flixel.group.FlxSpriteGroup;
import flixel.math.FlxPoint;
import flixel.system.FlxAssets.FlxShader;
import flixel.util.FlxSort;
import flixel.util.FlxColor;
import funkin.modding.IScriptedClass;
import funkin.modding.events.ScriptEvent;
import funkin.modding.events.ScriptEventType;
import funkin.modding.events.ScriptEventDispatcher;
import funkin.play.character.BaseCharacter;
import funkin.play.stage.StageData.StageDataCharacter;
import funkin.play.stage.StageData.StageDataParser;
import funkin.data.IRegistryEntry;
import funkin.data.stage.StageData;
import funkin.data.stage.StageData.StageDataCharacter;
import funkin.data.stage.StageRegistry;
import funkin.play.stage.StageProp;
import funkin.util.SortUtil;
import funkin.util.assets.FlxAnimationUtil;
@ -23,14 +26,25 @@ typedef StagePropGroup = FlxTypedSpriteGroup<StageProp>;
*
* A Stage is comprised of one or more props, each of which is a FlxSprite.
*/
class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass
class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements IRegistryEntry<StageData>
{
public final stageId:String;
public final stageName:String;
public final id:String;
final _data:StageData;
public final _data:StageData;
public var camZoom:Float = 1.0;
public var stageName(get, never):String;
function get_stageName():String
{
return _data?.name ?? 'Unknown';
}
public var camZoom(get, never):Float;
function get_camZoom():Float
{
return _data?.cameraZoom ?? 1.0;
}
var namedProps:Map<String, StageProp> = new Map<String, StageProp>();
var characters:Map<String, BaseCharacter> = new Map<String, BaseCharacter>();
@ -41,21 +55,18 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass
* They're used to cache the data needed to build the stage,
* then accessed and fleshed out when the stage needs to be built.
*
* @param stageId
* @param id
*/
public function new(stageId:String)
public function new(id:String)
{
super();
this.stageId = stageId;
_data = StageDataParser.parseStageData(this.stageId);
this.id = id;
_data = _fetchData(id);
if (_data == null)
{
throw 'Could not find stage data for stageId: $stageId';
}
else
{
this.stageName = _data.name;
throw 'Could not find stage data for stage id: $id';
}
}
@ -129,9 +140,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass
*/
function buildStage():Void
{
trace('Building stage for display: ${this.stageId}');
this.camZoom = _data.cameraZoom;
trace('Building stage for display: ${this.id}');
this.debugIconGroup = new FlxSpriteGroup();
@ -139,6 +148,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass
{
trace(' Placing prop: ${dataProp.name} (${dataProp.assetPath})');
var isSolidColor = dataProp.assetPath.startsWith('#');
var isAnimated = dataProp.animations.length > 0;
var propSprite:StageProp;
@ -162,6 +172,22 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass
propSprite.frames = Paths.getSparrowAtlas(dataProp.assetPath);
}
}
else if (isSolidColor)
{
var width:Int = 1;
var height:Int = 1;
switch (dataProp.scale)
{
case Left(value):
width = Std.int(value);
height = Std.int(value);
case Right(values):
width = Std.int(values[0]);
height = Std.int(values[1]);
}
propSprite.makeSolidColor(width, height, FlxColor.fromString(dataProp.assetPath));
}
else
{
// Initalize static sprite.
@ -177,13 +203,16 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass
continue;
}
switch (dataProp.scale)
if (!isSolidColor)
{
case Left(value):
propSprite.scale.set(value);
switch (dataProp.scale)
{
case Left(value):
propSprite.scale.set(value);
case Right(values):
propSprite.scale.set(values[0], values[1]);
case Right(values):
propSprite.scale.set(values[0], values[1]);
}
}
propSprite.updateHitbox();
@ -195,15 +224,8 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass
// If pixel, disable antialiasing.
propSprite.antialiasing = !dataProp.isPixel;
switch (dataProp.scroll)
{
case Left(value):
propSprite.scrollFactor.x = value;
propSprite.scrollFactor.y = value;
case Right(values):
propSprite.scrollFactor.x = values[0];
propSprite.scrollFactor.y = values[1];
}
propSprite.scrollFactor.x = dataProp.scroll[0];
propSprite.scrollFactor.y = dataProp.scroll[1];
propSprite.zIndex = dataProp.zIndex;
@ -731,6 +753,11 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass
return Sprite;
}
static function _fetchData(id:String):Null<StageData>
{
return StageRegistry.instance.parseEntryDataWithMigration(id, StageRegistry.instance.fetchEntryVersion(id));
}
public function onScriptEvent(event:ScriptEvent) {}
public function onPause(event:PauseScriptEvent) {}

View file

@ -1,548 +0,0 @@
package funkin.play.stage;
import funkin.data.animation.AnimationData;
import funkin.play.stage.ScriptedStage;
import funkin.play.stage.Stage;
import funkin.util.VersionUtil;
import funkin.util.assets.DataAssets;
import haxe.Json;
import openfl.Assets;
/**
* Contains utilities for loading and parsing stage data.
*/
class StageDataParser
{
/**
* 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 STAGE_DATA_VERSION:String = "1.0.0";
/**
* The current version rule check for the stage data format.
*/
public static final STAGE_DATA_VERSION_RULE:String = "1.0.x";
static final stageCache:Map<String, Stage> = new Map<String, Stage>();
static final DEFAULT_STAGE_ID = '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 loadStageCache():Void
{
// Clear any stages that are cached if there were any.
clearStageCache();
trace("Loading stage cache...");
//
// SCRIPTED STAGES
//
var scriptedStageClassNames:Array<String> = ScriptedStage.listScriptClasses();
trace(' Instantiating ${scriptedStageClassNames.length} scripted stages...');
for (stageCls in scriptedStageClassNames)
{
var stage:Stage = ScriptedStage.init(stageCls, DEFAULT_STAGE_ID);
if (stage != null)
{
trace(' Loaded scripted stage: ${stage.stageName}');
// Disable the rendering logic for stage until it's loaded.
// Note that kill() =/= destroy()
stage.kill();
// Then store it.
stageCache.set(stage.stageId, stage);
}
else
{
trace(' Failed to instantiate scripted stage class: ${stageCls}');
}
}
//
// UNSCRIPTED STAGES
//
var stageIdList:Array<String> = DataAssets.listDataFilesInPath('stages/');
var unscriptedStageIds:Array<String> = stageIdList.filter(function(stageId:String):Bool {
return !stageCache.exists(stageId);
});
trace(' Instantiating ${unscriptedStageIds.length} non-scripted stages...');
for (stageId in unscriptedStageIds)
{
var stage:Stage;
try
{
stage = new Stage(stageId);
if (stage != null)
{
trace(' Loaded stage data: ${stage.stageName}');
stageCache.set(stageId, stage);
}
}
catch (e)
{
trace(' An error occurred while loading stage data: ${stageId}');
// Assume error was already logged.
continue;
}
}
trace(' Successfully loaded ${Lambda.count(stageCache)} stages.');
}
public static function fetchStage(stageId:String):Null<Stage>
{
if (stageCache.exists(stageId))
{
trace('Successfully fetch stage: ${stageId}');
var stage:Stage = stageCache.get(stageId);
stage.revive();
return stage;
}
else
{
trace('Failed to fetch stage, not found in cache: ${stageId}');
return null;
}
}
static function clearStageCache():Void
{
if (stageCache != null)
{
for (stage in stageCache)
{
stage.destroy();
}
stageCache.clear();
}
}
/**
* Load a stage's JSON file, parse its data, and return it.
*
* @param stageId The stage to load.
* @return The stage data, or null if validation failed.
*/
public static function parseStageData(stageId:String):Null<StageData>
{
var rawJson:String = loadStageFile(stageId);
var stageData:StageData = migrateStageData(rawJson, stageId);
return validateStageData(stageId, stageData);
}
public static function listStageIds():Array<String>
{
return stageCache.keys().array();
}
static function loadStageFile(stagePath:String):String
{
var stageFilePath:String = Paths.json('stages/${stagePath}');
var rawJson = Assets.getText(stageFilePath).trim();
while (!rawJson.endsWith("}"))
{
rawJson = rawJson.substr(0, rawJson.length - 1);
}
return rawJson;
}
static function migrateStageData(rawJson:String, stageId:String):Null<StageData>
{
// If you update the stage data format in a breaking way,
// handle migration here by checking the `version` value.
try
{
var parser = new json2object.JsonParser<StageData>();
parser.ignoreUnknownVariables = false;
parser.fromJson(rawJson, '$stageId.json');
if (parser.errors.length > 0)
{
trace('[STAGE] Failed to parse stage data');
for (error in parser.errors)
funkin.data.DataError.printError(error);
return null;
}
return parser.value;
}
catch (e)
{
trace(' Error parsing data for stage: ${stageId}');
trace(' ${e}');
return null;
}
}
static final DEFAULT_ANIMTYPE:String = "sparrow";
static final DEFAULT_CAMERAZOOM:Float = 1.0;
static final DEFAULT_DANCEEVERY:Int = 0;
static final DEFAULT_ISPIXEL:Bool = false;
static final DEFAULT_NAME:String = "Untitled Stage";
static final DEFAULT_OFFSETS:Array<Float> = [0, 0];
static final DEFAULT_CAMERA_OFFSETS_BF:Array<Float> = [-100, -100];
static final DEFAULT_CAMERA_OFFSETS_DAD:Array<Float> = [150, -100];
static final DEFAULT_POSITION:Array<Float> = [0, 0];
static final DEFAULT_SCALE:Float = 1.0;
static final DEFAULT_ALPHA:Float = 1.0;
static final DEFAULT_SCROLL:Array<Float> = [0, 0];
static final DEFAULT_ZINDEX:Int = 0;
static final DEFAULT_CHARACTER_DATA:StageDataCharacter =
{
zIndex: DEFAULT_ZINDEX,
position: DEFAULT_POSITION,
cameraOffsets: DEFAULT_OFFSETS,
}
/**
* Set unspecified parameters to their defaults.
* If the parameter is mandatory, print an error message.
* @param id
* @param input
* @return The validated stage data
*/
static function validateStageData(id:String, input:StageData):Null<StageData>
{
if (input == null)
{
trace('ERROR: Could not parse stage data for "${id}".');
return null;
}
if (input.version == null)
{
trace('ERROR: Could not load stage data for "$id": missing version');
return null;
}
if (!VersionUtil.validateVersionStr(input.version, STAGE_DATA_VERSION_RULE))
{
trace('ERROR: Could not load stage data for "$id": bad version (got ${input.version}, expected ${STAGE_DATA_VERSION_RULE})');
return null;
}
if (input.name == null)
{
trace('WARN: Stage data for "$id" missing name');
input.name = DEFAULT_NAME;
}
if (input.cameraZoom == null)
{
input.cameraZoom = DEFAULT_CAMERAZOOM;
}
if (input.props == null)
{
input.props = [];
}
for (inputProp in input.props)
{
// It's fine for inputProp.name to be null
if (inputProp.assetPath == null)
{
trace('ERROR: Could not load stage data for "$id": missing assetPath for prop "${inputProp.name}"');
return null;
}
if (inputProp.position == null)
{
inputProp.position = DEFAULT_POSITION;
}
if (inputProp.zIndex == null)
{
inputProp.zIndex = DEFAULT_ZINDEX;
}
if (inputProp.isPixel == null)
{
inputProp.isPixel = DEFAULT_ISPIXEL;
}
if (inputProp.danceEvery == null)
{
inputProp.danceEvery = DEFAULT_DANCEEVERY;
}
if (inputProp.animType == null)
{
inputProp.animType = DEFAULT_ANIMTYPE;
}
switch (inputProp.scale)
{
case null:
inputProp.scale = Right([DEFAULT_SCALE, DEFAULT_SCALE]);
case Left(value):
inputProp.scale = Right([value, value]);
case Right(_):
// Do nothing
}
switch (inputProp.scroll)
{
case null:
inputProp.scroll = Right(DEFAULT_SCROLL);
case Left(value):
inputProp.scroll = Right([value, value]);
case Right(_):
// Do nothing
}
if (inputProp.alpha == null)
{
inputProp.alpha = DEFAULT_ALPHA;
}
if (inputProp.animations == null)
{
inputProp.animations = [];
}
if (inputProp.animations.length == 0 && inputProp.startingAnimation != null)
{
trace('ERROR: Could not load stage data for "$id": missing animations for prop "${inputProp.name}"');
return null;
}
for (inputAnimation in inputProp.animations)
{
if (inputAnimation.name == null)
{
trace('ERROR: Could not load stage data for "$id": missing animation name for prop "${inputProp.name}"');
return null;
}
if (inputAnimation.frameRate == null)
{
inputAnimation.frameRate = 24;
}
if (inputAnimation.offsets == null)
{
inputAnimation.offsets = DEFAULT_OFFSETS;
}
if (inputAnimation.looped == null)
{
inputAnimation.looped = true;
}
if (inputAnimation.flipX == null)
{
inputAnimation.flipX = false;
}
if (inputAnimation.flipY == null)
{
inputAnimation.flipY = false;
}
}
}
if (input.characters == null)
{
trace('ERROR: Could not load stage data for "$id": missing characters');
return null;
}
if (input.characters.bf == null)
{
input.characters.bf = DEFAULT_CHARACTER_DATA;
}
if (input.characters.dad == null)
{
input.characters.dad = DEFAULT_CHARACTER_DATA;
}
if (input.characters.gf == null)
{
input.characters.gf = DEFAULT_CHARACTER_DATA;
}
for (inputCharacter in [input.characters.bf, input.characters.dad, input.characters.gf])
{
if (inputCharacter.position == null || inputCharacter.position.length != 2)
{
inputCharacter.position = [0, 0];
}
}
// All good!
return input;
}
}
class StageData
{
/**
* The sematic version number of the stage data JSON format.
* Supports fancy comparisons like NPM does it's neat.
*/
public var version:String;
public var name:String;
public var cameraZoom:Null<Float>;
public var props:Array<StageDataProp>;
public var characters:StageDataCharacters;
public function new()
{
this.version = StageDataParser.STAGE_DATA_VERSION;
}
/**
* Convert this StageData into a JSON string.
*/
public function serialize(pretty:Bool = true):String
{
var writer = new json2object.JsonWriter<StageData>();
return writer.write(this, pretty ? ' ' : null);
}
}
typedef StageDataCharacters =
{
var bf:StageDataCharacter;
var dad:StageDataCharacter;
var gf:StageDataCharacter;
};
typedef StageDataProp =
{
/**
* The name of the prop for later lookup by scripts.
* Optional; if unspecified, the prop can't be referenced by scripts.
*/
@:optional
var name:String;
/**
* The asset used to display the prop.
*/
var assetPath:String;
/**
* The position of the prop as an [x, y] array of two floats.
*/
var position:Array<Float>;
/**
* A number determining the stack order of the prop, relative to other props and the characters in the stage.
* Props with lower numbers render below those with higher numbers.
* This is just like CSS, it isn't hard.
* @default 0
*/
@:optional
@:default(0)
var zIndex:Int;
/**
* If set to true, anti-aliasing will be forcibly disabled on the sprite.
* This prevents blurry images on pixel-art levels.
* @default false
*/
@:optional
@:default(false)
var isPixel:Bool;
/**
* Either the scale of the prop as a float, or the [w, h] scale as an array of two floats.
* Pro tip: On pixel-art levels, save the sprite small and set this value to 6 or so to save memory.
*/
@:jcustomparse(funkin.data.DataParse.eitherFloatOrFloats)
@:jcustomwrite(funkin.data.DataWrite.eitherFloatOrFloats)
@:optional
var scale:haxe.ds.Either<Float, Array<Float>>;
/**
* The alpha of the prop, as a float.
* @default 1.0
*/
@:optional
@:default(1.0)
var alpha:Float;
/**
* If not zero, this prop will play an animation every X beats of the song.
* This requires animations to be defined. If `danceLeft` and `danceRight` are defined,
* they will alternated between, otherwise the `idle` animation will be used.
*
* @default 0
*/
@:default(0)
@:optional
var danceEvery:Int;
/**
* How much the prop scrolls relative to the camera. Used to create a parallax effect.
* Represented as a float or as an [x, y] array of two floats.
* [1, 1] means the prop moves 1:1 with the camera.
* [0.5, 0.5] means the prop half as much as the camera.
* [0, 0] means the prop is not moved.
* @default [0, 0]
*/
@:jcustomparse(funkin.data.DataParse.eitherFloatOrFloats)
@:jcustomwrite(funkin.data.DataWrite.eitherFloatOrFloats)
@:optional
var scroll:haxe.ds.Either<Float, Array<Float>>;
/**
* An optional array of animations which the prop can play.
* @default Prop has no animations.
*/
@:optional
var animations:Array<AnimationData>;
/**
* If animations are used, this is the name of the animation to play first.
* @default Don't play an animation.
*/
@:optional
var startingAnimation:Null<String>;
/**
* The animation type to use.
* Options: "sparrow", "packer"
* @default "sparrow"
*/
@:default("sparrow")
@:optional
var animType:String;
};
typedef StageDataCharacter =
{
/**
* A number determining the stack order of the character, relative to props and other characters in the stage.
* Again, just like CSS.
* @default 0
*/
var zIndex:Int;
/**
* The position to render the character at.
*/
var position:Array<Float>;
/**
* The camera offsets to apply when focusing on the character on this stage.
* @default [-100, -100] for BF, [100, -100] for DAD/OPPONENT, [0, 0] for GF
*/
var cameraOffsets:Array<Float>;
};

View file

@ -1,10 +1,10 @@
package funkin.play.stage;
import funkin.modding.events.ScriptEvent;
import flixel.FlxSprite;
import funkin.graphics.FunkinSprite;
import funkin.modding.IScriptedClass.IStateStageProp;
class StageProp extends FlxSprite implements IStateStageProp
class StageProp extends FunkinSprite implements IStateStageProp
{
/**
* An internal name for this prop.

View file

@ -14,7 +14,7 @@ import thx.semver.Version;
@:forward(volume, mute)
abstract Save(RawSaveData)
{
// Version 2.0.1 adds attributes to `optionsChartEditor`, that should return default values if they are null.
// Version 2.0.2 adds attributes to `optionsChartEditor`, that should return default values if they are null.
public static final SAVE_DATA_VERSION:thx.semver.Version = "2.0.2";
public static final SAVE_DATA_VERSION_RULE:thx.semver.VersionRule = "2.0.x";
@ -110,9 +110,7 @@ abstract Save(RawSaveData)
metronomeVolume: 1.0,
hitsoundsEnabledPlayer: true,
hitsoundsEnabledOpponent: true,
instVolume: 1.0,
voicesVolume: 1.0,
playbackSpeed: 1.0,
themeMusic: true
},
};
}
@ -349,38 +347,21 @@ abstract Save(RawSaveData)
return this.optionsChartEditor.hitsoundsEnabledOpponent;
}
public var chartEditorInstVolume(get, set):Float;
public var chartEditorThemeMusic(get, set):Bool;
function get_chartEditorInstVolume():Float
function get_chartEditorThemeMusic():Bool
{
if (this.optionsChartEditor.instVolume == null) this.optionsChartEditor.instVolume = 1.0;
if (this.optionsChartEditor.themeMusic == null) this.optionsChartEditor.themeMusic = true;
return this.optionsChartEditor.instVolume;
return this.optionsChartEditor.themeMusic;
}
function set_chartEditorInstVolume(value:Float):Float
function set_chartEditorThemeMusic(value:Bool):Bool
{
// Set and apply.
this.optionsChartEditor.instVolume = value;
this.optionsChartEditor.themeMusic = value;
flush();
return this.optionsChartEditor.instVolume;
}
public var chartEditorVoicesVolume(get, set):Float;
function get_chartEditorVoicesVolume():Float
{
if (this.optionsChartEditor.voicesVolume == null) this.optionsChartEditor.voicesVolume = 1.0;
return this.optionsChartEditor.voicesVolume;
}
function set_chartEditorVoicesVolume(value:Float):Float
{
// Set and apply.
this.optionsChartEditor.voicesVolume = value;
flush();
return this.optionsChartEditor.voicesVolume;
return this.optionsChartEditor.themeMusic;
}
public var chartEditorPlaybackSpeed(get, set):Float;
@ -776,7 +757,6 @@ typedef SaveScoreData =
typedef SaveScoreTallyData =
{
var killer:Int;
var sick:Int;
var good:Int;
var bad:Int;
@ -1041,6 +1021,12 @@ typedef SaveDataChartEditorOptions =
*/
var ?hitsoundsEnabledOpponent:Bool;
/**
* Theme music in the Chart Editor.
* @default `true`
*/
var ?themeMusic:Bool;
/**
* Instrumental volume in the Chart Editor.
* @default `1.0`

View file

@ -120,7 +120,6 @@ class SaveDataMigrator
accuracy: inputSaveData.songCompletion.get('${levelId}-easy') ?? 0.0,
tallies:
{
killer: 0,
sick: 0,
good: 0,
bad: 0,
@ -140,7 +139,6 @@ class SaveDataMigrator
accuracy: inputSaveData.songCompletion.get('${levelId}') ?? 0.0,
tallies:
{
killer: 0,
sick: 0,
good: 0,
bad: 0,
@ -160,7 +158,6 @@ class SaveDataMigrator
accuracy: inputSaveData.songCompletion.get('${levelId}-hard') ?? 0.0,
tallies:
{
killer: 0,
sick: 0,
good: 0,
bad: 0,
@ -183,7 +180,6 @@ class SaveDataMigrator
accuracy: 0,
tallies:
{
killer: 0,
sick: 0,
good: 0,
bad: 0,
@ -209,7 +205,6 @@ class SaveDataMigrator
accuracy: 0,
tallies:
{
killer: 0,
sick: 0,
good: 0,
bad: 0,
@ -235,7 +230,6 @@ class SaveDataMigrator
accuracy: 0,
tallies:
{
killer: 0,
sick: 0,
good: 0,
bad: 0,

View file

@ -274,4 +274,5 @@ enum abstract AtlasFont(String) from String to String
{
var DEFAULT = "default";
var BOLD = "bold";
var FREEPLAY_CLEAR = "freeplay-clear";
}

File diff suppressed because it is too large Load diff

View file

@ -59,6 +59,12 @@ class AddEventsCommand implements ChartEditorCommand
state.sortChartData();
}
public function shouldAddToHistory(state:ChartEditorState):Bool
{
// This command is undoable. Add to the history if we actually performed an action.
return (events.length > 0);
}
public function toString():String
{
var len:Int = events.length;

View file

@ -59,6 +59,12 @@ class AddNotesCommand implements ChartEditorCommand
state.sortChartData();
}
public function shouldAddToHistory(state:ChartEditorState):Bool
{
// This command is undoable. Add to the history if we actually performed an action.
return (notes.length > 0);
}
public function toString():String
{
if (notes.length == 1)

View file

@ -64,6 +64,12 @@ class ChangeStartingBPMCommand implements ChartEditorCommand
Conductor.instance.mapTimeChanges(state.currentSongMetadata.timeChanges);
}
public function shouldAddToHistory(state:ChartEditorState):Bool
{
// This command is undoable. Add to the history if we actually performed an action.
return (targetBPM != previousBPM);
}
public function toString():String
{
return 'Change Starting BPM to ${targetBPM}';

View file

@ -6,6 +6,8 @@ package funkin.ui.debug.charting.commands;
*
* To make a functionality compatible with the undo/redo history, create a new class
* that implements ChartEditorCommand, then call `ChartEditorState.performCommand(new Command())`
*
* NOTE: Make the constructor very simple, as it may be called without executing by the command palette.
*/
interface ChartEditorCommand
{
@ -22,6 +24,15 @@ interface ChartEditorCommand
*/
public function undo(state:ChartEditorState):Void;
/**
* Return whether or not this command should be appended to the in the undo/redo history.
* Generally this should be true, it should only be false if the command is minor and non-destructive,
* like copying to the clipboard.
*
* Called after `execute()` is performed.
*/
public function shouldAddToHistory(state:ChartEditorState):Bool;
/**
* Get a short description of the action (for the UI).
* For example, return `Add Left Note` to display `Undo Add Left Note` in the menu.

View file

@ -0,0 +1,144 @@
package funkin.ui.debug.charting.commands;
import funkin.data.song.SongData.SongNoteData;
import funkin.data.song.SongData.SongEventData;
import funkin.data.song.SongDataUtils;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
/**
* Command that copies a given set of notes and song events to the clipboard,
* without deleting them from the chart editor.
*/
@:nullSafety
@:access(funkin.ui.debug.charting.ChartEditorState)
class CopyItemsCommand implements ChartEditorCommand
{
var notes:Array<SongNoteData>;
var events:Array<SongEventData>;
public function new(notes:Array<SongNoteData>, events:Array<SongEventData>)
{
this.notes = notes;
this.events = events;
}
public function execute(state:ChartEditorState):Void
{
// Calculate a single time offset for all the notes and events.
var timeOffset:Null<Int> = state.currentNoteSelection.length > 0 ? Std.int(state.currentNoteSelection[0].time) : null;
if (state.currentEventSelection.length > 0)
{
if (timeOffset == null || state.currentEventSelection[0].time < timeOffset)
{
timeOffset = Std.int(state.currentEventSelection[0].time);
}
}
SongDataUtils.writeItemsToClipboard(
{
notes: SongDataUtils.buildNoteClipboard(state.currentNoteSelection, timeOffset),
events: SongDataUtils.buildEventClipboard(state.currentEventSelection, timeOffset),
});
performVisuals(state);
}
function performVisuals(state:ChartEditorState):Void
{
if (state.currentNoteSelection.length > 0)
{
// Display the "Copied Notes" text.
if (state.txtCopyNotif != null)
{
state.txtCopyNotif.visible = true;
state.txtCopyNotif.text = "Copied " + state.currentNoteSelection.length + " notes to clipboard";
state.txtCopyNotif.x = FlxG.mouse.x - (state.txtCopyNotif.width / 2);
state.txtCopyNotif.y = FlxG.mouse.y - 16;
FlxTween.tween(state.txtCopyNotif, {y: state.txtCopyNotif.y - 32}, 0.5,
{
type: FlxTween.ONESHOT,
ease: FlxEase.quadOut,
onComplete: function(_) {
state.txtCopyNotif.visible = false;
}
});
}
// Wiggle the notes.
for (note in state.renderedNotes.members)
{
if (state.isNoteSelected(note.noteData))
{
FlxTween.globalManager.cancelTweensOf(note);
FlxTween.globalManager.cancelTweensOf(note.scale);
note.playNoteAnimation();
var prevX:Float = note.scale.x;
var prevY:Float = note.scale.y;
note.scale.x *= 1.2;
note.scale.y *= 1.2;
note.angle = FlxG.random.bool() ? -10 : 10;
FlxTween.tween(note, {"angle": 0}, 0.8, {ease: FlxEase.elasticOut});
FlxTween.tween(note.scale, {"y": prevX, "x": prevY}, 0.7,
{
ease: FlxEase.elasticOut,
onComplete: function(_) {
note.playNoteAnimation();
}
});
}
}
// Wiggle the events.
for (event in state.renderedEvents.members)
{
if (state.isEventSelected(event.eventData))
{
FlxTween.globalManager.cancelTweensOf(event);
FlxTween.globalManager.cancelTweensOf(event.scale);
event.playAnimation();
var prevX:Float = event.scale.x;
var prevY:Float = event.scale.y;
event.scale.x *= 1.2;
event.scale.y *= 1.2;
event.angle = FlxG.random.bool() ? -10 : 10;
FlxTween.tween(event, {"angle": 0}, 0.8, {ease: FlxEase.elasticOut});
FlxTween.tween(event.scale, {"y": prevX, "x": prevY}, 0.7,
{
ease: FlxEase.elasticOut,
onComplete: function(_) {
event.playAnimation();
}
});
}
}
}
}
public function undo(state:ChartEditorState):Void
{
// This command is not undoable. Do nothing.
}
public function shouldAddToHistory(state:ChartEditorState):Bool
{
// This command is not undoable. Don't add it to the history.
return false;
}
public function toString():String
{
var len:Int = notes.length + events.length;
if (notes.length == 0) return 'Copy $len Events to Clipboard';
else if (events.length == 0) return 'Copy $len Notes to Clipboard';
else
return 'Copy $len Items to Clipboard';
}
}

View file

@ -56,6 +56,12 @@ class CutItemsCommand implements ChartEditorCommand
state.sortChartData();
}
public function shouldAddToHistory(state:ChartEditorState):Bool
{
// This command is undoable. Always add it to the history.
return (notes.length > 0 || events.length > 0);
}
public function toString():String
{
var len:Int = notes.length + events.length;

View file

@ -10,17 +10,16 @@ import funkin.data.song.SongData.SongEventData;
@:access(funkin.ui.debug.charting.ChartEditorState)
class DeselectAllItemsCommand implements ChartEditorCommand
{
var previousNoteSelection:Array<SongNoteData>;
var previousEventSelection:Array<SongEventData>;
var previousNoteSelection:Array<SongNoteData> = [];
var previousEventSelection:Array<SongEventData> = [];
public function new(?previousNoteSelection:Array<SongNoteData>, ?previousEventSelection:Array<SongEventData>)
{
this.previousNoteSelection = previousNoteSelection == null ? [] : previousNoteSelection;
this.previousEventSelection = previousEventSelection == null ? [] : previousEventSelection;
}
public function new() {}
public function execute(state:ChartEditorState):Void
{
this.previousNoteSelection = state.currentNoteSelection;
this.previousEventSelection = state.currentEventSelection;
state.currentNoteSelection = [];
state.currentEventSelection = [];
@ -35,6 +34,12 @@ class DeselectAllItemsCommand implements ChartEditorCommand
state.noteDisplayDirty = true;
}
public function shouldAddToHistory(state:ChartEditorState):Bool
{
// This command is undoable. Add to the history if we actually performed an action.
return (previousNoteSelection.length > 0 || previousEventSelection.length > 0);
}
public function toString():String
{
return 'Deselect All Items';

View file

@ -45,16 +45,27 @@ class DeselectItemsCommand implements ChartEditorCommand
state.notePreviewDirty = true;
}
public function shouldAddToHistory(state:ChartEditorState):Bool
{
// This command is undoable. Add to the history if we actually performed an action.
return (notes.length > 0 || events.length > 0);
}
public function toString():String
{
var noteCount = notes.length + events.length;
var isPlural = (notes.length + events.length) > 1;
var notesOnly = (notes.length > 0 && events.length == 0);
var eventsOnly = (notes.length == 0 && events.length > 0);
if (noteCount == 1)
if (notesOnly)
{
var dir:String = notes[0].getDirectionName();
return 'Deselect $dir Items';
return 'Deselect ${notes.length} ${isPlural ? 'Notes' : 'Note'}';
}
else if (eventsOnly)
{
return 'Deselect ${events.length} ${isPlural ? 'Events' : 'Event'}';
}
return 'Deselect ${noteCount} Items';
return 'Deselect ${notes.length + events.length} Items';
}
}

View file

@ -13,17 +13,25 @@ class ExtendNoteLengthCommand implements ChartEditorCommand
var note:SongNoteData;
var oldLength:Float;
var newLength:Float;
var unit:Unit;
public function new(note:SongNoteData, newLength:Float)
public function new(note:SongNoteData, newLength:Float, unit:Unit = MILLISECONDS)
{
this.note = note;
this.oldLength = note.length;
this.newLength = newLength;
this.unit = unit;
}
public function execute(state:ChartEditorState):Void
{
note.length = newLength;
switch (unit)
{
case MILLISECONDS:
this.note.length = newLength;
case STEPS:
this.note.setStepLength(newLength);
}
state.saveDataDirty = true;
state.noteDisplayDirty = true;
@ -36,7 +44,8 @@ class ExtendNoteLengthCommand implements ChartEditorCommand
{
state.playSound(Paths.sound('chartingSounds/undo'));
note.length = oldLength;
// Always use milliseconds for undoing
this.note.length = oldLength;
state.saveDataDirty = true;
state.noteDisplayDirty = true;
@ -45,8 +54,31 @@ class ExtendNoteLengthCommand implements ChartEditorCommand
state.sortChartData();
}
public function shouldAddToHistory(state:ChartEditorState):Bool
{
// This command is undoable. Add to the history if we actually performed an action.
return (oldLength != newLength);
}
public function toString():String
{
return 'Extend Note Length';
if (oldLength == 0)
{
return 'Add Hold to Note';
}
else if (newLength == 0)
{
return 'Remove Hold from Note';
}
else
{
return 'Extend Hold Note Length';
}
}
}
enum Unit
{
MILLISECONDS;
STEPS;
}

View file

@ -51,6 +51,12 @@ class FlipNotesCommand implements ChartEditorCommand
state.sortChartData();
}
public function shouldAddToHistory(state:ChartEditorState):Bool
{
// This command is undoable. Add to the history if we actually performed an action.
return (notes.length > 0);
}
public function toString():String
{
var len:Int = notes.length;

View file

@ -12,19 +12,19 @@ import funkin.data.song.SongDataUtils;
@:access(funkin.ui.debug.charting.ChartEditorState)
class InvertSelectedItemsCommand implements ChartEditorCommand
{
var previousNoteSelection:Array<SongNoteData>;
var previousEventSelection:Array<SongEventData>;
var previousNoteSelection:Array<SongNoteData> = [];
var previousEventSelection:Array<SongEventData> = [];
public function new(?previousNoteSelection:Array<SongNoteData>, ?previousEventSelection:Array<SongEventData>)
{
this.previousNoteSelection = previousNoteSelection == null ? [] : previousNoteSelection;
this.previousEventSelection = previousEventSelection == null ? [] : previousEventSelection;
}
public function new() {}
public function execute(state:ChartEditorState):Void
{
this.previousNoteSelection = state.currentNoteSelection;
this.previousEventSelection = state.currentEventSelection;
state.currentNoteSelection = SongDataUtils.subtractNotes(state.currentSongChartNoteData, previousNoteSelection);
state.currentEventSelection = SongDataUtils.subtractEvents(state.currentSongChartEventData, previousEventSelection);
state.noteDisplayDirty = true;
}
@ -36,6 +36,12 @@ class InvertSelectedItemsCommand implements ChartEditorCommand
state.noteDisplayDirty = true;
}
public function shouldAddToHistory(state:ChartEditorState):Bool
{
// This command is undoable. Add to the history if we actually performed an action.
return (previousNoteSelection.length > 0 || previousEventSelection.length > 0);
}
public function toString():String
{
return 'Invert Selected Items';

View file

@ -65,6 +65,12 @@ class MoveEventsCommand implements ChartEditorCommand
state.sortChartData();
}
public function shouldAddToHistory(state:ChartEditorState):Bool
{
// This command is undoable. Add to the history if we actually performed an action.
return (events.length > 0);
}
public function toString():String
{
var len:Int = events.length;

View file

@ -88,6 +88,12 @@ class MoveItemsCommand implements ChartEditorCommand
state.sortChartData();
}
public function shouldAddToHistory(state:ChartEditorState):Bool
{
// This command is undoable. Add to the history if we actually performed an action.
return (notes.length > 0 || events.length > 0);
}
public function toString():String
{
var len:Int = notes.length + events.length;

View file

@ -67,6 +67,12 @@ class MoveNotesCommand implements ChartEditorCommand
state.sortChartData();
}
public function shouldAddToHistory(state:ChartEditorState):Bool
{
// This command is undoable. Add to the history if we actually performed an action.
return (notes.length > 0);
}
public function toString():String
{
var len:Int = notes.length;

View file

@ -71,6 +71,12 @@ class PasteItemsCommand implements ChartEditorCommand
state.sortChartData();
}
public function shouldAddToHistory(state:ChartEditorState):Bool
{
// This command is undoable. Add to the history if we actually performed an action.
return (addedNotes.length > 0 || addedEvents.length > 0);
}
public function toString():String
{
var currentClipboard:SongClipboardItems = SongDataUtils.readItemsFromClipboard();

View file

@ -48,6 +48,12 @@ class RemoveEventsCommand implements ChartEditorCommand
state.sortChartData();
}
public function shouldAddToHistory(state:ChartEditorState):Bool
{
// This command is undoable. Add to the history if we actually performed an action.
return (events.length > 0);
}
public function toString():String
{
if (events.length == 1 && events[0] != null)

View file

@ -62,6 +62,12 @@ class RemoveItemsCommand implements ChartEditorCommand
state.sortChartData();
}
public function shouldAddToHistory(state:ChartEditorState):Bool
{
// This command is undoable. Add to the history if we actually performed an action.
return (notes.length > 0 || events.length > 0);
}
public function toString():String
{
return 'Remove ${notes.length + events.length} Items';

View file

@ -50,6 +50,12 @@ class RemoveNotesCommand implements ChartEditorCommand
state.sortChartData();
}
public function shouldAddToHistory(state:ChartEditorState):Bool
{
// This command is undoable. Add to the history if we actually performed an action.
return (notes.length > 0);
}
public function toString():String
{
if (notes.length == 1 && notes[0] != null)

View file

@ -10,19 +10,25 @@ import funkin.data.song.SongData.SongEventData;
@:access(funkin.ui.debug.charting.ChartEditorState)
class SelectAllItemsCommand implements ChartEditorCommand
{
var previousNoteSelection:Array<SongNoteData>;
var previousEventSelection:Array<SongEventData>;
var shouldSelectNotes:Bool;
var shouldSelectEvents:Bool;
public function new(?previousNoteSelection:Array<SongNoteData>, ?previousEventSelection:Array<SongEventData>)
var previousNoteSelection:Array<SongNoteData> = [];
var previousEventSelection:Array<SongEventData> = [];
public function new(shouldSelectNotes:Bool, shouldSelectEvents:Bool)
{
this.previousNoteSelection = previousNoteSelection == null ? [] : previousNoteSelection;
this.previousEventSelection = previousEventSelection == null ? [] : previousEventSelection;
this.shouldSelectNotes = shouldSelectNotes;
this.shouldSelectEvents = shouldSelectEvents;
}
public function execute(state:ChartEditorState):Void
{
state.currentNoteSelection = state.currentSongChartNoteData;
state.currentEventSelection = state.currentSongChartEventData;
this.previousNoteSelection = state.currentNoteSelection;
this.previousEventSelection = state.currentEventSelection;
state.currentNoteSelection = shouldSelectNotes ? state.currentSongChartNoteData : [];
state.currentEventSelection = shouldSelectEvents ? state.currentSongChartEventData : [];
state.noteDisplayDirty = true;
}
@ -35,8 +41,29 @@ class SelectAllItemsCommand implements ChartEditorCommand
state.noteDisplayDirty = true;
}
public function shouldAddToHistory(state:ChartEditorState):Bool
{
// This command is undoable. Add to the history if we actually performed an action.
return (state.currentNoteSelection.length > 0 || state.currentEventSelection.length > 0);
}
public function toString():String
{
return 'Select All Items';
if (shouldSelectNotes && !shouldSelectEvents)
{
return 'Select All Notes';
}
else if (shouldSelectEvents && !shouldSelectNotes)
{
return 'Select All Events';
}
else if (shouldSelectNotes && shouldSelectEvents)
{
return 'Select All Notes and Events';
}
else
{
return 'Select Nothing (Huh?)';
}
}
}

View file

@ -15,10 +15,10 @@ class SelectItemsCommand implements ChartEditorCommand
var notes:Array<SongNoteData>;
var events:Array<SongEventData>;
public function new(notes:Array<SongNoteData>, events:Array<SongEventData>)
public function new(?notes:Array<SongNoteData>, ?events:Array<SongEventData>)
{
this.notes = notes;
this.events = events;
this.notes = notes ?? [];
this.events = events ?? [];
}
public function execute(state:ChartEditorState):Void
@ -72,6 +72,12 @@ class SelectItemsCommand implements ChartEditorCommand
state.notePreviewDirty = true;
}
public function shouldAddToHistory(state:ChartEditorState):Bool
{
// This command is undoable. Add to the history if we actually performed an action.
return (notes.length > 0 || events.length > 0);
}
public function toString():String
{
var len:Int = notes.length + events.length;

View file

@ -13,20 +13,20 @@ class SetItemSelectionCommand implements ChartEditorCommand
{
var notes:Array<SongNoteData>;
var events:Array<SongEventData>;
var previousNoteSelection:Array<SongNoteData>;
var previousEventSelection:Array<SongEventData>;
var previousNoteSelection:Array<SongNoteData> = [];
var previousEventSelection:Array<SongEventData> = [];
public function new(notes:Array<SongNoteData>, events:Array<SongEventData>, previousNoteSelection:Array<SongNoteData>,
previousEventSelection:Array<SongEventData>)
public function new(notes:Array<SongNoteData>, events:Array<SongEventData>)
{
this.notes = notes;
this.events = events;
this.previousNoteSelection = previousNoteSelection == null ? [] : previousNoteSelection;
this.previousEventSelection = previousEventSelection == null ? [] : previousEventSelection;
}
public function execute(state:ChartEditorState):Void
{
this.previousNoteSelection = state.currentNoteSelection;
this.previousEventSelection = state.currentEventSelection;
state.currentNoteSelection = notes;
state.currentEventSelection = events;
@ -67,8 +67,14 @@ class SetItemSelectionCommand implements ChartEditorCommand
state.noteDisplayDirty = true;
}
public function shouldAddToHistory(state:ChartEditorState):Bool
{
// Add to the history if we actually performed an action.
return (state.currentNoteSelection != previousNoteSelection && state.currentEventSelection != previousEventSelection);
}
public function toString():String
{
return 'Select ${notes.length} Items';
return 'Select ${notes.length + events.length} Items';
}
}

View file

@ -38,6 +38,12 @@ class SwitchDifficultyCommand implements ChartEditorCommand
state.notePreviewDirty = true;
}
public function shouldAddToHistory(state:ChartEditorState):Bool
{
// Add to the history if we actually performed an action.
return (prevVariation != newVariation || prevDifficulty != newDifficulty);
}
public function toString():String
{
return 'Switch Difficulty';

View file

@ -11,6 +11,9 @@ import flixel.graphics.frames.FlxFramesCollection;
import flixel.graphics.frames.FlxTileFrames;
import flixel.math.FlxPoint;
import funkin.data.song.SongData.SongEventData;
import haxe.ui.tooltips.ToolTipRegionOptions;
import funkin.util.HaxeUIUtil;
import haxe.ui.tooltips.ToolTipManager;
/**
* A sprite that can be used to display a song event in a chart.
@ -36,6 +39,13 @@ class ChartEditorEventSprite extends FlxSprite
public var overrideStepTime(default, set):Null<Float> = null;
public var tooltip:ToolTipRegionOptions;
/**
* Whether this sprite is a "ghost" sprite used when hovering to place a new event.
*/
public var isGhost:Bool = false;
function set_overrideStepTime(value:Null<Float>):Null<Float>
{
if (overrideStepTime == value) return overrideStepTime;
@ -45,12 +55,14 @@ class ChartEditorEventSprite extends FlxSprite
return overrideStepTime;
}
public function new(parent:ChartEditorState)
public function new(parent:ChartEditorState, isGhost:Bool = false)
{
super();
this.parentState = parent;
this.isGhost = isGhost;
this.tooltip = HaxeUIUtil.buildTooltip('N/A');
this.frames = buildFrames();
buildAnimations();
@ -119,8 +131,10 @@ class ChartEditorEventSprite extends FlxSprite
return DEFAULT_EVENT;
}
public function playAnimation(name:String):Void
public function playAnimation(?name:String):Void
{
if (name == null) name = eventData?.event ?? DEFAULT_EVENT;
var correctedName = correctAnimationName(name);
this.animation.play(correctedName);
refresh();
@ -140,6 +154,7 @@ class ChartEditorEventSprite extends FlxSprite
// Disown parent. MAKE SURE TO REVIVE BEFORE REUSING
this.kill();
this.visible = false;
updateTooltipPosition();
return null;
}
else
@ -149,6 +164,8 @@ class ChartEditorEventSprite extends FlxSprite
this.eventData = value;
// Update the position to match the note data.
updateEventPosition();
// Update the tooltip text.
this.tooltip.tipData = {text: this.eventData.buildTooltip()};
return this.eventData;
}
}
@ -167,6 +184,31 @@ class ChartEditorEventSprite extends FlxSprite
this.x += origin.x;
this.y += origin.y;
}
this.updateTooltipPosition();
}
public function updateTooltipPosition():Void
{
// No tooltip for ghost sprites.
if (this.isGhost) return;
if (this.eventData == null)
{
// Disable the tooltip.
ToolTipManager.instance.unregisterTooltipRegion(this.tooltip);
}
else
{
// Update the position.
this.tooltip.left = this.x;
this.tooltip.top = this.y;
this.tooltip.width = this.width;
this.tooltip.height = this.height;
// Enable the tooltip.
ToolTipManager.instance.registerTooltipRegion(this.tooltip);
}
}
/**

View file

@ -39,6 +39,17 @@ class ChartEditorHoldNoteSprite extends SustainTrail
setup();
}
public override function updateHitbox():Void
{
// Expand the clickable hitbox to the full column width, then nudge to the left to re-center it.
width = ChartEditorState.GRID_SIZE;
height = graphicHeight;
var xOffset = (ChartEditorState.GRID_SIZE - graphicWidth) / 2;
offset.set(-xOffset, 0);
origin.set(width * 0.5, height * 0.5);
}
/**
* Set the height directly, to a value in pixels.
* @param h The desired height in pixels.
@ -52,6 +63,25 @@ class ChartEditorHoldNoteSprite extends SustainTrail
fullSustainLength = sustainLength;
}
#if FLX_DEBUG
/**
* Call this to override how debug bounding boxes are drawn for this sprite.
*/
public override function drawDebugOnCamera(camera:flixel.FlxCamera):Void
{
if (!camera.visible || !camera.exists || !isOnScreen(camera)) return;
var rect = getBoundingBox(camera);
trace('hold note bounding box: ' + rect.x + ', ' + rect.y + ', ' + rect.width + ', ' + rect.height);
var gfx = beginDrawDebug(camera);
debugBoundingBoxColor = 0xffFF66FF;
gfx.lineStyle(2, color, 0.5); // thickness, color, alpha
gfx.drawRect(rect.x, rect.y, rect.width, rect.height);
endDrawDebug(camera);
}
#end
function setup():Void
{
strumTime = 999999999;
@ -60,7 +90,9 @@ class ChartEditorHoldNoteSprite extends SustainTrail
active = true;
visible = true;
alpha = 1.0;
width = graphic.width / 8 * zoom; // amount of notes * 2
graphicWidth = graphic.width / 8 * zoom; // amount of notes * 2
updateHitbox();
}
public override function revive():Void
@ -154,7 +186,7 @@ class ChartEditorHoldNoteSprite extends SustainTrail
}
this.x += ChartEditorState.GRID_SIZE / 2;
this.x -= this.width / 2;
this.x -= this.graphicWidth / 2;
this.y += ChartEditorState.GRID_SIZE / 2;
@ -163,5 +195,8 @@ class ChartEditorHoldNoteSprite extends SustainTrail
this.x += origin.x;
this.y += origin.y;
}
// Account for expanded clickable hitbox.
this.x += this.offset.x;
}
}

View file

@ -70,9 +70,9 @@ class ChartEditorNotePreview extends FlxSprite
* @param event The data for the event.
* @param songLengthInMs The total length of the song in milliseconds.
*/
public function addEvent(event:SongEventData, songLengthInMs:Int):Void
public function addEvent(event:SongEventData, songLengthInMs:Int, ?isSelection:Bool = false):Void
{
drawNote(-1, false, Std.int(event.time), songLengthInMs);
drawNote(-1, false, Std.int(event.time), songLengthInMs, isSelection);
}
/**
@ -114,6 +114,19 @@ class ChartEditorNotePreview extends FlxSprite
}
}
/**
* Add an array of selected events to the preview.
* @param events The data for the events.
* @param songLengthInMs The total length of the song in milliseconds.
*/
public function addSelectedEvents(events:Array<SongEventData>, songLengthInMs:Int):Void
{
for (event in events)
{
addEvent(event, songLengthInMs, true);
}
}
/**
* Draws a note on the preview.
* @param dir Note data.

View file

@ -1,20 +1,33 @@
package funkin.ui.debug.charting.components;
import flixel.addons.display.FlxSliceSprite;
import flixel.FlxSprite;
import funkin.data.song.SongData.SongNoteData;
import flixel.math.FlxRect;
import funkin.data.song.SongData.SongEventData;
import funkin.data.song.SongData.SongNoteData;
import funkin.ui.debug.charting.handlers.ChartEditorThemeHandler;
/**
* A sprite that can be used to display a square over a selected note or event in the chart.
* Designed to be used and reused efficiently. Has no gameplay functionality.
*/
class ChartEditorSelectionSquareSprite extends FlxSprite
@:nullSafety
@:access(funkin.ui.debug.charting.ChartEditorState)
class ChartEditorSelectionSquareSprite extends FlxSliceSprite
{
public var noteData:Null<SongNoteData>;
public var eventData:Null<SongEventData>;
public function new()
public function new(chartEditorState:ChartEditorState)
{
super();
super(chartEditorState.selectionSquareBitmap,
new FlxRect(ChartEditorThemeHandler.SELECTION_SQUARE_BORDER_WIDTH
+ 4, ChartEditorThemeHandler.SELECTION_SQUARE_BORDER_WIDTH
+ 4,
ChartEditorState.GRID_SIZE
- (2 * ChartEditorThemeHandler.SELECTION_SQUARE_BORDER_WIDTH + 8),
ChartEditorState.GRID_SIZE
- (2 * ChartEditorThemeHandler.SELECTION_SQUARE_BORDER_WIDTH + 8)),
32, 32);
}
}

View file

@ -25,6 +25,10 @@ class ChartEditorEventContextMenu extends ChartEditorBaseContextMenu
function initialize()
{
contextmenuEdit.onClick = function(_) {
chartEditorState.showToolbox(ChartEditorState.CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT);
}
contextmenuDelete.onClick = function(_) {
chartEditorState.performCommand(new RemoveEventsCommand([data]));
}

View file

@ -0,0 +1,43 @@
package funkin.ui.debug.charting.contextmenus;
import haxe.ui.containers.menus.Menu;
import haxe.ui.containers.menus.MenuItem;
import haxe.ui.core.Screen;
import funkin.data.song.SongData.SongNoteData;
import funkin.ui.debug.charting.commands.FlipNotesCommand;
import funkin.ui.debug.charting.commands.RemoveNotesCommand;
import funkin.ui.debug.charting.commands.ExtendNoteLengthCommand;
@:access(funkin.ui.debug.charting.ChartEditorState)
@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/context-menus/hold-note.xml"))
class ChartEditorHoldNoteContextMenu extends ChartEditorBaseContextMenu
{
var contextmenuFlip:MenuItem;
var contextmenuDelete:MenuItem;
var data:SongNoteData;
public function new(chartEditorState2:ChartEditorState, xPos2:Float = 0, yPos2:Float = 0, data:SongNoteData)
{
super(chartEditorState2, xPos2, yPos2);
this.data = data;
initialize();
}
function initialize():Void
{
// NOTE: Remember to use commands here to ensure undo/redo works properly
contextmenuFlip.onClick = function(_) {
chartEditorState.performCommand(new FlipNotesCommand([data]));
}
contextmenuRemoveHold.onClick = function(_) {
chartEditorState.performCommand(new ExtendNoteLengthCommand(data, 0));
}
contextmenuDelete.onClick = function(_) {
chartEditorState.performCommand(new RemoveNotesCommand([data]));
}
}
}

View file

@ -6,6 +6,7 @@ import haxe.ui.core.Screen;
import funkin.data.song.SongData.SongNoteData;
import funkin.ui.debug.charting.commands.FlipNotesCommand;
import funkin.ui.debug.charting.commands.RemoveNotesCommand;
import funkin.ui.debug.charting.commands.ExtendNoteLengthCommand;
@:access(funkin.ui.debug.charting.ChartEditorState)
@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/context-menus/note.xml"))
@ -31,6 +32,10 @@ class ChartEditorNoteContextMenu extends ChartEditorBaseContextMenu
chartEditorState.performCommand(new FlipNotesCommand([data]));
}
contextmenuAddHold.onClick = function(_) {
chartEditorState.performCommand(new ExtendNoteLengthCommand(data, 4, STEPS));
}
contextmenuDelete.onClick = function(_) {
chartEditorState.performCommand(new RemoveNotesCommand([data]));
}

View file

@ -13,6 +13,7 @@ import haxe.ui.notifications.NotificationType;
// @:nullSafety // TODO: Fix null safety when used with HaxeUI build macros.
@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/dialogs/upload-chart.xml"))
@:access(funkin.ui.debug.charting.ChartEditorState)
class ChartEditorUploadChartDialog extends ChartEditorBaseDialog
{
var dropHandlers:Array<DialogDropTarget> = [];

View file

@ -0,0 +1,311 @@
package funkin.ui.debug.charting.dialogs;
import funkin.input.Cursor;
import funkin.ui.debug.charting.dialogs.ChartEditorBaseDialog.DialogDropTarget;
import funkin.ui.debug.charting.dialogs.ChartEditorBaseDialog.DialogParams;
import funkin.util.FileUtil;
import funkin.play.character.CharacterData;
import haxe.io.Path;
import haxe.ui.components.Button;
import haxe.ui.components.Label;
import haxe.ui.containers.dialogs.Dialog.DialogButton;
import haxe.ui.containers.dialogs.Dialog.DialogEvent;
import haxe.ui.containers.Box;
import haxe.ui.containers.dialogs.Dialogs;
import haxe.ui.core.Component;
import haxe.ui.notifications.NotificationManager;
import haxe.ui.notifications.NotificationType;
// @:nullSafety // TODO: Fix null safety when used with HaxeUI build macros.
@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/dialogs/upload-vocals.xml"))
@:access(funkin.ui.debug.charting.ChartEditorState)
class ChartEditorUploadVocalsDialog extends ChartEditorBaseDialog
{
var dropHandlers:Array<DialogDropTarget> = [];
var vocalContainer:Component;
var dialogCancel:Button;
var dialogNoVocals:Button;
var dialogContinue:Button;
var charIds:Array<String>;
var instId:String;
var hasClearedVocals:Bool = false;
public function new(state2:ChartEditorState, charIds:Array<String>, params2:DialogParams)
{
super(state2, params2);
this.charIds = charIds;
this.instId = chartEditorState.currentInstrumentalId;
dialogCancel.onClick = function(_) {
hideDialog(DialogButton.CANCEL);
}
dialogNoVocals.onClick = function(_) {
// Dismiss
chartEditorState.wipeVocalData();
hideDialog(DialogButton.APPLY);
};
dialogContinue.onClick = function(_) {
// Dismiss
hideDialog(DialogButton.APPLY);
};
buildDropHandlers();
}
function buildDropHandlers():Void
{
for (charKey in charIds)
{
trace('Adding vocal upload for character ${charKey}');
var charMetadata:Null<CharacterData> = CharacterDataParser.fetchCharacterData(charKey);
var charName:String = charMetadata?.name ?? charKey;
var vocalsEntry = new ChartEditorUploadVocalsEntry(charName);
var dropHandler:DialogDropTarget = {component: vocalsEntry, handler: null};
var onDropFile:String->Void = function(pathStr:String) {
trace('Selected file: $pathStr');
var path:Path = new Path(pathStr);
if (chartEditorState.loadVocalsFromPath(path, charKey, this.instId, !this.hasClearedVocals))
{
this.hasClearedVocals = true;
// Tell the user the load was successful.
chartEditorState.success('Loaded Vocals', 'Loaded vocals for $charName (${path.file}.${path.ext}), variation ${chartEditorState.selectedVariation}');
#if FILE_DROP_SUPPORTED
vocalsEntry.vocalsEntryLabel.text = 'Voices for $charName (drag and drop, or click to browse)\nSelected file: ${path.file}.${path.ext}';
#else
vocalsEntry.vocalsEntryLabel.text = 'Voices for $charName (click to browse)\n${path.file}.${path.ext}';
#end
dialogNoVocals.hidden = true;
chartEditorState.removeDropHandler(dropHandler);
}
else
{
trace('Failed to load vocal track (${path.file}.${path.ext})');
chartEditorState.error('Failed to Load Vocals',
'Failed to load vocal track (${path.file}.${path.ext}) for variation (${chartEditorState.selectedVariation})');
#if FILE_DROP_SUPPORTED
vocalsEntry.vocalsEntryLabel.text = 'Drag and drop vocals for $charName here, or click to browse.';
#else
vocalsEntry.vocalsEntryLabel.text = 'Click to browse for vocals for $charName.';
#end
}
};
vocalsEntry.onClick = function(_event) {
Dialogs.openBinaryFile('Open $charName Vocals', [
{label: 'Audio File (.ogg)', extension: 'ogg'}], function(selectedFile) {
if (selectedFile != null && selectedFile.bytes != null)
{
trace('Selected file: ' + selectedFile.name);
if (chartEditorState.loadVocalsFromBytes(selectedFile.bytes, charKey, this.instId, !this.hasClearedVocals))
{
hasClearedVocals = true;
// Tell the user the load was successful.
chartEditorState.success('Loaded Vocals',
'Loaded vocals for $charName (${selectedFile.name}), variation ${chartEditorState.selectedVariation}');
#if FILE_DROP_SUPPORTED
vocalsEntry.vocalsEntryLabel.text = 'Voices for $charName (drag and drop, or click to browse)\nSelected file: ${selectedFile.name}';
#else
vocalsEntry.vocalsEntryLabel.text = 'Voices for $charName (click to browse)\n${selectedFile.name}';
#end
dialogNoVocals.hidden = true;
}
else
{
trace('Failed to load vocal track (${selectedFile.fullPath})');
chartEditorState.error('Failed to Load Vocals',
'Failed to load vocal track (${selectedFile.name}) for variation (${chartEditorState.selectedVariation})');
#if FILE_DROP_SUPPORTED
vocalsEntry.vocalsEntryLabel.text = 'Drag and drop vocals for $charName here, or click to browse.';
#else
vocalsEntry.vocalsEntryLabel.text = 'Click to browse for vocals for $charName.';
#end
}
}
});
}
dropHandler.handler = onDropFile;
// onDropFile
#if FILE_DROP_SUPPORTED
dropHandlers.push(dropHandler);
#end
vocalContainer.addComponent(vocalsEntry);
}
}
public static function build(state:ChartEditorState, charIds:Array<String>, ?closable:Bool, ?modal:Bool):ChartEditorUploadVocalsDialog
{
var dialog = new ChartEditorUploadVocalsDialog(state, charIds,
{
closable: closable ?? false,
modal: modal ?? true
});
for (dropTarget in dialog.dropHandlers)
{
state.addDropHandler(dropTarget);
}
dialog.showDialog(modal ?? true);
return dialog;
}
public override function onClose(event:DialogEvent):Void
{
super.onClose(event);
if (event.button != DialogButton.APPLY && !this.closable)
{
// User cancelled the wizard! Back to the welcome dialog.
chartEditorState.openWelcomeDialog(this.closable);
}
for (dropTarget in dropHandlers)
{
chartEditorState.removeDropHandler(dropTarget);
}
}
public override function lock():Void
{
super.lock();
this.dialogCancel.disabled = true;
}
public override function unlock():Void
{
super.unlock();
this.dialogCancel.disabled = false;
}
/**
* Called when clicking the Upload Chart box.
*/
public function onClickChartBox():Void
{
if (this.locked) return;
this.lock();
// TODO / BUG: File filtering not working on mac finder dialog, so we don't use it for now
#if !mac
FileUtil.browseForBinaryFile('Open Chart', [FileUtil.FILE_EXTENSION_INFO_FNFC], onSelectFile, onCancelBrowse);
#else
FileUtil.browseForBinaryFile('Open Chart', null, onSelectFile, onCancelBrowse);
#end
}
/**
* Called when a file is selected by dropping a file onto the Upload Chart box.
*/
function onDropFileChartBox(pathStr:String):Void
{
var path:Path = new Path(pathStr);
trace('Dropped file (${path})');
try
{
var result:Null<Array<String>> = ChartEditorImportExportHandler.loadFromFNFCPath(chartEditorState, path.toString());
if (result != null)
{
chartEditorState.success('Loaded Chart',
result.length == 0 ? 'Loaded chart (${path.toString()})' : 'Loaded chart (${path.toString()})\n${result.join("\n")}');
this.hideDialog(DialogButton.APPLY);
}
else
{
chartEditorState.failure('Failed to Load Chart', 'Failed to load chart (${path.toString()})');
}
}
catch (err)
{
chartEditorState.failure('Failed to Load Chart', 'Failed to load chart (${path.toString()}): ${err}');
}
}
/**
* Called when a file is selected by the dialog displayed when clicking the Upload Chart box.
*/
function onSelectFile(selectedFile:SelectedFileInfo):Void
{
this.unlock();
if (selectedFile != null && selectedFile.bytes != null)
{
try
{
var result:Null<Array<String>> = ChartEditorImportExportHandler.loadFromFNFC(chartEditorState, selectedFile.bytes);
if (result != null)
{
chartEditorState.success('Loaded Chart',
result.length == 0 ? 'Loaded chart (${selectedFile.name})' : 'Loaded chart (${selectedFile.name})\n${result.join("\n")}');
if (selectedFile.fullPath != null) chartEditorState.currentWorkingFilePath = selectedFile.fullPath;
this.hideDialog(DialogButton.APPLY);
}
}
catch (err)
{
chartEditorState.failure('Failed to Load Chart', 'Failed to load chart (${selectedFile.name}): ${err}');
}
}
}
function onCancelBrowse():Void
{
this.unlock();
}
}
@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/dialogs/upload-vocals-entry.xml"))
class ChartEditorUploadVocalsEntry extends Box
{
public var vocalsEntryLabel:Label;
var charName:String;
public function new(charName:String)
{
super();
this.charName = charName;
#if FILE_DROP_SUPPORTED
vocalsEntryLabel.text = 'Drag and drop vocals for $charName here, or click to browse.';
#else
vocalsEntryLabel.text = 'Click to browse for vocals for $charName.';
#end
this.onMouseOver = function(_event) {
// if (this.locked) return;
this.swapClass('upload-bg', 'upload-bg-hover');
Cursor.cursorMode = Pointer;
}
this.onMouseOut = function(_event) {
this.swapClass('upload-bg-hover', 'upload-bg');
Cursor.cursorMode = Default;
}
}
}

View file

@ -188,8 +188,9 @@ class ChartEditorAudioHandler
state.audioVisGroup.playerVis.realtimeVisLenght = Conductor.instance.getStepTimeInMs(16) * 0.00195;
state.audioVisGroup.playerVis.daHeight = (ChartEditorState.GRID_SIZE) * 16;
state.audioVisGroup.playerVis.detail = 1;
state.audioVisGroup.playerVis.y = Math.max(state.gridTiledSprite?.y ?? 0.0, ChartEditorState.GRID_INITIAL_Y_POS - ChartEditorState.GRID_TOP_PAD);
state.audioVocalTrackGroup.playerVoicesOffset = state.currentSongOffsets.getVocalOffset(charId);
state.audioVocalTrackGroup.playerVoicesOffset = state.currentVocalOffset;
return true;
case DAD:
state.audioVocalTrackGroup.addOpponentVoice(vocalTrack);
@ -199,8 +200,9 @@ class ChartEditorAudioHandler
state.audioVisGroup.opponentVis.realtimeVisLenght = Conductor.instance.getStepTimeInMs(16) * 0.00195;
state.audioVisGroup.opponentVis.daHeight = (ChartEditorState.GRID_SIZE) * 16;
state.audioVisGroup.opponentVis.detail = 1;
state.audioVisGroup.opponentVis.y = Math.max(state.gridTiledSprite?.y ?? 0.0, ChartEditorState.GRID_INITIAL_Y_POS - ChartEditorState.GRID_TOP_PAD);
state.audioVocalTrackGroup.opponentVoicesOffset = state.currentSongOffsets.getVocalOffset(charId);
state.audioVocalTrackGroup.opponentVoicesOffset = state.currentVocalOffset;
return true;
case OTHER:
@ -221,6 +223,10 @@ class ChartEditorAudioHandler
{
state.audioVocalTrackGroup.clear();
}
if (state.audioVisGroup != null)
{
state.audioVisGroup.clearAllVis();
}
}
/**

View file

@ -2,6 +2,7 @@ package funkin.ui.debug.charting.handlers;
import funkin.ui.debug.charting.contextmenus.ChartEditorDefaultContextMenu;
import funkin.ui.debug.charting.contextmenus.ChartEditorEventContextMenu;
import funkin.ui.debug.charting.contextmenus.ChartEditorHoldNoteContextMenu;
import funkin.ui.debug.charting.contextmenus.ChartEditorNoteContextMenu;
import funkin.ui.debug.charting.contextmenus.ChartEditorSelectionContextMenu;
import haxe.ui.containers.menus.Menu;
@ -23,16 +24,33 @@ class ChartEditorContextMenuHandler
displayMenu(state, new ChartEditorDefaultContextMenu(state, xPos, yPos));
}
/**
* Opened when shift+right-clicking a selection of multiple items.
*/
public static function openSelectionContextMenu(state:ChartEditorState, xPos:Float, yPos:Float)
{
displayMenu(state, new ChartEditorSelectionContextMenu(state, xPos, yPos));
}
/**
* Opened when shift+right-clicking a single note.
*/
public static function openNoteContextMenu(state:ChartEditorState, xPos:Float, yPos:Float, data:SongNoteData)
{
displayMenu(state, new ChartEditorNoteContextMenu(state, xPos, yPos, data));
}
/**
* Opened when shift+right-clicking a single hold note.
*/
public static function openHoldNoteContextMenu(state:ChartEditorState, xPos:Float, yPos:Float, data:SongNoteData)
{
displayMenu(state, new ChartEditorHoldNoteContextMenu(state, xPos, yPos, data));
}
/**
* Opened when shift+right-clicking a single event.
*/
public static function openEventContextMenu(state:ChartEditorState, xPos:Float, yPos:Float, data:SongEventData)
{
displayMenu(state, new ChartEditorEventContextMenu(state, xPos, yPos, data));

View file

@ -13,12 +13,13 @@ import funkin.play.character.BaseCharacter;
import funkin.play.character.CharacterData;
import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.play.song.Song;
import funkin.play.stage.StageData;
import funkin.data.stage.StageData;
import funkin.ui.debug.charting.dialogs.ChartEditorAboutDialog;
import funkin.ui.debug.charting.dialogs.ChartEditorBaseDialog.DialogDropTarget;
import funkin.ui.debug.charting.dialogs.ChartEditorCharacterIconSelectorMenu;
import funkin.ui.debug.charting.dialogs.ChartEditorUploadChartDialog;
import funkin.ui.debug.charting.dialogs.ChartEditorWelcomeDialog;
import funkin.ui.debug.charting.dialogs.ChartEditorUploadVocalsDialog;
import funkin.ui.debug.charting.util.ChartEditorDropdowns;
import funkin.util.Constants;
import funkin.util.DateUtil;
@ -59,11 +60,8 @@ using Lambda;
class ChartEditorDialogHandler
{
// Paths to HaxeUI layout files for each dialog.
static final CHART_EDITOR_DIALOG_UPLOAD_CHART_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-chart');
static final CHART_EDITOR_DIALOG_UPLOAD_INST_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-inst');
static final CHART_EDITOR_DIALOG_SONG_METADATA_LAYOUT:String = Paths.ui('chart-editor/dialogs/song-metadata');
static final CHART_EDITOR_DIALOG_UPLOAD_VOCALS_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-vocals');
static final CHART_EDITOR_DIALOG_UPLOAD_VOCALS_ENTRY_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-vocals-entry');
static final CHART_EDITOR_DIALOG_OPEN_CHART_PARTS_LAYOUT:String = Paths.ui('chart-editor/dialogs/open-chart-parts');
static final CHART_EDITOR_DIALOG_OPEN_CHART_PARTS_ENTRY_LAYOUT:String = Paths.ui('chart-editor/dialogs/open-chart-parts-entry');
static final CHART_EDITOR_DIALOG_IMPORT_CHART_LAYOUT:String = Paths.ui('chart-editor/dialogs/import-chart');
@ -105,6 +103,56 @@ class ChartEditorDialogHandler
return dialog;
}
/**
* Builds and opens a dialog letting the user browse for a chart file to open.
* @param state The current chart editor state.
* @param closable Whether the dialog can be closed by the user.
* @return The dialog that was opened.
*/
public static function openBrowseFNFC(state:ChartEditorState, closable:Bool):Null<Dialog>
{
var dialog = ChartEditorUploadChartDialog.build(state, closable);
dialog.zIndex = 1000;
state.isHaxeUIDialogOpen = true;
return dialog;
}
/**
* Builds and opens a dialog where the user uploads vocals for the current song.
* @param state The current chart editor state.
* @param closable Whether the dialog can be closed by the user.
* @return The dialog that was opened.
*/
public static function openUploadVocalsDialog(state:ChartEditorState, closable:Bool = true):Dialog
{
var charData:SongCharacterData = state.currentSongMetadata.playData.characters;
var hasClearedVocals:Bool = false;
var charIdsForVocals:Array<String> = [charData.player, charData.opponent];
var dialog = ChartEditorUploadVocalsDialog.build(state, charIdsForVocals, closable);
dialog.zIndex = 1000;
state.isHaxeUIDialogOpen = true;
return dialog;
}
/**
* Builds and opens the dialog for selecting a character.
*/
public static function openCharacterDropdown(state:ChartEditorState, charType:CharacterType, lockPosition:Bool = false):Null<Menu>
{
var menu = ChartEditorCharacterIconSelectorMenu.build(state, charType, lockPosition);
menu.zIndex = 1000;
return menu;
}
/**
* Builds and opens a dialog letting the user know a backup is available, and prompting them to load it.
*/
@ -186,22 +234,6 @@ class ChartEditorDialogHandler
return dialog;
}
/**
* Builds and opens a dialog letting the user browse for a chart file to open.
* @param state The current chart editor state.
* @param closable Whether the dialog can be closed by the user.
* @return The dialog that was opened.
*/
public static function openBrowseFNFC(state:ChartEditorState, closable:Bool):Null<Dialog>
{
var dialog = ChartEditorUploadChartDialog.build(state, closable);
dialog.zIndex = 1000;
state.isHaxeUIDialogOpen = true;
return dialog;
}
/**
* Open the wizard for opening an existing chart from individual files.
* @param state
@ -288,15 +320,6 @@ class ChartEditorDialogHandler
};
}
public static function openCharacterDropdown(state:ChartEditorState, charType:CharacterType, lockPosition:Bool = false):Null<Menu>
{
var menu = ChartEditorCharacterIconSelectorMenu.build(state, charType, lockPosition);
menu.zIndex = 1000;
return menu;
}
public static function openCreateSongWizardBasicOnly(state:ChartEditorState, closable:Bool):Void
{
// Step 1. Song Metadata
@ -699,150 +722,6 @@ class ChartEditorDialogHandler
return dialog;
}
/**
* Builds and opens a dialog where the user uploads vocals for the current song.
* @param state The current chart editor state.
* @param closable Whether the dialog can be closed by the user.
* @return The dialog that was opened.
*/
public static function openUploadVocalsDialog(state:ChartEditorState, closable:Bool = true):Dialog
{
var instId:String = state.currentInstrumentalId;
var charIdsForVocals:Array<String> = [];
var charData:SongCharacterData = state.currentSongMetadata.playData.characters;
var hasClearedVocals:Bool = false;
charIdsForVocals.push(charData.player);
charIdsForVocals.push(charData.opponent);
var dialog:Null<Dialog> = openDialog(state, CHART_EDITOR_DIALOG_UPLOAD_VOCALS_LAYOUT, true, closable);
if (dialog == null) throw 'Could not locate Upload Vocals dialog';
var dialogContainer:Null<Component> = dialog.findComponent('vocalContainer');
if (dialogContainer == null) throw 'Could not locate vocalContainer in Upload Vocals dialog';
var buttonCancel:Null<Button> = dialog.findComponent('dialogCancel', Button);
if (buttonCancel == null) throw 'Could not locate dialogCancel button in Upload Vocals dialog';
buttonCancel.onClick = function(_) {
dialog.hideDialog(DialogButton.CANCEL);
}
var dialogNoVocals:Null<Button> = dialog.findComponent('dialogNoVocals', Button);
if (dialogNoVocals == null) throw 'Could not locate dialogNoVocals button in Upload Vocals dialog';
dialogNoVocals.onClick = function(_) {
// Dismiss
state.wipeVocalData();
dialog.hideDialog(DialogButton.APPLY);
};
for (charKey in charIdsForVocals)
{
trace('Adding vocal upload for character ${charKey}');
var charMetadata:Null<CharacterData> = CharacterDataParser.fetchCharacterData(charKey);
var charName:String = charMetadata != null ? charMetadata.name : charKey;
var vocalsEntry:Component = RuntimeComponentBuilder.fromAsset(CHART_EDITOR_DIALOG_UPLOAD_VOCALS_ENTRY_LAYOUT);
var vocalsEntryLabel:Null<Label> = vocalsEntry.findComponent('vocalsEntryLabel', Label);
if (vocalsEntryLabel == null) throw 'Could not locate vocalsEntryLabel in Upload Vocals dialog';
#if FILE_DROP_SUPPORTED
vocalsEntryLabel.text = 'Drag and drop vocals for $charName here, or click to browse.';
#else
vocalsEntryLabel.text = 'Click to browse for vocals for $charName.';
#end
var dropHandler:DialogDropTarget = {component: vocalsEntry, handler: null};
var onDropFile:String->Void = function(pathStr:String) {
trace('Selected file: $pathStr');
var path:Path = new Path(pathStr);
if (state.loadVocalsFromPath(path, charKey, instId, !hasClearedVocals))
{
hasClearedVocals = true;
// Tell the user the load was successful.
state.success('Loaded Vocals', 'Loaded vocals for $charName (${path.file}.${path.ext}), variation ${state.selectedVariation}');
#if FILE_DROP_SUPPORTED
vocalsEntryLabel.text = 'Voices for $charName (drag and drop, or click to browse)\nSelected file: ${path.file}.${path.ext}';
#else
vocalsEntryLabel.text = 'Voices for $charName (click to browse)\n${path.file}.${path.ext}';
#end
dialogNoVocals.hidden = true;
state.removeDropHandler(dropHandler);
}
else
{
trace('Failed to load vocal track (${path.file}.${path.ext})');
state.error('Failed to Load Vocals', 'Failed to load vocal track (${path.file}.${path.ext}) for variation (${state.selectedVariation})');
#if FILE_DROP_SUPPORTED
vocalsEntryLabel.text = 'Drag and drop vocals for $charName here, or click to browse.';
#else
vocalsEntryLabel.text = 'Click to browse for vocals for $charName.';
#end
}
};
dropHandler.handler = onDropFile;
vocalsEntry.onClick = function(_event) {
Dialogs.openBinaryFile('Open $charName Vocals', [
{label: 'Audio File (.ogg)', extension: 'ogg'}], function(selectedFile) {
if (selectedFile != null && selectedFile.bytes != null)
{
trace('Selected file: ' + selectedFile.name);
if (state.loadVocalsFromBytes(selectedFile.bytes, charKey, instId, !hasClearedVocals))
{
hasClearedVocals = true;
// Tell the user the load was successful.
state.success('Loaded Vocals', 'Loaded vocals for $charName (${selectedFile.name}), variation ${state.selectedVariation}');
#if FILE_DROP_SUPPORTED
vocalsEntryLabel.text = 'Voices for $charName (drag and drop, or click to browse)\nSelected file: ${selectedFile.name}';
#else
vocalsEntryLabel.text = 'Voices for $charName (click to browse)\n${selectedFile.name}';
#end
dialogNoVocals.hidden = true;
}
else
{
trace('Failed to load vocal track (${selectedFile.fullPath})');
state.error('Failed to Load Vocals', 'Failed to load vocal track (${selectedFile.name}) for variation (${state.selectedVariation})');
#if FILE_DROP_SUPPORTED
vocalsEntryLabel.text = 'Drag and drop vocals for $charName here, or click to browse.';
#else
vocalsEntryLabel.text = 'Click to browse for vocals for $charName.';
#end
}
}
});
}
// onDropFile
#if FILE_DROP_SUPPORTED
addDropHandler(dropHandler);
#end
dialogContainer.addComponent(vocalsEntry);
}
var dialogContinue:Null<Button> = dialog.findComponent('dialogContinue', Button);
if (dialogContinue == null) throw 'Could not locate dialogContinue button in Upload Vocals dialog';
dialogContinue.onClick = function(_) {
// Dismiss
dialog.hideDialog(DialogButton.APPLY);
};
return dialog;
}
/**
* Builds and opens a dialog where the user upload the JSON files for a song.
* @param state The current chart editor state.

View file

@ -2,6 +2,10 @@ package funkin.ui.debug.charting.handlers;
import funkin.util.PlatformUtil;
/**
* Handles modifying the shortcut text of menu items based on the current platform.
* On MacOS, `Ctrl`, `Alt`, and `Shift` are replaced with `` (or `^`), ``, and ``, respectively.
*/
@:access(funkin.ui.debug.charting.ChartEditorState)
class ChartEditorShortcutHandler
{
@ -18,7 +22,8 @@ class ChartEditorShortcutHandler
state.menubarItemCopy.shortcutText = ctrlOrCmd('C');
state.menubarItemPaste.shortcutText = ctrlOrCmd('V');
state.menubarItemSelectAll.shortcutText = ctrlOrCmd('A');
state.menubarItemSelectAllNotes.shortcutText = ctrlOrCmd('A');
state.menubarItemSelectAllEvents.shortcutText = ctrlOrCmd(alt('A'));
state.menubarItemSelectInverse.shortcutText = ctrlOrCmd('I');
state.menubarItemSelectNone.shortcutText = ctrlOrCmd('D');
state.menubarItemSelectBeforeCursor.shortcutText = shift('Home');

View file

@ -52,7 +52,7 @@ class ChartEditorThemeHandler
// Border on the square highlighting selected notes.
static final SELECTION_SQUARE_BORDER_COLOR_LIGHT:FlxColor = 0xFF339933;
static final SELECTION_SQUARE_BORDER_COLOR_DARK:FlxColor = 0xFF339933;
static final SELECTION_SQUARE_BORDER_WIDTH:Int = 1;
public static final SELECTION_SQUARE_BORDER_WIDTH:Int = 1;
// Fill on the square highlighting selected notes.
// Make sure this is transparent so you can see the notes underneath.
@ -317,6 +317,12 @@ class ChartEditorThemeHandler
ChartEditorState.GRID_SIZE
- (2 * SELECTION_SQUARE_BORDER_WIDTH + 8)),
32, 32);
state.selectionBoxSprite.scrollFactor.set(0, 0);
state.selectionBoxSprite.zIndex = 30;
state.add(state.selectionBoxSprite);
state.setSelectionBoxBounds();
}
static function updateNotePreview(state:ChartEditorState):Void

View file

@ -1,7 +1,6 @@
package funkin.ui.debug.charting.handlers;
import funkin.play.stage.StageData.StageDataParser;
import funkin.play.stage.StageData;
import funkin.data.stage.StageData;
import funkin.play.character.CharacterData;
import funkin.play.character.CharacterData.CharacterDataParser;
import haxe.ui.components.HorizontalSlider;
@ -16,8 +15,7 @@ import funkin.play.character.CharacterData;
import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.play.event.SongEvent;
import funkin.play.song.SongSerializer;
import funkin.play.stage.StageData;
import funkin.play.stage.StageData.StageDataParser;
import funkin.data.stage.StageData;
import haxe.ui.RuntimeComponentBuilder;
import funkin.ui.debug.charting.util.ChartEditorDropdowns;
import funkin.ui.haxeui.components.CharacterPlayer;
@ -38,6 +36,7 @@ import haxe.ui.containers.dialogs.Dialog.DialogEvent;
import funkin.ui.debug.charting.toolboxes.ChartEditorBaseToolbox;
import funkin.ui.debug.charting.toolboxes.ChartEditorMetadataToolbox;
import funkin.ui.debug.charting.toolboxes.ChartEditorEventDataToolbox;
import funkin.ui.debug.charting.toolboxes.ChartEditorDifficultyToolbox;
import haxe.ui.containers.Frame;
import haxe.ui.containers.Grid;
import haxe.ui.containers.TreeView;
@ -86,7 +85,7 @@ class ChartEditorToolboxHandler
case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYTEST_PROPERTIES_LAYOUT:
onShowToolboxPlaytestProperties(state, toolbox);
case ChartEditorState.CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT:
onShowToolboxDifficulty(state, toolbox);
cast(toolbox, ChartEditorBaseToolbox).refresh();
case ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT:
// TODO: Fix this.
cast(toolbox, ChartEditorBaseToolbox).refresh();
@ -125,8 +124,6 @@ class ChartEditorToolboxHandler
onHideToolboxEventData(state, toolbox);
case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYTEST_PROPERTIES_LAYOUT:
onHideToolboxPlaytestProperties(state, toolbox);
case ChartEditorState.CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT:
onHideToolboxDifficulty(state, toolbox);
case ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT:
onHideToolboxMetadata(state, toolbox);
case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT:
@ -311,8 +308,6 @@ class ChartEditorToolboxHandler
static function onHideToolboxEventData(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
static function onHideToolboxDifficulty(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
static function onShowToolboxPlaytestProperties(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
static function onHideToolboxPlaytestProperties(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
@ -360,91 +355,15 @@ class ChartEditorToolboxHandler
return toolbox;
}
static function buildToolboxDifficultyLayout(state:ChartEditorState):Null<CollapsibleDialog>
static function buildToolboxDifficultyLayout(state:ChartEditorState):Null<ChartEditorBaseToolbox>
{
var toolbox:CollapsibleDialog = cast RuntimeComponentBuilder.fromAsset(ChartEditorState.CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT);
var toolbox:ChartEditorBaseToolbox = ChartEditorDifficultyToolbox.build(state);
if (toolbox == null) return null;
// Starting position.
toolbox.x = 125;
toolbox.y = 200;
toolbox.onDialogClosed = function(event:UIEvent) {
state.menubarItemToggleToolboxDifficulty.selected = false;
}
var difficultyToolboxAddVariation:Null<Button> = toolbox.findComponent('difficultyToolboxAddVariation', Button);
if (difficultyToolboxAddVariation == null)
throw 'ChartEditorToolboxHandler.buildToolboxDifficultyLayout() - Could not find difficultyToolboxAddVariation component.';
var difficultyToolboxAddDifficulty:Null<Button> = toolbox.findComponent('difficultyToolboxAddDifficulty', Button);
if (difficultyToolboxAddDifficulty == null)
throw 'ChartEditorToolboxHandler.buildToolboxDifficultyLayout() - Could not find difficultyToolboxAddDifficulty component.';
var difficultyToolboxSaveMetadata:Null<Button> = toolbox.findComponent('difficultyToolboxSaveMetadata', Button);
if (difficultyToolboxSaveMetadata == null)
throw 'ChartEditorToolboxHandler.buildToolboxDifficultyLayout() - Could not find difficultyToolboxSaveMetadata component.';
var difficultyToolboxSaveChart:Null<Button> = toolbox.findComponent('difficultyToolboxSaveChart', Button);
if (difficultyToolboxSaveChart == null)
throw 'ChartEditorToolboxHandler.buildToolboxDifficultyLayout() - Could not find difficultyToolboxSaveChart component.';
// var difficultyToolboxSaveAll:Null<Button> = toolbox.findComponent('difficultyToolboxSaveAll', Button);
// if (difficultyToolboxSaveAll == null) throw 'ChartEditorToolboxHandler.buildToolboxDifficultyLayout() - Could not find difficultyToolboxSaveAll component.';
var difficultyToolboxLoadMetadata:Null<Button> = toolbox.findComponent('difficultyToolboxLoadMetadata', Button);
if (difficultyToolboxLoadMetadata == null)
throw 'ChartEditorToolboxHandler.buildToolboxDifficultyLayout() - Could not find difficultyToolboxLoadMetadata component.';
var difficultyToolboxLoadChart:Null<Button> = toolbox.findComponent('difficultyToolboxLoadChart', Button);
if (difficultyToolboxLoadChart == null)
throw 'ChartEditorToolboxHandler.buildToolboxDifficultyLayout() - Could not find difficultyToolboxLoadChart component.';
difficultyToolboxAddVariation.onClick = function(_:UIEvent) {
state.openAddVariationDialog(true);
};
difficultyToolboxAddDifficulty.onClick = function(_:UIEvent) {
state.openAddDifficultyDialog(true);
};
difficultyToolboxSaveMetadata.onClick = function(_:UIEvent) {
var vari:String = state.selectedVariation != Constants.DEFAULT_VARIATION ? '-${state.selectedVariation}' : '';
FileUtil.writeFileReference('${state.currentSongId}$vari-metadata.json', state.currentSongMetadata.serialize());
};
difficultyToolboxSaveChart.onClick = function(_:UIEvent) {
var vari:String = state.selectedVariation != Constants.DEFAULT_VARIATION ? '-${state.selectedVariation}' : '';
FileUtil.writeFileReference('${state.currentSongId}$vari-chart.json', state.currentSongChartData.serialize());
};
difficultyToolboxLoadMetadata.onClick = function(_:UIEvent) {
// Replace metadata for current variation.
SongSerializer.importSongMetadataAsync(function(songMetadata) {
state.currentSongMetadata = songMetadata;
});
};
difficultyToolboxLoadChart.onClick = function(_:UIEvent) {
// Replace chart data for current variation.
SongSerializer.importSongChartDataAsync(function(songChartData) {
state.currentSongChartData = songChartData;
state.noteDisplayDirty = true;
});
};
state.difficultySelectDirty = true;
return toolbox;
}
static function onShowToolboxDifficulty(state:ChartEditorState, toolbox:CollapsibleDialog):Void
{
// Update the selected difficulty when reopening the toolbox.
var treeView:Null<TreeView> = toolbox.findComponent('difficultyToolboxTree');
if (treeView == null) return;
var current = state.getCurrentTreeDifficultyNode(treeView);
if (current == null) return;
treeView.selectedNode = current;
trace('selected node: ${treeView.selectedNode}');
}
static function buildToolboxMetadataLayout(state:ChartEditorState):Null<ChartEditorBaseToolbox>
{
var toolbox:ChartEditorBaseToolbox = ChartEditorMetadataToolbox.build(state);

View file

@ -0,0 +1,239 @@
package funkin.ui.debug.charting.toolboxes;
import funkin.play.character.BaseCharacter.CharacterType;
import funkin.play.character.CharacterData;
import funkin.data.stage.StageData;
import funkin.data.stage.StageRegistry;
import funkin.ui.debug.charting.commands.ChangeStartingBPMCommand;
import funkin.ui.debug.charting.util.ChartEditorDropdowns;
import haxe.ui.components.Button;
import haxe.ui.components.CheckBox;
import haxe.ui.containers.dialogs.Dialogs;
import haxe.ui.containers.dialogs.Dialog.DialogButton;
import funkin.data.song.SongData.SongMetadata;
import haxe.ui.components.DropDown;
import haxe.ui.components.HorizontalSlider;
import funkin.util.FileUtil;
import haxe.ui.containers.dialogs.MessageBox.MessageBoxType;
import funkin.play.song.SongSerializer;
import haxe.ui.components.Label;
import haxe.ui.components.NumberStepper;
import haxe.ui.components.Slider;
import haxe.ui.components.TextField;
import funkin.play.stage.Stage;
import haxe.ui.containers.Box;
import haxe.ui.containers.TreeView;
import haxe.ui.containers.TreeViewNode;
import haxe.ui.containers.Frame;
import haxe.ui.events.UIEvent;
/**
* The toolbox which allows viewing the list of difficulties, switching to a specific one,
* and adding/removing variations and difficulties.
*/
// @:nullSafety // TODO: Fix null safety when used with HaxeUI build macros.
@:access(funkin.ui.debug.charting.ChartEditorState)
@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/toolboxes/difficulty.xml"))
class ChartEditorDifficultyToolbox extends ChartEditorBaseToolbox
{
var difficultyToolboxTree:TreeView;
var difficultyToolboxAddVariation:Button;
var difficultyToolboxAddDifficulty:Button;
var difficultyToolboxRemoveDifficulty:Button;
var difficultyToolboxSaveMetadata:Button;
var difficultyToolboxSaveChart:Button;
var difficultyToolboxLoadMetadata:Button;
var difficultyToolboxLoadChart:Button;
public function new(chartEditorState2:ChartEditorState)
{
super(chartEditorState2);
initialize();
this.onDialogClosed = onClose;
}
function onClose(event:UIEvent)
{
chartEditorState.menubarItemToggleToolboxDifficulty.selected = false;
}
function initialize():Void
{
// Starting position.
// TODO: Save and load this.
this.x = 150;
this.y = 250;
difficultyToolboxAddVariation.onClick = function(_:UIEvent) {
chartEditorState.openAddVariationDialog(true);
};
difficultyToolboxAddDifficulty.onClick = function(_:UIEvent) {
chartEditorState.openAddDifficultyDialog(true);
};
difficultyToolboxRemoveDifficulty.onClick = function(_:UIEvent) {
var currentVariation:String = chartEditorState.selectedVariation;
var currentDifficulty:String = chartEditorState.selectedDifficulty;
trace('Removing difficulty "$currentVariation:$currentDifficulty"');
var callback = (button) -> {
switch (button)
{
case DialogButton.YES:
// Remove the difficulty.
chartEditorState.removeDifficulty(currentVariation, currentDifficulty);
refresh();
case DialogButton.NO: // Do nothing.
default: // Do nothing.
}
}
Dialogs.messageBox("Are you sure? This cannot be undone.", "Remove Difficulty", MessageBoxType.TYPE_YESNO, callback);
};
difficultyToolboxSaveMetadata.onClick = function(_:UIEvent) {
var vari:String = chartEditorState.selectedVariation != Constants.DEFAULT_VARIATION ? '-${chartEditorState.selectedVariation}' : '';
FileUtil.writeFileReference('${chartEditorState.currentSongId}$vari-metadata.json', chartEditorState.currentSongMetadata.serialize());
};
difficultyToolboxSaveChart.onClick = function(_:UIEvent) {
var vari:String = chartEditorState.selectedVariation != Constants.DEFAULT_VARIATION ? '-${chartEditorState.selectedVariation}' : '';
FileUtil.writeFileReference('${chartEditorState.currentSongId}$vari-chart.json', chartEditorState.currentSongChartData.serialize());
};
difficultyToolboxLoadMetadata.onClick = function(_:UIEvent) {
// Replace metadata for current variation.
SongSerializer.importSongMetadataAsync(function(songMetadata) {
chartEditorState.currentSongMetadata = songMetadata;
});
};
difficultyToolboxLoadChart.onClick = function(_:UIEvent) {
// Replace chart data for current variation.
SongSerializer.importSongChartDataAsync(function(songChartData) {
chartEditorState.currentSongChartData = songChartData;
chartEditorState.noteDisplayDirty = true;
});
};
refresh();
}
/**
* Clear the tree view and rebuild it with the current song metadata (variation and difficulty list).
*/
public function updateTree():Void
{
// Clear the tree view so we can rebuild it.
difficultyToolboxTree.clearNodes();
// , icon: 'haxeui-core/styles/default/haxeui_tiny.png'
var treeSong:TreeViewNode = difficultyToolboxTree.addNode({id: 'stv_song', text: 'S: ${chartEditorState.currentSongName}'});
treeSong.expanded = true;
for (curVariation in chartEditorState.availableVariations)
{
var variationMetadata:Null<SongMetadata> = chartEditorState.songMetadata.get(curVariation);
if (variationMetadata == null) continue;
var treeVariation:TreeViewNode = treeSong.addNode(
{
id: 'stv_variation_$curVariation',
text: 'V: ${curVariation.toTitleCase()}'
});
treeVariation.expanded = true;
var difficultyList:Array<String> = variationMetadata.playData.difficulties;
for (difficulty in difficultyList)
{
var _treeDifficulty:TreeViewNode = treeVariation.addNode(
{
id: 'stv_difficulty_${curVariation}_$difficulty',
text: 'D: ${difficulty.toTitleCase()}'
});
}
}
difficultyToolboxTree.onChange = onTreeChange;
refreshTreeSelection();
}
/**
* Set the selected item in the tree to the current variation/difficulty.
*
* @param targetNode The node to select. If null, the current variation/difficulty will be used.
*/
public function refreshTreeSelection():Void
{
var targetNode = getCurrentTreeNode();
if (targetNode != null) difficultyToolboxTree.selectedNode = targetNode;
}
/**
* Get the node in the tree representing the current variation/difficulty.
*/
function getCurrentTreeNode():TreeViewNode
{
return
difficultyToolboxTree.findNodeByPath('stv_song/stv_variation_$chartEditorState.selectedVariation/stv_difficulty_${chartEditorState.selectedVariation}_$chartEditorState.selectedDifficulty',
'id');
}
/**
* Called when an item in the tree is selected. Updates the current variation/difficulty.
*/
function onTreeChange(event:UIEvent):Void
{
// Get the newly selected node.
var treeView:TreeView = cast event.target;
var targetNode:TreeViewNode = difficultyToolboxTree.selectedNode;
if (targetNode == null)
{
trace('No target node!');
// Reset the user's selection.
refreshTreeSelection();
return;
}
switch (targetNode.data.id.split('_')[1])
{
case 'difficulty':
var variation:String = targetNode.data.id.split('_')[2];
var difficulty:String = targetNode.data.id.split('_')[3];
if (variation != null && difficulty != null)
{
trace('Changing difficulty to "$variation:$difficulty"');
chartEditorState.selectedVariation = variation;
chartEditorState.selectedDifficulty = difficulty;
chartEditorState.refreshToolbox(ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT);
refreshTreeSelection();
}
// case 'song':
// case 'variation':
default:
// Reset the user's selection.
trace('Selected wrong node type, resetting selection.');
refreshTreeSelection();
chartEditorState.refreshToolbox(ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT);
}
}
public override function refresh():Void
{
super.refresh();
refreshTreeSelection();
}
public static function build(chartEditorState:ChartEditorState):ChartEditorDifficultyToolbox
{
return new ChartEditorDifficultyToolbox(chartEditorState);
}
}

View file

@ -2,7 +2,7 @@ package funkin.ui.debug.charting.toolboxes;
import funkin.play.character.BaseCharacter.CharacterType;
import funkin.play.character.CharacterData;
import funkin.play.stage.StageData;
import funkin.data.stage.StageData;
import funkin.play.event.SongEvent;
import funkin.data.event.SongEventSchema;
import funkin.ui.debug.charting.commands.ChangeStartingBPMCommand;

View file

@ -2,7 +2,8 @@ package funkin.ui.debug.charting.toolboxes;
import funkin.play.character.BaseCharacter.CharacterType;
import funkin.play.character.CharacterData;
import funkin.play.stage.StageData;
import funkin.data.stage.StageData;
import funkin.data.stage.StageRegistry;
import funkin.ui.debug.charting.commands.ChangeStartingBPMCommand;
import funkin.ui.debug.charting.util.ChartEditorDropdowns;
import haxe.ui.components.Button;
@ -13,6 +14,7 @@ import haxe.ui.components.Label;
import haxe.ui.components.NumberStepper;
import haxe.ui.components.Slider;
import haxe.ui.components.TextField;
import funkin.play.stage.Stage;
import haxe.ui.containers.Box;
import haxe.ui.containers.Frame;
import haxe.ui.events.UIEvent;
@ -148,7 +150,12 @@ class ChartEditorMetadataToolbox extends ChartEditorBaseToolbox
inputOffsetVocal.onChange = function(event:UIEvent) {
if (event.value == null) return;
chartEditorState.currentSongMetadata.offsets.setVocalOffset(chartEditorState.currentSongMetadata.playData.characters.player, event.value);
chartEditorState.currentVocalOffset = event.value;
if (chartEditorState.audioVocalTrackGroup != null)
{
chartEditorState.audioVocalTrackGroup.playerVoicesOffset = event.value;
chartEditorState.audioVocalTrackGroup.opponentVoicesOffset = event.value;
}
};
inputScrollSpeed.onChange = function(event:UIEvent) {
var valid:Bool = event.target.value != null && event.target.value > 0;
@ -189,6 +196,8 @@ class ChartEditorMetadataToolbox extends ChartEditorBaseToolbox
inputStage.value = chartEditorState.currentSongMetadata.playData.stage;
inputNoteStyle.value = chartEditorState.currentSongMetadata.playData.noteStyle;
inputBPM.value = chartEditorState.currentSongMetadata.timeChanges[0].bpm;
inputOffsetInst.value = chartEditorState.currentSongMetadata.offsets.getInstrumentalOffset();
inputOffsetVocal.value = chartEditorState.currentSongMetadata.offsets.getVocalOffset(chartEditorState.currentSongMetadata.playData.characters.player);
inputScrollSpeed.value = chartEditorState.currentSongChartScrollSpeed;
labelScrollSpeed.text = 'Scroll Speed: ${chartEditorState.currentSongChartScrollSpeed}x';
frameVariation.text = 'Variation: ${chartEditorState.selectedVariation.toTitleCase()}';
@ -199,11 +208,11 @@ class ChartEditorMetadataToolbox extends ChartEditorBaseToolbox
inputTimeSignature.value = {id: currentTimeSignature, text: currentTimeSignature};
var stageId:String = chartEditorState.currentSongMetadata.playData.stage;
var stageData:Null<StageData> = StageDataParser.parseStageData(stageId);
var stage:Null<Stage> = StageRegistry.instance.fetchEntry(stageId);
if (inputStage != null)
{
inputStage.value = (stageData != null) ?
{id: stageId, text: stageData.name} :
inputStage.value = (stage != null) ?
{id: stage.id, text: stage.stageName} :
{id: "mainStage", text: "Main Stage"};
}

View file

@ -2,10 +2,11 @@ package funkin.ui.debug.charting.util;
import funkin.data.notestyle.NoteStyleRegistry;
import funkin.play.notes.notestyle.NoteStyle;
import funkin.play.stage.StageData;
import funkin.play.stage.StageData.StageDataParser;
import funkin.data.stage.StageData;
import funkin.data.stage.StageRegistry;
import funkin.play.character.CharacterData;
import haxe.ui.components.DropDown;
import funkin.play.stage.Stage;
import funkin.play.character.BaseCharacter.CharacterType;
import funkin.play.character.CharacterData.CharacterDataParser;
@ -60,16 +61,16 @@ class ChartEditorDropdowns
{
dropDown.dataSource.clear();
var stageIds:Array<String> = StageDataParser.listStageIds();
var stageIds:Array<String> = StageRegistry.instance.listEntryIds();
var returnValue:DropDownEntry = {id: "mainStage", text: "Main Stage"};
for (stageId in stageIds)
{
var stage:Null<StageData> = StageDataParser.parseStageData(stageId);
var stage:Null<Stage> = StageRegistry.instance.fetchEntry(stageId);
if (stage == null) continue;
var value = {id: stageId, text: stage.name};
var value = {id: stage.id, text: stage.stageName};
if (startingStageId == stageId) returnValue = value;
dropDown.dataSource.add(value);

View file

@ -5,15 +5,17 @@ import flixel.input.mouse.FlxMouseEvent;
import flixel.math.FlxPoint;
import funkin.play.character.BaseCharacter;
import funkin.play.PlayState;
import funkin.play.stage.StageData;
import funkin.data.stage.StageData;
import funkin.play.stage.StageProp;
import funkin.graphics.shaders.StrokeShader;
import funkin.ui.haxeui.HaxeUISubState;
import funkin.ui.debug.stage.StageEditorCommand;
import funkin.util.SerializerUtil;
import funkin.data.stage.StageRegistry;
import funkin.util.MouseUtil;
import haxe.ui.containers.ListView;
import haxe.ui.core.Component;
import funkin.graphics.FunkinSprite;
import haxe.ui.events.UIEvent;
import haxe.ui.RuntimeComponentBuilder;
import openfl.events.Event;
@ -354,7 +356,13 @@ class StageOffsetSubState extends HaxeUISubState
function prepStageStuff():String
{
var stageLol:StageData = StageDataParser.parseStageData(PlayState.instance.currentStageId);
var stageLol:StageData = StageRegistry.instance.fetchEntry(PlayState.instance.currentStageId)?._data;
if (stageLol == null)
{
FlxG.log.error("Stage not found in registry!");
return "";
}
for (prop in stageLol.props)
{
@ -378,6 +386,6 @@ class StageOffsetSubState extends HaxeUISubState
stageLol.characters.gf.position[0] = Std.int(GF_FEET_SNIIIIIIIIIIIIIFFFF.x);
stageLol.characters.gf.position[1] = Std.int(GF_FEET_SNIIIIIIIIIIIIIFFFF.y);
return SerializerUtil.toJSON(stageLol);
return stageLol.serialize();
}
}

View file

@ -1,6 +1,5 @@
package funkin.ui.freeplay;
import funkin.input.Controls;
import flash.text.TextField;
import flixel.addons.display.FlxGridOverlay;
import flixel.addons.transition.FlxTransitionableState;
@ -23,33 +22,35 @@ import flixel.tweens.FlxTween;
import flixel.util.FlxColor;
import flixel.util.FlxSpriteUtil;
import flixel.util.FlxTimer;
import funkin.input.Controls.Control;
import funkin.data.level.LevelRegistry;
import funkin.data.song.SongRegistry;
import funkin.graphics.adobeanimate.FlxAtlasSprite;
import funkin.graphics.shaders.AngleMask;
import funkin.graphics.shaders.HSVShader;
import funkin.graphics.shaders.PureColor;
import funkin.util.MathUtil;
import funkin.graphics.shaders.StrokeShader;
import funkin.input.Controls;
import funkin.input.Controls.Control;
import funkin.play.components.HealthIcon;
import funkin.play.PlayState;
import funkin.play.PlayStatePlaylist;
import funkin.play.song.Song;
import funkin.save.Save;
import funkin.save.Save.SaveScoreData;
import funkin.ui.AtlasText;
import funkin.ui.freeplay.BGScrollingText;
import funkin.ui.freeplay.DifficultyStars;
import funkin.ui.freeplay.DJBoyfriend;
import funkin.ui.freeplay.FreeplayScore;
import funkin.ui.freeplay.LetterSort;
import funkin.ui.freeplay.SongMenuItem;
import funkin.ui.mainmenu.MainMenuState;
import funkin.ui.MusicBeatState;
import funkin.ui.MusicBeatSubState;
import funkin.ui.mainmenu.MainMenuState;
import funkin.ui.transition.LoadingState;
import funkin.ui.transition.StickerSubState;
import funkin.util.MathUtil;
import funkin.util.MathUtil;
import lime.app.Future;
import lime.utils.Assets;
@ -64,7 +65,7 @@ class FreeplayState extends MusicBeatSubState
var currentDifficulty:String = Constants.DEFAULT_DIFFICULTY;
var fp:FreeplayScore;
var txtCompletion:FlxText;
var txtCompletion:AtlasText;
var lerpCompletion:Float = 0;
var intendedCompletion:Float = 0;
var lerpScore:Float = 0;
@ -87,6 +88,8 @@ class FreeplayState extends MusicBeatSubState
var grpCapsules:FlxTypedGroup<SongMenuItem>;
var curCapsule:SongMenuItem;
var curPlaying:Bool = false;
var ostName:FlxText;
var difficultyStars:DifficultyStars;
var dj:DJBoyfriend;
@ -150,15 +153,10 @@ class FreeplayState extends MusicBeatSubState
for (songId in LevelRegistry.instance.parseEntryData(levelId).songs)
{
var song:Song = SongRegistry.instance.fetchEntry(songId);
var songBaseDifficulty:SongDifficulty = song.getDifficulty(Constants.DEFAULT_DIFFICULTY);
var songName = songBaseDifficulty.songName;
var songOpponent = songBaseDifficulty.characters.opponent;
var songDifficulties = song.listDifficulties();
songs.push(new FreeplaySongData(levelId, songId, song));
songs.push(new FreeplaySongData(songId, songName, levelId, songOpponent, songDifficulties));
for (difficulty in songDifficulties)
for (difficulty in song.listDifficulties())
{
diffIdsTotal.pushUnique(difficulty);
}
@ -334,6 +332,8 @@ class FreeplayState extends MusicBeatSubState
if (diffSprite.difficultyId == currentDifficulty) diffSprite.visible = true;
}
// NOTE: This is an AtlasSprite because we use an animation to bring it into view.
// TODO: Add the ability to select the album graphic.
var albumArt:FlxAtlasSprite = new FlxAtlasSprite(640, 360, Paths.animateAtlas("freeplay/albumRoll"));
albumArt.visible = false;
add(albumArt);
@ -347,7 +347,7 @@ class FreeplayState extends MusicBeatSubState
var albumTitle:FlxSprite = new FlxSprite(947, 491).loadGraphic(Paths.image('freeplay/albumTitle-fnfvol1'));
var albumArtist:FlxSprite = new FlxSprite(1010, 607).loadGraphic(Paths.image('freeplay/albumArtist-kawaisprite'));
var difficultyStars:DifficultyStars = new DifficultyStars(140, 39);
difficultyStars = new DifficultyStars(140, 39);
difficultyStars.stars.visible = false;
albumTitle.visible = false;
@ -382,11 +382,16 @@ class FreeplayState extends MusicBeatSubState
add(overhangStuff);
FlxTween.tween(overhangStuff, {y: 0}, 0.3, {ease: FlxEase.quartOut});
var fnfFreeplay:FlxText = new FlxText(0, 12, 0, "FREEPLAY", 48);
var fnfFreeplay:FlxText = new FlxText(8, 8, 0, "FREEPLAY", 48);
fnfFreeplay.font = "VCR OSD Mono";
fnfFreeplay.visible = false;
exitMovers.set([overhangStuff, fnfFreeplay],
ostName = new FlxText(8, 8, FlxG.width - 8 - 8, "OFFICIAL OST", 48);
ostName.font = "VCR OSD Mono";
ostName.alignment = RIGHT;
ostName.visible = false;
exitMovers.set([overhangStuff, fnfFreeplay, ostName],
{
y: -overhangStuff.height,
x: 0,
@ -397,8 +402,9 @@ class FreeplayState extends MusicBeatSubState
var sillyStroke = new StrokeShader(0xFFFFFFFF, 2, 2);
fnfFreeplay.shader = sillyStroke;
add(fnfFreeplay);
add(ostName);
var fnfHighscoreSpr:FlxSprite = new FlxSprite(890, 70);
var fnfHighscoreSpr:FlxSprite = new FlxSprite(860, 70);
fnfHighscoreSpr.frames = Paths.getSparrowAtlas('freeplay/highscore');
fnfHighscoreSpr.animation.addByPrefix("highscore", "highscore", 24, false);
fnfHighscoreSpr.visible = false;
@ -415,8 +421,10 @@ class FreeplayState extends MusicBeatSubState
fp.visible = false;
add(fp);
txtCompletion = new FlxText(1200, 77, 0, "0", 32);
txtCompletion.font = "VCR OSD Mono";
var clearBoxSprite:FlxSprite = new FlxSprite(1165, 65).loadGraphic(Paths.image('freeplay/clearBox'));
add(clearBoxSprite);
txtCompletion = new AtlasText(1185, 87, "69", AtlasFont.FREEPLAY_CLEAR);
txtCompletion.visible = false;
add(txtCompletion);
@ -485,6 +493,7 @@ class FreeplayState extends MusicBeatSubState
new FlxTimer().start(1 / 24, function(handShit) {
fnfHighscoreSpr.visible = true;
fnfFreeplay.visible = true;
ostName.visible = true;
fp.visible = true;
fp.updateScore(0);
@ -674,9 +683,32 @@ class FreeplayState extends MusicBeatSubState
lerpScore = MathUtil.coolLerp(lerpScore, intendedScore, 0.2);
lerpCompletion = MathUtil.coolLerp(lerpCompletion, intendedCompletion, 0.9);
if (Math.isNaN(lerpScore))
{
lerpScore = intendedScore;
}
if (Math.isNaN(lerpCompletion))
{
lerpCompletion = intendedCompletion;
}
fp.updateScore(Std.int(lerpScore));
txtCompletion.text = Math.floor(lerpCompletion * 100) + "%";
txtCompletion.text = '${Math.floor(lerpCompletion * 100)}';
// Right align the completion percentage
switch (txtCompletion.text.length)
{
case 3:
txtCompletion.x = 1185 - 10;
case 2:
txtCompletion.x = 1185;
case 1:
txtCompletion.x = 1185 + 24;
default:
txtCompletion.x = 1185;
}
handleInputs(elapsed);
}
@ -913,6 +945,11 @@ class FreeplayState extends MusicBeatSubState
intendedCompletion = 0.0;
}
if (intendedCompletion == Math.POSITIVE_INFINITY || intendedCompletion == Math.NEGATIVE_INFINITY || Math.isNaN(intendedCompletion))
{
intendedCompletion = 0;
}
grpDifficulties.group.forEach(function(diffSprite) {
diffSprite.visible = false;
});
@ -938,6 +975,27 @@ class FreeplayState extends MusicBeatSubState
}
}
}
if (change != 0)
{
// Update the song capsules to reflect the new difficulty info.
for (songCapsule in grpCapsules.members)
{
if (songCapsule == null) continue;
if (songCapsule.songData != null)
{
songCapsule.songData.currentDifficulty = currentDifficulty;
songCapsule.init(null, null, songCapsule.songData);
}
else
{
songCapsule.init(null, null, null);
}
}
}
// Set the difficulty star count on the right.
difficultyStars.difficulty = daSong?.songRating ?? difficultyStars.difficulty; // yay haxe 4.3
}
// Clears the cache of songs, frees up memory, they' ll have to be loaded in later tho function clearDaCache(actualSongTho:String)
@ -1046,6 +1104,10 @@ class FreeplayState extends MusicBeatSubState
{
currentDifficulty = rememberedDifficulty;
}
// Set the difficulty star count on the right.
var daSong = songs[curSelected];
difficultyStars.difficulty = daSong?.songRating ?? 0;
}
function changeSelection(change:Int = 0)
@ -1176,19 +1238,47 @@ class FreeplaySongData
{
public var isFav:Bool = false;
public var songId:String = "";
public var songName:String = "";
public var levelId:String = "";
public var songCharacter:String = "";
public var songDifficulties:Array<String> = [];
var song:Song;
public function new(songId:String, songName:String, levelId:String, songCharacter:String, songDifficulties:Array<String>)
public var levelId(default, null):String = "";
public var songId(default, null):String = "";
public var songDifficulties(default, null):Array<String> = [];
public var songName(default, null):String = "";
public var songCharacter(default, null):String = "";
public var songRating(default, null):Int = 0;
public var currentDifficulty(default, set):String = Constants.DEFAULT_DIFFICULTY;
function set_currentDifficulty(value:String):String
{
if (currentDifficulty == value) return value;
currentDifficulty = value;
updateValues();
return value;
}
public function new(levelId:String, songId:String, song:Song)
{
this.songId = songId;
this.songName = songName;
this.levelId = levelId;
this.songCharacter = songCharacter;
this.songDifficulties = songDifficulties;
this.songId = songId;
this.song = song;
updateValues();
}
function updateValues():Void
{
this.songDifficulties = song.listDifficulties();
if (!this.songDifficulties.contains(currentDifficulty)) currentDifficulty = Constants.DEFAULT_DIFFICULTY;
var songDifficulty:SongDifficulty = song.getDifficulty(currentDifficulty);
if (songDifficulty == null) return;
this.songName = songDifficulty.songName;
this.songCharacter = songDifficulty.characters.opponent;
this.songRating = songDifficulty.difficultyRating;
}
}

View file

@ -35,11 +35,6 @@ class SongMenuItem extends FlxSpriteGroup
var ranks:Array<String> = ["fail", "average", "great", "excellent", "perfect"];
// lol...
var diffRanks:Array<String> = [
"00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12", "14", "15"
];
public var targetPos:FlxPoint = new FlxPoint();
public var doLerp:Bool = false;
public var doJumpIn:Bool = false;
@ -47,10 +42,12 @@ class SongMenuItem extends FlxSpriteGroup
public var doJumpOut:Bool = false;
public var onConfirm:Void->Void;
public var diffGrayscale:Grayscale;
public var grayscaleShader:Grayscale;
public var hsvShader(default, set):HSVShader;
var diffRatingSprite:FlxSprite;
public function new(x:Float, y:Float)
{
super(x, y);
@ -75,26 +72,30 @@ class SongMenuItem extends FlxSpriteGroup
add(ranking);
grpHide.add(ranking);
diffGrayscale = new Grayscale(1);
var diffRank = new FlxSprite(145, 90).loadGraphic(Paths.image("freeplay/diffRankings/diff" + FlxG.random.getObject(diffRanks)));
diffRank.shader = diffGrayscale;
diffRank.visible = false;
add(diffRank);
diffRank.origin.set(capsule.origin.x - diffRank.x, capsule.origin.y - diffRank.y);
grpHide.add(diffRank);
switch (rank)
{
case "perfect":
ranking.x -= 10;
}
grayscaleShader = new Grayscale(1);
diffRatingSprite = new FlxSprite(145, 90).loadGraphic(Paths.image("freeplay/diffRatings/diff00"));
diffRatingSprite.shader = grayscaleShader;
diffRatingSprite.visible = false;
add(diffRatingSprite);
diffRatingSprite.origin.set(capsule.origin.x - diffRatingSprite.x, capsule.origin.y - diffRatingSprite.y);
grpHide.add(diffRatingSprite);
songText = new CapsuleText(capsule.width * 0.26, 45, 'Random', Std.int(40 * realScaled));
add(songText);
grpHide.add(songText);
pixelIcon = new FlxSprite(155, 15);
// TODO: Use value from metadata instead of random.
updateDifficultyRating(FlxG.random.int(0, 15));
pixelIcon = new FlxSprite(160, 35);
pixelIcon.makeGraphic(32, 32, 0x00000000);
pixelIcon.antialiasing = false;
pixelIcon.active = false;
@ -113,6 +114,12 @@ class SongMenuItem extends FlxSpriteGroup
setVisibleGrp(false);
}
function updateDifficultyRating(newRating:Int)
{
var ratingPadded:String = newRating < 10 ? '0$newRating' : '$newRating';
diffRatingSprite.loadGraphic(Paths.image('freeplay/diffRatings/diff${ratingPadded}'));
}
function set_hsvShader(value:HSVShader):HSVShader
{
this.hsvShader = value;
@ -149,16 +156,17 @@ class SongMenuItem extends FlxSpriteGroup
updateSelected();
}
public function init(x:Float, y:Float, songData:Null<FreeplaySongData>)
public function init(?x:Float, ?y:Float, songData:Null<FreeplaySongData>)
{
this.x = x;
this.y = y;
if (x != null) this.x = x;
if (y != null) this.y = y;
this.songData = songData;
// Update capsule text.
songText.text = songData?.songName ?? 'Random';
// Update capsule character.
if (songData?.songCharacter != null) setCharacter(songData.songCharacter);
updateDifficultyRating(songData?.songRating ?? 0);
// Update opacity, offsets, etc.
updateSelected();
}
@ -200,7 +208,14 @@ class SongMenuItem extends FlxSpriteGroup
pixelIcon.loadGraphic(Paths.image(charPath));
pixelIcon.scale.x = pixelIcon.scale.y = 2;
pixelIcon.origin.x = 100;
switch (char)
{
case "parents-christmas":
pixelIcon.origin.x = 140;
default:
pixelIcon.origin.x = 100;
}
// pixelIcon.origin.x = capsule.origin.x;
// pixelIcon.offset.x -= pixelIcon.origin.x;
}
@ -336,7 +351,7 @@ class SongMenuItem extends FlxSpriteGroup
function updateSelected():Void
{
diffGrayscale.setAmount(this.selected ? 0 : 0.8);
grayscaleShader.setAmount(this.selected ? 0 : 0.8);
songText.alpha = this.selected ? 1 : 0.6;
songText.blurredText.visible = this.selected ? true : false;
capsule.offset.x = this.selected ? 0 : -5;

View file

@ -8,6 +8,7 @@ import flixel.group.FlxGroup;
import flixel.input.actions.FlxActionInput;
import flixel.input.gamepad.FlxGamepadInputID;
import flixel.input.keyboard.FlxKey;
import funkin.graphics.FunkinSprite;
import funkin.input.Controls;
import funkin.ui.AtlasText;
import funkin.ui.MenuList;
@ -61,8 +62,8 @@ class ControlsMenu extends funkin.ui.options.OptionsState.Page
if (FlxG.gamepads.numActiveGamepads > 0)
{
var devicesBg:FlxSprite = new FlxSprite();
devicesBg.makeGraphic(FlxG.width, 100, 0xFFFAFD6D);
var devicesBg:FunkinSprite = new FunkinSprite();
devicesBg.makeSolidColor(FlxG.width, 100, 0xFFFAFD6D);
add(devicesBg);
deviceList = new TextMenuList(Horizontal, None);
add(deviceList);

View file

@ -180,9 +180,9 @@ class Level implements IRegistryEntry<LevelData>
return difficulties;
}
public function buildProps():Array<LevelProp>
public function buildProps(?existingProps:Array<LevelProp>):Array<LevelProp>
{
var props:Array<LevelProp> = [];
var props:Array<LevelProp> = existingProps == null ? [] : [for (x in existingProps) x];
if (_data.props.length == 0) return props;
@ -190,11 +190,22 @@ class Level implements IRegistryEntry<LevelData>
{
var propData = _data.props[propIndex];
var propSprite:Null<LevelProp> = LevelProp.build(propData);
if (propSprite == null) continue;
// Attempt to reuse the `LevelProp` object.
// This prevents animations from resetting.
var existingProp:Null<LevelProp> = props[propIndex];
if (existingProp != null)
{
existingProp.propData = propData;
existingProp.x = propData.offsets[0] + FlxG.width * 0.25 * propIndex;
}
else
{
var propSprite:Null<LevelProp> = LevelProp.build(propData);
if (propSprite == null) continue;
propSprite.x += FlxG.width * 0.25 * propIndex;
props.push(propSprite);
propSprite.x = propData.offsets[0] + FlxG.width * 0.25 * propIndex;
props.push(propSprite);
}
}
return props;

View file

@ -6,9 +6,26 @@ import funkin.data.level.LevelData;
class LevelProp extends Bopper
{
public function new(danceEvery:Int)
public var propData(default, set):Null<LevelPropData> = null;
function set_propData(value:LevelPropData):LevelPropData
{
super(danceEvery);
// Only reset the prop if the asset path has changed.
if (propData == null || value.assetPath != this.propData.assetPath)
{
this.visible = (value != null);
this.propData = value;
danceEvery = this.propData.danceEvery;
applyData();
}
return this.propData;
}
public function new(propData:LevelPropData)
{
super(propData.danceEvery);
this.propData = propData;
}
public function playConfirm():Void
@ -16,50 +33,51 @@ class LevelProp extends Bopper
playAnimation('confirm', true, true);
}
public static function build(propData:Null<LevelPropData>):Null<LevelProp>
function applyData():Void
{
if (propData == null) return null;
var isAnimated:Bool = propData.animations.length > 0;
var prop:LevelProp = new LevelProp(propData.danceEvery);
if (isAnimated)
{
// Initalize sprite frames.
// Sparrow atlas only LEL.
prop.frames = Paths.getSparrowAtlas(propData.assetPath);
this.frames = Paths.getSparrowAtlas(propData.assetPath);
}
else
{
// Initalize static sprite.
prop.loadGraphic(Paths.image(propData.assetPath));
this.loadGraphic(Paths.image(propData.assetPath));
// Disables calls to update() for a performance boost.
prop.active = false;
this.active = false;
}
if (prop.frames == null || prop.frames.numFrames == 0)
if (this.frames == null || this.frames.numFrames == 0)
{
trace('ERROR: Could not build texture for level prop (${propData.assetPath}).');
return null;
return;
}
var scale:Float = propData.scale * (propData.isPixel ? 6 : 1);
prop.scale.set(scale, scale);
prop.antialiasing = !propData.isPixel;
prop.alpha = propData.alpha;
prop.x = propData.offsets[0];
prop.y = propData.offsets[1];
this.scale.set(scale, scale);
this.antialiasing = !propData.isPixel;
this.alpha = propData.alpha;
this.x = propData.offsets[0];
this.y = propData.offsets[1];
FlxAnimationUtil.addAtlasAnimations(prop, propData.animations);
FlxAnimationUtil.addAtlasAnimations(this, propData.animations);
for (propAnim in propData.animations)
{
prop.setAnimationOffsets(propAnim.name, propAnim.offsets[0], propAnim.offsets[1]);
this.setAnimationOffsets(propAnim.name, propAnim.offsets[0], propAnim.offsets[1]);
}
prop.dance();
prop.animation.paused = true;
this.dance();
this.animation.paused = true;
}
return prop;
public static function build(propData:Null<LevelPropData>):Null<LevelProp>
{
if (propData == null) return null;
return new LevelProp(propData);
}
}

View file

@ -10,6 +10,7 @@ import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.text.FlxText;
import flixel.addons.transition.FlxTransitionableState;
import flixel.tweens.FlxEase;
import funkin.graphics.FunkinSprite;
import funkin.ui.MusicBeatState;
import flixel.tweens.FlxTween;
import flixel.util.FlxColor;
@ -153,7 +154,7 @@ class StoryMenuState extends MusicBeatState
updateBackground();
var black:FlxSprite = new FlxSprite(levelBackground.x, 0).makeGraphic(FlxG.width, Std.int(400 + levelBackground.y), FlxColor.BLACK);
var black:FunkinSprite = new FunkinSprite(levelBackground.x, 0).makeSolidColor(FlxG.width, Std.int(400 + levelBackground.y), FlxColor.BLACK);
black.zIndex = levelBackground.zIndex - 1;
add(black);
@ -636,8 +637,7 @@ class StoryMenuState extends MusicBeatState
function updateProps():Void
{
levelProps.clear();
for (prop in currentLevel.buildProps())
for (prop in currentLevel.buildProps(levelProps.members))
{
prop.zIndex = 1000;
levelProps.add(prop);

View file

@ -15,7 +15,7 @@ class OutdatedSubState extends MusicBeatState
override function create()
{
super.create();
var bg:FlxSprite = new FlxSprite().makeGraphic(FlxG.width, FlxG.height, FlxColor.BLACK);
var bg:FunkinSprite = new FunkinSprite().makeSolidColor(FlxG.width, FlxG.height, FlxColor.BLACK);
add(bg);
var ver = "v" + Application.current.meta.get('version');
var txt:FlxText = new FlxText(0, 0, FlxG.width,

View file

@ -13,6 +13,7 @@ import funkin.audio.visualize.SpectogramSprite;
import funkin.graphics.shaders.ColorSwap;
import funkin.graphics.shaders.LeftMaskShader;
import funkin.data.song.SongRegistry;
import funkin.graphics.FunkinSprite;
import funkin.ui.MusicBeatState;
import funkin.data.song.SongData.SongMusicData;
import funkin.graphics.shaders.TitleOutline;
@ -118,7 +119,8 @@ class TitleState extends MusicBeatState
persistentUpdate = true;
var bg:FlxSprite = new FlxSprite().makeGraphic(FlxG.width, FlxG.height, FlxColor.BLACK);
var bg:FunkinSprite = new FunkinSprite().makeSolidColor(FlxG.width, FlxG.height, FlxColor.BLACK);
bg.screenCenter();
add(bg);
logoBl = new FlxSprite(-150, -100);

View file

@ -13,6 +13,7 @@ import funkin.play.song.Song.SongDifficulty;
import funkin.ui.mainmenu.MainMenuState;
import funkin.ui.MusicBeatState;
import haxe.io.Path;
import funkin.graphics.FunkinSprite;
import lime.app.Future;
import lime.app.Promise;
import lime.utils.AssetLibrary;
@ -42,7 +43,7 @@ class LoadingState extends MusicBeatState
override function create():Void
{
var bg:FlxSprite = new FlxSprite().makeGraphic(FlxG.width, FlxG.height, 0xFFcaff4d);
var bg:FlxSprite = new FunkinSprite().makeSolidColor(FlxG.width, FlxG.height, 0xFFcaff4d);
add(bg);
funkay = new FlxSprite();
@ -53,7 +54,7 @@ class LoadingState extends MusicBeatState
funkay.scrollFactor.set();
funkay.screenCenter();
loadBar = new FlxSprite(0, FlxG.height - 20).makeGraphic(FlxG.width, 10, 0xFFff16d2);
loadBar = new FunkinSprite(0, FlxG.height - 20).makeSolidColor(FlxG.width, 10, 0xFFff16d2);
loadBar.screenCenter(X);
add(loadBar);

View file

@ -70,7 +70,7 @@ class Constants
public static final URL_KICKSTARTER:String = 'https://www.kickstarter.com/projects/funkin/friday-night-funkin-the-full-ass-game/';
/**
* GIT REPO DATA
* REPOSITORY DATA
*/
// ==============================
@ -86,6 +86,11 @@ class Constants
public static final GIT_HASH:String = funkin.util.macro.GitCommit.getGitCommitHash();
#end
/**
* The current library versions, as provided by hmm.
*/
public static final LIBRARY_VERSIONS:Array<String> = funkin.util.macro.HaxelibVersions.getLibraryVersions();
/**
* COLORS
*/
@ -358,6 +363,12 @@ class Constants
*/
public static final SCORE_HOLD_BONUS_PER_SECOND:Float = 250.0;
public static final JUDGEMENT_KILLER_COMBO_BREAK:Bool = false;
public static final JUDGEMENT_SICK_COMBO_BREAK:Bool = false;
public static final JUDGEMENT_GOOD_COMBO_BREAK:Bool = false;
public static final JUDGEMENT_BAD_COMBO_BREAK:Bool = true;
public static final JUDGEMENT_SHIT_COMBO_BREAK:Bool = true;
/**
* FILE EXTENSIONS
*/

View file

@ -0,0 +1,17 @@
package funkin.util;
import haxe.ui.tooltips.ToolTipRegionOptions;
class HaxeUIUtil
{
public static function buildTooltip(text:String, ?left:Float, ?top:Float, ?width:Float, ?height:Float):ToolTipRegionOptions
{
return {
tipData: {text: text},
left: left ?? 0.0,
top: top ?? 0.0,
width: width ?? 0.0,
height: height ?? 0.0
}
}
}

View file

@ -123,6 +123,17 @@ class CrashHandler
fullContents += '=====================\n';
fullContents += 'Haxelibs: \n';
for (lib in Constants.LIBRARY_VERSIONS)
{
fullContents += '- ${lib}\n';
}
fullContents += '\n';
fullContents += '=====================\n';
fullContents += '\n';
fullContents += message;

View file

@ -0,0 +1,67 @@
package funkin.util.macro;
import haxe.io.Path;
class HaxelibVersions
{
public static macro function getLibraryVersions():haxe.macro.Expr.ExprOf<Array<String>>
{
#if !display
return macro $v{formatHmmData(readHmmData())};
#else
// `#if display` is used for code completion. In this case returning an
// empty string is good enough; We don't want to call functions on every hint.
var commitHash:Array<String> = [];
return macro $v{commitHash};
#end
}
#if (macro)
static function readHmmData():hmm.HmmConfig
{
return hmm.HmmConfig.HmmConfigs.readHmmJsonOrThrow();
}
static function formatHmmData(hmmData:hmm.HmmConfig):Array<String>
{
var result:Array<String> = [];
for (library in hmmData.dependencies)
{
switch (library)
{
case Haxelib(name, version):
result.push('${name} haxelib(${o(version)})');
case Git(name, url, ref, dir):
result.push('${name} git(${url}/${o(dir, '')}:${o(ref)})');
case Mercurial(name, url, ref, dir):
result.push('${name} mercurial(${url}/${o(dir, '')}:${o(ref)})');
case Dev(name, path):
result.push('${name} dev(${path})');
}
}
return result;
}
static function o(option:haxe.ds.Option<String>, defaultValue:String = 'None'):String
{
switch (option)
{
case Some(value):
return value;
case None:
return defaultValue;
}
}
static function readLibraryCurrentVersion(libraryName:String):String
{
var path = Path.join([Path.addTrailingSlash(Sys.getCwd()), '.haxelib', libraryName, '.current']);
// This is compile time so we should always have Sys available.
var result = sys.io.File.getContent(path);
return result;
}
#end
}

View file

@ -1,32 +1,42 @@
package funkin.util.tools;
import haxe.Int64;
/**
* @see https://github.com/fponticelli/thx.core/blob/master/src/thx/Int64s.hx
* Why `haxe.Int64` doesn't have a built-in `toFloat` function is beyond me.
*/
class Int64Tools
{
static var min = haxe.Int64.make(0x80000000, 0);
static var one = haxe.Int64.make(0, 1);
static var two = haxe.Int64.ofInt(2);
static var zero = haxe.Int64.make(0, 0);
static var ten = haxe.Int64.ofInt(10);
private inline static var MAX_32_PRECISION:Float = 4294967296.0;
public static function toFloat(i:haxe.Int64):Float
public static function fromFloat(f:Float):Int64
{
var isNegative = false;
if (i < 0)
var h = Std.int(f / MAX_32_PRECISION);
var l = Std.int(f);
return Int64.make(h, l);
}
public static function toFloat(i:Int64):Float
{
var f:Float = Int64.getLow(i);
if (f < 0) f += MAX_32_PRECISION;
return (Int64.getHigh(i) * MAX_32_PRECISION + f);
}
public static function isToIntSafe(i:Int64):Bool
{
return i.high != i.low >> 31;
}
public static function toIntSafe(i:Int64):Int
{
try
{
if (i < min) return -9223372036854775808.0; // most -ve value can't be made +ve
isNegative = true;
i = -i;
return Int64.toInt(i);
}
var multiplier = 1.0, ret = 0.0;
for (_ in 0...64)
catch (e:Dynamic)
{
if (haxe.Int64.and(i, one) != zero) ret += multiplier;
multiplier *= 2.0;
i = haxe.Int64.shr(i, 1);
throw 'Could not represent value "${Int64.toStr(i)}" as an integer.';
}
return (isNegative ? -1 : 1) * ret;
}
}