1
0
Fork 0
mirror of https://github.com/ninjamuffin99/Funkin.git synced 2025-12-08 04:58:48 +00:00

Compare commits

...

59 commits

Author SHA1 Message Date
Hyper_ 0dcabdf54b refactor: Simplify finite check 2025-12-07 16:21:04 -03:00
Hyper_ 9abb0c9dea Fix incorrect NaN checks 2025-12-07 16:21:04 -03:00
AbnormalPoof 758f712eb5 Add global offsets for Pico and Nene's pixel variants 2025-12-07 01:40:24 -07:00
Furo 01029817c0 Added funnyColor backwards compatibility 2025-12-07 01:40:20 -07:00
Furo 8a31e12d10 Change BGScrollingText to a single FlxText
BGScrollingText is now a single FlxText drawn multiple time in a single frame which is more optimized that a FlxSpriteGroup with multiple FlxSprites taking the text's graphic
2025-12-07 01:40:20 -07:00
EliteMasterEric 90ab04caa1 Tweak vertical offset on the measure numbers. 2025-12-07 01:40:20 -07:00
EliteMasterEric ba7f89b9a2 Redo the backing color of the measure ticks. 2025-12-07 01:40:20 -07:00
EliteMasterEric ee36cbbbcf Rewrite measure ticks to not get redrawn EVERY FRAME GODAM 2025-12-07 01:40:20 -07:00
Hyper_ d74b8fb4ca fix: Cursor not properly fading out when exiting CharSelect with backspace 2025-12-07 01:40:20 -07:00
Cameron Taylor 8fa622e147 remove unused pitch stuff in CharSelect 2025-12-07 01:40:20 -07:00
Cameron Taylor a12950d5a7 fix char select inputs for keyb and controllers 2025-12-07 01:40:20 -07:00
EliteMasterEric 71bc847698 Clean up the Lime target config dropdown by removing unused targets like AIR and Flash. 2025-12-07 01:39:25 -07:00
AbnormalPoof a560bf51a9 Re-export Pico's PERFECT rank animation with BTA 2025-12-07 01:39:25 -07:00
AbnormalPoof 866e5aa008 Fix the cursor lerping from the top left in character select 2025-12-07 01:39:22 -07:00
AbnormalPoof 3c213ad45c Add getFramesWithKeyword() 2025-12-07 01:39:22 -07:00
anysad a0b933ce8f Remove unused Chart Editor Context Menu code 2025-12-07 01:39:22 -07:00
MAJigsaw77 671e1435d2 Bump hxvlc to 2.2.5. 2025-11-30 19:20:23 -07:00
MAJigsaw77 369dad3951 Remove the yellow notice. 2025-11-30 19:20:23 -07:00
MAJigsaw77 8ecde809e6 Improve library version checks. 2025-11-30 19:20:23 -07:00
AbnormalPoof c4394f0d12 Lower him a little 2025-11-30 19:19:54 -07:00
AbnormalPoof 63cbaef4c4 Re-export Boyfriend (Car) to texture atlas 2025-11-30 19:19:51 -07:00
AbnormalPoof 4120b20a24 Revert "remove keyboard "scheme" related code, completely unused"
This reverts commit 7c8b7eee578b4c5caabf194d645b1473f233dd46.
2025-11-30 19:19:47 -07:00
EliteMasterEric a40972926d Update HXCPP to support building with C++20 2025-11-30 19:19:47 -07:00
Cameron Taylor 368531f6fa update our custom FunkinAction check() function with newer logic from FlxAction 2025-11-29 04:51:05 -07:00
Cameron Taylor b9d4ce70cc remove the unused _P and _R stuff from the Action enum abstract 2025-11-29 04:51:05 -07:00
Cameron Taylor 6ef3fe4faf more controls.hx cleanup, remove a few unused functions, and add Void return types to make checkstyle happier 2025-11-29 04:51:04 -07:00
Cameron Taylor 8abc9ab306 remove keyboard "scheme" related code, completely unused 2025-11-29 04:51:04 -07:00
Cameron Taylor 8349999833 remove more unused vars in controls.hx 2025-11-29 04:51:04 -07:00
AbnormalPoof cf52bcdf65 Adjust Darnell's idle animation 2025-11-29 04:50:41 -07:00
AbnormalPoof 4e03cf0a5d Re-export THE CHUDDER!!!! 2025-11-29 04:50:39 -07:00
Kolo 8952acae30 better backwards compatibility for FlxAtlasSprite -> FunkinSprite 2025-11-29 00:59:14 -07:00
Cameron Taylor 16c685ac98 use FlxMath.wrap() to make this cursor wrapping cleaner in CharSelectSubState 2025-11-28 18:45:23 -07:00
Cameron Taylor 5472bf691f clean up spamDirection code, use FlxDirectionFlags 2025-11-28 18:45:23 -07:00
MoonDroid 6a71f95cf9 fix: Make lerping consistent across different FPS 2025-11-28 18:45:23 -07:00
AbnormalPoof 48f3e984f4 Fix the cigarette not appearing in the Pico doppelganger cutscene 2025-11-28 18:43:46 -07:00
Cameron Taylor b125a49410 move character select cursor sprite code into it's own class 2025-11-28 18:43:44 -07:00
VirtuGuy de19d65ad6 Fix ghost tapping behavior when unpausing 2025-11-28 18:43:44 -07:00
Cameron Taylor 6334215be4 early return for Chart Editor handleCursor() 2025-11-28 18:43:44 -07:00
EliteMasterEric a3b7891b42 Fix a bug where cycling difficulties wasn't calculated properly when switching variations. 2025-11-28 18:43:44 -07:00
EliteMasterEric 53c62e219b Switching difficulty now forces the selection to be cleared. 2025-11-28 18:43:44 -07:00
EliteMasterEric adb96897dc Add difficulty switches to the undo/redo history. 2025-11-28 18:43:44 -07:00
Hyper_ 9a77539664 Add hardware keyboard detection on Android 2025-11-28 18:43:44 -07:00
MoonDroid 0d91d68300 Update lime ref 2025-11-28 18:43:44 -07:00
EliteMasterEric d49fd76bd1 Implemented Clone Difficulty button in the Difficulty toolbox.
Fixes to Clone Difficulty dialog, and added Move Difficulty dialog.

assets submod
2025-11-28 18:43:44 -07:00
Kolo 329ec524c0 fix starting a playtest opening pause menu 2025-11-27 13:21:18 -07:00
AbnormalPoof 3f89464608 Make Monster's singLEFT-hold offset consistent with singLEFT 2025-11-26 13:51:19 -06:00
Hundrec 118238105b Adjust lyrics for Monster and Winter Horrorland
Fix some errors and formatting
2025-11-26 13:51:07 -06:00
AbnormalPoof 0d32ccc214 Update assets and add getAtlasSettings() for texture atlas characters
Co-authored-by: VirtuGuy <Wyatt2010shaw@gmail.com>
2025-11-26 13:50:29 -06:00
EliteMasterEric ccbae3da7d Implement a checkbox to hide the waveform, which improves FPS. 2025-11-25 23:10:49 -07:00
EliteMasterEric b4058fd7b0 Code cleanup and additional docs for WaveformSprite. 2025-11-25 23:10:47 -07:00
EliteMasterEric c6add57710 Pause the chart editor when focus is lost. 2025-11-25 23:10:47 -07:00
Hundrec c33ec8c0ea Don't play Stress Pico cutscene if charting 2025-11-25 23:10:47 -07:00
PurSnake fe683eba43 Properly load variation scripts in the Chart Editor when playtesting 2025-11-25 23:10:43 -07:00
amyspark-ng ✨ 1534eb2422 fix notelay sound when not in grid 2025-11-25 22:08:05 -07:00
AbnormalPoof c2fd17c551 Re-export Pico's confirm backing card with BTA to bake the filters 2025-11-25 12:01:22 -07:00
Karim Akra ec804394b7 Fix stickers transition looking off-size when resizing the game during a song 2025-11-25 10:23:10 -07:00
VirtuGuy 04ce409f33 Fix anim editor onion skin offset problems 2025-11-25 10:16:30 -07:00
AbnormalPoof 635f6c094e Get the first owned character ID and use that for results 2025-11-25 10:16:30 -07:00
AbnormalPoof 0fbd7a7998 Fix the nametag erroneously showing as "Boyfriend" when entering character select as Pico 2025-11-25 10:01:22 -07:00
35 changed files with 1984 additions and 1620 deletions

112
.vscode/settings.json vendored
View file

@ -1,31 +1,22 @@
{
"[haxe]": {
// Automatically keep Haxe files formatted.
"editor.formatOnSave": true,
"editor.formatOnPaste": true,
"editor.codeActionsOnSave": {
"source.organizeImports": "never"
},
"editor.codeActionsOnSave": { "source.organizeImports": "never" },
"editor.defaultFormatter": "nadako.vshaxe",
"editor.tabSize": 2
},
"[json]": {
// Automatically keep JSON files formatted.
"editor.formatOnSave": true,
"editor.formatOnPaste": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[jsonc]": {
// Automatically keep JSONC files formatted.
"editor.formatOnSave": true,
"editor.formatOnPaste": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"prettier.tabWidth": 2,
// XML formatting style configuration
// XML Formatting
"xml.format.enabled": true,
"xml.format.legacy": false,
"xml.format.emptyElements": "collapse",
@ -33,7 +24,7 @@
"xml.format.enforceQuoteStyle": "preferred",
"xml.format.preserveAttributeLineBreaks": false,
"xml.format.preservedNewlines": 0,
"xml.format.splitAttributes": false,
"xml.format.splitAttributes": "preserve",
"xml.format.joinCDATALines": true,
"xml.format.preserveEmptyContent": false,
"xml.format.joinCommentLines": false,
@ -56,26 +47,19 @@
"xml.format.maxLineWidth": 0,
"xml.format.grammarAwareFormatting": true,
// Generic file formatting style configuration
// General formatting
"files.insertFinalNewline": true,
"files.trimFinalNewlines": false,
"files.trimTrailingWhitespace": true,
// Automatically detect indentation.
"editor.detectIndentation": true,
"editor.insertSpaces": true,
"editor.tabSize": 2,
// Automatically enforce Linux style line endings.
"prettier.tabWidth": 2,
"files.eol": "\n",
"haxe.displayPort": "auto",
"haxe.enableCompilationServer": true,
"haxe.enableServerView": true,
"haxe.displayServer": {
"arguments": ["-v"]
},
// Fix file associations for HScript.
"haxe.displayServer": { "arguments": ["-v"] },
"files.associations": {
"*.hxp": "haxe",
"*.hscript": "haxe",
@ -84,14 +68,27 @@
"*.hxc": "haxe"
},
"projectManager.git.baseFolders": ["./"],
"haxecheckstyle.sourceFolders": ["src", "source"],
"haxecheckstyle.externalSourceRoots": [],
"haxecheckstyle.configurationFile": "checkstyle.json",
"haxecheckstyle.codeSimilarityBufferSize": 100,
"lime.projectFile": "project.hxp",
"lime.targets": [
{ "name": "windows", "enabled": true, "label": "Windows" },
{ "name": "mac", "enabled": true, "label": "macOS" },
{ "name": "linux", "enabled": true, "label": "Linux" },
{ "name": "html5", "enabled": true, "label": "HTML5" },
{ "name": "android", "enabled": true, "label": "Android" },
{ "name": "ios", "enabled": true, "label": "iOS" },
// Disabled targets
{ "name": "hl", "enabled": false, "label": "HashLink" },
{ "name": "air", "enabled": false },
{ "name": "electron", "enabled": false },
{ "name": "flash", "enabled": false },
{ "name": "neko", "enabled": false },
{ "name": "tvos", "enabled": false }
],
"lime.defaultTargetConfiguration": "Windows / Debug",
"lime.targetConfigurations": [
{
"label": "Windows / Debug (Discord)",
@ -103,21 +100,11 @@
"target": "windows",
"args": ["-debug", "-DANIMATE", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "HashLink / Debug (FlxAnimate Test)",
"target": "hl",
"args": ["-debug", "-DANIMATE"]
},
{
"label": "Windows / Debug (Straight to Freeplay)",
"target": "windows",
"args": ["-debug", "-DFREEPLAY", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "HashLink / Debug (Straight to Freeplay)",
"target": "hl",
"args": ["-debug", "-DFREEPLAY"]
},
{
"label": "Windows / Debug (Straight to Play - Bopeebo Normal)",
"target": "windows",
@ -132,26 +119,6 @@
"target": "windows",
"args": ["-debug", "-DSONG=2hot", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "HashLink / Debug (Straight to Play - Bopeebo Normal)",
"target": "hl",
"args": ["-debug", "-DSONG=bopeebo -DDIFFICULTY=normal"]
},
{
"label": "Windows / Debug (Conversation Test)",
"target": "windows",
"args": ["-debug", "-DDIALOGUE", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "HashLink / Debug (Conversation Test)",
"target": "hl",
"args": ["-debug", "-DDIALOGUE"]
},
{
"label": "Windows / Debug (Results Screen Test)",
"target": "windows",
"args": ["-debug", "-DRESULTS"]
},
{
"label": "Windows / Debug (Straight to Stage Editor)",
"target": "windows",
@ -172,36 +139,16 @@
"target": "windows",
"args": ["-debug", "-DHXVLC_LOGGING", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "HashLink / Debug (Straight to Animation Editor)",
"target": "hl",
"args": ["-debug", "-DANIMDEBUG"]
},
{
"label": "Windows / Debug (Latency Test)",
"target": "windows",
"args": ["-debug", "-DLATENCY", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "HashLink / Debug (Latency Test)",
"target": "hl",
"args": ["-debug", "-DLATENCY"]
},
{
"label": "Windows / Debug (Waveform Test)",
"target": "windows",
"args": ["-debug", "-DWAVEFORM", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "Windows / Release (GitHub Actions)",
"target": "windows",
"args": ["-release", "-DGITHUB_BUILD"]
},
{
"label": "HashLink / Debug (Waveform Test)",
"target": "hl",
"args": ["-debug", "-DWAVEFORM"]
},
{
"label": "HTML5 / Debug (Watch)",
"target": "html5",
@ -209,10 +156,7 @@
}
],
"lime.buildTypes": [
{
"label": "Debug",
"args": ["-debug", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{ "label": "Debug", "args": ["-debug", "-DFEATURE_DEBUG_FUNCTIONS"] },
{
"label": "Debug (Unlock Everything)",
"args": ["-debug", "-DUNLOCK_EVERYTHING"]
@ -225,10 +169,7 @@
"label": "Debug (Straight to Chart Editor)",
"args": ["-debug", "-DCHARTING", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "Release",
"args": ["-release"]
},
{ "label": "Release", "args": ["-release"] },
{
"label": "Release (GitHub Actions)",
"args": ["-release", "-DGITHUB_BUILD"]
@ -244,9 +185,6 @@
],
"vscord.app.privacyMode.enable": true,
"json.schemas": [
{
"fileMatch": ["/hmm.json"],
"url": "./.vscode/schema/hmm.json"
}
{ "fileMatch": ["/hmm.json"], "url": "./.vscode/schema/hmm.json" }
]
}

2
assets

@ -1 +1 @@
Subproject commit d1ab5ea1199c5750fe749c3efd095ffc7bcf779b
Subproject commit 1a961f111381eb3bfc452166c4e4b5a18b409781

View file

@ -111,7 +111,7 @@
"name": "hxcpp",
"type": "git",
"dir": null,
"ref": "6546fa5c3ad1bac065f144745122ab5a6d4195ff",
"ref": "5932340d095a7eea8635fe4d1355f1c0efd0b3c2",
"url": "https://github.com/FunkinCrew/hxcpp"
},
{
@ -146,7 +146,7 @@
{
"name": "hxvlc",
"type": "haxelib",
"version": "2.2.4"
"version": "2.2.5"
},
{
"name": "json2object",
@ -173,7 +173,7 @@
"name": "lime",
"type": "git",
"dir": null,
"ref": "27092822abf7b8c2ec2905053cf5435be4de838e",
"ref": "d1596fe7daa9c479384ad2705e7813dbccf28839",
"url": "https://github.com/FunkinCrew/lime"
},
{

View file

@ -2049,7 +2049,7 @@ class Project extends HXProject
@SuppressWarnings('checkstyle:Dynamic')
function checkLibraries():Void
{
var outdatedLibraries:Map<String, Array<String>> = new Map<String, Array<String>>();
var diffrentLibraries:Map<String, Array<String>> = new Map<String, Array<String>>();
var hmmData:Dynamic = haxe.Json.parse(sys.io.File.getContent(#if ios '../../../../../' + #end 'hmm.json'));
@ -2063,53 +2063,71 @@ class Project extends HXProject
var libraryCurrentVersion:String = readLibraryCurrentVersion(libraryName);
var libraryCurrentCommitHash:String = readLibraryGitCommitHash(libraryName);
if (libraryDev != "" && !isLibraryLocalGitDev(libraryName))
{
switch (library.type)
{
case 'haxelib':
if (libraryDev != "")
{
outdatedLibraries.set(libraryName, [libraryDev, library.version]);
}
else if (library.version != libraryCurrentVersion)
{
outdatedLibraries.set(libraryName, [libraryCurrentVersion, library.version]);
}
diffrentLibraries.set(libraryName, [libraryDev, libraryCurrentCommitHash, library.version, 'haxelib']);
case 'git':
if (libraryDev != "" && !isLibraryGitDev(libraryName))
if (libraryCurrentCommitHash != library.ref)
{
outdatedLibraries.set(libraryName, [libraryDev, library.ref]);
diffrentLibraries.set(libraryName, [libraryDev, libraryCurrentCommitHash, library.ref, 'git']);
}
}
}
else
{
var commitHash:String = readLibraryGitCommitHash(libraryName);
if (commitHash != library.ref && commitHash != "")
switch (library.type)
{
outdatedLibraries.set(libraryName, [commitHash, library.ref]);
case 'haxelib':
if (library.version != libraryCurrentVersion)
{
diffrentLibraries.set(libraryName, ['', libraryCurrentVersion, library.version, 'haxelib']);
}
case 'git':
if (libraryCurrentCommitHash != library.ref)
{
diffrentLibraries.set(libraryName, ['', libraryCurrentCommitHash, library.ref, 'git']);
}
}
}
}
if (Lambda.count(outdatedLibraries) > 0)
if (Lambda.count(diffrentLibraries) > 0)
{
warn("The following haxelibs diverge from the versions set in hmm.json.".bold().yellow());
warn("They may be outdated, so it is recommended to abort compilation and run `hmm reinstall [library]` to update each library.".bold().yellow());
warn("You may ignore this warning if your libraries are newer than the versions in hmm.json, or if you know what you're doing.".bold().yellow());
warn("Some libraries differ from the versions defined in hmm.json.".bold().yellow());
warn("To ensure consistency, it's recommended to stop the build and run `hmm reinstall [library]`.".bold().yellow());
warn("If you're intentionally using development builds or newer versions, you can ignore this warning.".bold().yellow());
Sys.println('');
for (libraryName in outdatedLibraries.keys())
for (libraryName in diffrentLibraries.keys())
{
var versions:Null<Array<String>> = outdatedLibraries.get(libraryName);
if (versions == null) continue;
var infos:Null<Array<String>> = diffrentLibraries.get(libraryName);
var outdatedVersion:String = versions[0];
var expectedVersion:String = versions[1];
if (infos == null) continue;
Sys.println("- " + libraryName.replace(",", ".") + (haxe.io.Path.isAbsolute(outdatedVersion) ? " (Development Build)".blue().bold() : ""));
Sys.println((" Current version: " + outdatedVersion).red());
Sys.println((" Expected version: " + expectedVersion).green());
var devPath:String = infos[0];
var currentVersion:String = infos[1];
var expectedVersion:String = infos[2];
var libType:String = infos[3];
if (haxe.io.Path.isAbsolute(devPath))
{
Sys.println("- " + libraryName.replace(",", ".") + " (Development Build)".blue().bold());
Sys.println((" Path: " + haxe.io.Path.removeTrailingSlashes(devPath)).blue());
}
else
{
Sys.println("- " + libraryName.replace(",", "."));
}
Sys.println((" Current: " + currentVersion).red());
Sys.println((" Expected: " + expectedVersion).green());
}
Sys.println('');
@ -2136,11 +2154,20 @@ class Project extends HXProject
static function readLibraryGitCommitHash(libraryName:String):String
{
var commit:String = '';
var gitProccess = new sys.io.Process("git", ["-C", getLibraryGitPath(libraryName), "rev-parse", "HEAD"]);
gitProccess.exitCode(true);
commit = gitProccess.stdout.readAll().toString().trim();
return gitProccess.stdout.readAll().toString().trim();
if (commit.length <= 0)
{
gitProccess = new sys.io.Process("git", ["-C", readLibraryDev(libraryName), "rev-parse", "HEAD"]);
gitProccess.exitCode(true);
commit = gitProccess.stdout.readAll().toString().trim();
}
return commit;
}
static function getLibraryCurrentFile(libraryName:String):String
@ -2158,7 +2185,7 @@ class Project extends HXProject
return haxe.io.Path.join([haxe.io.Path.addTrailingSlash(Sys.getCwd()), '.haxelib', libraryName, 'git']);
}
static function isLibraryGitDev(libraryName:String):Bool
static function isLibraryLocalGitDev(libraryName:String):Bool
{
final gitPath:String = getLibraryGitPath(libraryName);
final devFile:String = getLibraryDevFile(libraryName);

View file

@ -3,6 +3,10 @@ package funkin.audio.waveform;
import funkin.graphics.rendering.MeshRender;
import flixel.util.FlxColor;
/**
* A sprite which displays the waveform of audio data.
* Generate a WaveformData and provide it to this sprite.
*/
class WaveformSprite extends MeshRender
{
static final DEFAULT_COLOR:FlxColor = FlxColor.WHITE;
@ -18,14 +22,17 @@ class WaveformSprite extends MeshRender
* Do this any time the data or drawable area of the waveform changes.
* This often (but not always) needs to be done every frame.
*/
var isWaveformDirty:Bool = true;
var isWaveformDirty:Bool;
/**
* If true, force the waveform to redraw every frame.
* Useful if the waveform's clipRect is constantly changing.
*/
public var forceUpdate:Bool = false;
public var forceUpdate:Bool;
/**
* The data to render the waveform with.
*/
public var waveformData(default, set):Null<WaveformData>;
function set_waveformData(value:Null<WaveformData>):Null<WaveformData>
@ -52,6 +59,9 @@ class WaveformSprite extends MeshRender
return waveformColor;
}
/**
* Whether the Waveform is horizontal or vertical.
*/
public var orientation(default, set):WaveformOrientation;
function set_orientation(value:WaveformOrientation):WaveformOrientation
@ -68,7 +78,7 @@ class WaveformSprite extends MeshRender
*/
public var time(default, set):Float;
function set_time(value:Float)
function set_time(value:Float):Float
{
if (time == value) return value;
@ -77,13 +87,22 @@ class WaveformSprite extends MeshRender
return time;
}
override function set_visible(value:Bool):Bool
{
if (visible == value) return value;
visible = value;
isWaveformDirty = true;
return visible;
}
/**
* The duration, in seconds, that the waveform represents.
* The section of waveform from `time` to `time + duration` and `width` are used to determine how many samples each pixel represents.
*/
public var duration(default, set):Float;
function set_duration(value:Float)
function set_duration(value:Float):Float
{
if (duration == value) return value;
@ -120,13 +139,13 @@ class WaveformSprite extends MeshRender
*
* NOTE: This is technically doubled since it's applied above and below the center of the waveform.
*/
public var minWaveformSize:Int = 1;
public var minWaveformSize:Int;
/**
* A multiplier on the size of the waveform.
* Still capped at the width and height set for the sprite.
*/
public var amplitude:Float = 1.0;
public var amplitude:Float;
public function new(?waveformData:WaveformData, ?orientation:WaveformOrientation, ?color:FlxColor, ?duration:Float)
{
@ -135,6 +154,11 @@ class WaveformSprite extends MeshRender
this.width = DEFAULT_WIDTH;
this.height = DEFAULT_HEIGHT;
this.minWaveformSize = 1;
this.amplitude = 1.0;
this.isWaveformDirty = true;
this.forceUpdate = false;
this.waveformData = waveformData;
this.orientation = orientation ?? DEFAULT_ORIENTATION;
this.time = 0.0;
@ -151,7 +175,7 @@ class WaveformSprite extends MeshRender
isWaveformDirty = true;
}
public override function update(elapsed:Float)
public override function update(elapsed:Float):Void
{
super.update(elapsed);
@ -170,6 +194,11 @@ class WaveformSprite extends MeshRender
makeGraphic(1, 1, this.waveformColor);
}
public override function draw():Void
{
super.draw();
}
/**
* @param offsetX Horizontal offset to draw the waveform at, in samples.
*/
@ -183,12 +212,12 @@ class WaveformSprite extends MeshRender
this.clear();
if (waveformData == null) return;
if (waveformData == null || !this.visible) return;
// Center point of the waveform. When horizontal this is half the height, when vertical this is half the width.
var waveformCenterPos:Int = orientation == HORIZONTAL ? Std.int(this.height / 2) : Std.int(this.width / 2);
var oneSecondInIndices:Int = waveformData.secondsToIndex(1);
// var oneSecondInIndices:Int = waveformData.secondsToIndex(1)
var startTime:Float = time;
var endTime:Float = time + duration;
@ -223,7 +252,10 @@ class WaveformSprite extends MeshRender
var isBeforeClipRect:Bool = (clipRect != null) && ((orientation == HORIZONTAL) ? pixelPos < clipRect.x : pixelPos < clipRect.y);
if (isBeforeClipRect) continue;
if (isBeforeClipRect)
{
continue;
}
var isAfterClipRect:Bool = (clipRect != null)
&& ((orientation == HORIZONTAL) ? pixelPos > (clipRect.x + clipRect.width) : pixelPos > (clipRect.y + clipRect.height));
@ -426,20 +458,40 @@ class WaveformSprite extends MeshRender
}
}
public static function buildFromWaveformData(data:WaveformData, ?orientation:WaveformOrientation, ?color:FlxColor, ?duration:Float)
/**
* Build a WaveformSprite from waveform data.
* @param data The data for the waveform to use.
* @param orientation Whether the waveform should be horizontal or vertical.
* @param color The color of the waveform.
* @param duration The width of the waveform, in seconds.
*
* @return The resulting WaveformSprite.
*/
public static function buildFromWaveformData(data:WaveformData, ?orientation:WaveformOrientation, ?color:FlxColor, ?duration:Float):WaveformSprite
{
return new WaveformSprite(data, orientation, color, duration);
}
public static function buildFromFunkinSound(sound:FunkinSound, ?orientation:WaveformOrientation, ?color:FlxColor, ?duration:Float)
/**
* Build a WaveformSprite from a FunkinSound's waveform data.
* @param sound The audio for the waveform to use.
* @param orientation Whether the waveform should be horizontal or vertical.
* @param color The color of the waveform.
* @param duration The width of the waveform, in seconds.
*
* @return The resulting WaveformSprite.
*/
public static function buildFromFunkinSound(sound:FunkinSound, ?orientation:WaveformOrientation, ?color:FlxColor, ?duration:Float):WaveformSprite
{
// TODO: Build waveform data from FunkinSound.
var data = null;
var data = WaveformDataParser.interpretFlxSound(sound);
return buildFromWaveformData(data, orientation, color, duration);
}
}
/**
* The possible orientations of a waveform.
*/
enum WaveformOrientation
{
HORIZONTAL;

View file

@ -157,7 +157,7 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
* @param id The ID of the entry.
* @return `true` if the entry has an attached script, `false` otherwise.
*/
public function isScriptedEntry(id:String):Bool
public function isScriptedEntry(id:String, ?params:Null<P>):Bool
{
return scriptedEntryIds.exists(id);
}
@ -167,7 +167,7 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
* @param id The ID of the entry.
* @return The class name, or `null` if it does not exist.
*/
public function getScriptedEntryClassName(id:String):Null<String>
public function getScriptedEntryClassName(id:String, ?params:Null<P>):Null<String>
{
return scriptedEntryIds.get(id);
}

View file

@ -129,6 +129,29 @@ using funkin.data.song.migrator.SongDataMigrator;
return parseEntryMetadataRaw(contents);
}
public override function isScriptedEntry(id:String, ?params:Null<SongEntryParams>)
{
var variation:String = params?.variation ?? Constants.DEFAULT_VARIATION;
if (variation != Constants.DEFAULT_VARIATION)
{
return scriptedSongVariations.exists('${id}:${variation}');
}
return super.isScriptedEntry(id, params);
}
public override function getScriptedEntryClassName(id:String, ?params:Null<SongEntryParams>):Null<String>
{
var variation:String = params?.variation ?? Constants.DEFAULT_VARIATION;
if (variation != Constants.DEFAULT_VARIATION)
{
final variationSongId:ScriptedSong = cast scriptedSongVariations.get('${id}:${variation}');
@:privateAccess
var path:String = variationSongId._asc._c.name;
return path;
}
return super.getScriptedEntryClassName(id, params);
}
/**
* We override `fetchEntry` to handle song variations!
*/

View file

@ -0,0 +1,26 @@
package funkin.external.android;
#if android
import lime.system.JNI;
/**
* Utility class for keyboard detection.
*/
class KeyboardUtil
{
/**
* Returns `true` if a keyboard is currently connected to the device.
*/
public static var keyboardConnected(get, never):Bool;
@:noCompletion
static function get_keyboardConnected():Bool
{
final method:Null<Dynamic> = JNIUtil.createStaticMethod('funkin/util/KeyboardUtil', 'isKeyboardConnected', '()Z');
if (method == null) return false;
return inline JNI.callStatic(method, []);
}
}
#end

View file

@ -0,0 +1,14 @@
package funkin.util;
import org.haxe.extension.Extension;
public class KeyboardUtil
{
public static boolean isKeyboardConnected()
{
if (Extension.mainContext == null) return false;
// KEYBOARD_UNDEFINED = 0, KEYBOARD_NOKEYS = 1
return Extension.mainContext.getResources().getConfiguration().keyboard > 1;
}
}

View file

@ -21,6 +21,7 @@ import animate.internal.elements.AtlasInstance;
import animate.internal.elements.SymbolInstance;
import animate.FlxAnimate;
import animate.FlxAnimateFrames;
import haxe.io.Path;
using StringTools;
@ -104,10 +105,41 @@ class FunkinSprite extends FlxAnimate
/**
* @param x Starting X position
* @param y Starting Y position
* @param path The asset path for the graphic
* @param atlasSettings The optional settings for the texture atlas
*/
public function new(?x:Float = 0, ?y:Float = 0)
public function new(?x:Float = 0, ?y:Float = 0, ?path:String, ?atlasSettings:AtlasSpriteSettings)
{
super(x, y);
if (path != null)
{
var ext:String = Path.extension(path);
switch (ext)
{
case 'png':
this.loadGraphic(path);
case '':
// Do the opposite of Paths.animateAtlas since that function is called in loadTextureAtlas.
var lib:String = Paths.getLibrary(path);
if (lib == 'preload')
{
path = path.replace('assets/images/', '');
}
else
{
path = path.replace('$lib:assets/$lib/images/', '');
}
this.loadTextureAtlas(path, lib, atlasSettings);
default:
FlxG.log.warn('Texture path $path is not a valid path. Make sure the path points to either an image or a folder with the texture atlas files!');
}
}
}
override function initVars():Void
@ -419,6 +451,46 @@ class FunkinSprite extends FlxAnimate
return false;
}
/**
* Gets every frame on every symbol that starts with the given keyword.
* @param keyword The keyword to search for.
* @return An array of frames.
*/
public function getFramesWithKeyword(keyword:String):Array<animate.internal.Frame>
{
if (!this.isAnimate)
{
trace('WARNING: getFramesWithKeyword() only works texture atlases!');
return [];
}
var symbolItems:Array<animate.internal.SymbolItem> = [];
var frames:Array<animate.internal.Frame> = [];
@:privateAccess
for (symbol in this.library.dictionary.keys())
{
var symbolItem:Null<animate.internal.SymbolItem> = this.library.getSymbol(symbol);
if (symbolItem == null) continue;
if (symbolItem.name.contains(keyword))
{
symbolItems.push(symbolItem);
}
}
for (symbolItem in symbolItems)
{
symbolItem.timeline.forEachLayer((layer) -> {
layer.forEachFrame((frame) -> {
frames.push(frame);
});
});
}
return frames;
}
/**
* Gets the current animation ID.
*/

View file

@ -70,6 +70,11 @@ class MeshRender extends FlxStrip
add_tri(a, c, d);
}
public override function draw():Void
{
super.draw();
}
/**
* Build a quad from four points.
*

View file

@ -19,19 +19,20 @@ import flixel.math.FlxPoint;
*/
class Controls extends FlxActionSet
{
/**
/*
* A list of actions that a player would invoke via some input device.
* Uses FlxActions to funnel various inputs to a single action.
*/
var _ui_up = new FunkinAction(Action.UI_UP);
var _ui_left = new FunkinAction(Action.UI_LEFT);
var _ui_right = new FunkinAction(Action.UI_RIGHT);
var _ui_down = new FunkinAction(Action.UI_DOWN);
var _note_up = new FunkinAction(Action.NOTE_UP);
var _note_left = new FunkinAction(Action.NOTE_LEFT);
var _note_right = new FunkinAction(Action.NOTE_RIGHT);
var _note_down = new FunkinAction(Action.NOTE_DOWN);
var _accept = new FunkinAction(Action.ACCEPT);
var _back = new FunkinAction(Action.BACK);
var _pause = new FunkinAction(Action.PAUSE);
@ -126,26 +127,6 @@ class Controls extends FlxActionSet
inline function get_UI_DOWN_R()
return _ui_down.checkJustReleased();
public var UI_UP_GAMEPAD(get, never):Bool;
inline function get_UI_UP_GAMEPAD()
return _ui_up.checkPressedGamepad();
public var UI_LEFT_GAMEPAD(get, never):Bool;
inline function get_UI_LEFT_GAMEPAD()
return _ui_left.checkPressedGamepad();
public var UI_RIGHT_GAMEPAD(get, never):Bool;
inline function get_UI_RIGHT_GAMEPAD()
return _ui_right.checkPressedGamepad();
public var UI_DOWN_GAMEPAD(get, never):Bool;
inline function get_UI_DOWN_GAMEPAD()
return _ui_down.checkPressedGamepad();
public var NOTE_UP(get, never):Bool;
inline function get_NOTE_UP()
@ -527,9 +508,8 @@ class Controls extends FlxActionSet
* Calls a function passing each action bound by the specified control
* @param control
* @param func
* @return ->Void)
*/
function forEachBound(control:Control, func:FunkinAction->FlxInputState->Void)
function forEachBound(control:Control, func:FunkinAction->FlxInputState->Void):Void
{
switch (control)
{
@ -624,7 +604,7 @@ class Controls extends FlxActionSet
}
}
public function replaceBinding(control:Control, device:Device, toAdd:Int, toRemove:Int)
public function replaceBinding(control:Control, device:Device, toAdd:Int, toRemove:Int):Void
{
if (toAdd == toRemove) return;
@ -638,7 +618,7 @@ class Controls extends FlxActionSet
}
}
function replaceKey(action:FlxActionDigital, toAdd:FlxKey, toRemove:FlxKey, state:FlxInputState)
function replaceKey(action:FlxActionDigital, toAdd:FlxKey, toRemove:FlxKey, state:FlxInputState):Void
{
if (action.inputs.length == 0)
{
@ -689,7 +669,7 @@ class Controls extends FlxActionSet
}
}
function replaceButton(action:FlxActionDigital, deviceID:Int, toAdd:FlxGamepadInputID, toRemove:FlxGamepadInputID, state:FlxInputState)
function replaceButton(action:FlxActionDigital, deviceID:Int, toAdd:FlxGamepadInputID, toRemove:FlxGamepadInputID, state:FlxInputState):Void
{
if (action.inputs.length == 0)
{
@ -717,7 +697,7 @@ class Controls extends FlxActionSet
}
}
public function copyFrom(controls:Controls, ?device:Device)
public function copyFrom(controls:Controls, ?device:Device):Void
{
for (name in controls.byName.keys())
{
@ -744,7 +724,7 @@ class Controls extends FlxActionSet
}
}
inline public function copyTo(controls:Controls, ?device:Device)
inline public function copyTo(controls:Controls, ?device:Device):Void
{
controls.copyFrom(this, device);
}
@ -767,7 +747,7 @@ class Controls extends FlxActionSet
* Sets all actions that pertain to the binder to trigger when the supplied keys are used.
* If binder is a literal you can inline this
*/
public function bindKeys(control:Control, keys:Array<FlxKey>)
public function bindKeys(control:Control, keys:Array<FlxKey>):Void
{
forEachBound(control, function(action, state) addKeys(action, keys, state));
}
@ -776,12 +756,12 @@ class Controls extends FlxActionSet
* Sets all actions that pertain to the binder to trigger when the supplied keys are used.
* If binder is a literal you can inline this
*/
public function unbindKeys(control:Control, keys:Array<FlxKey>)
public function unbindKeys(control:Control, keys:Array<FlxKey>):Void
{
forEachBound(control, function(action, _) removeKeys(action, keys));
}
static function addKeys(action:FlxActionDigital, keys:Array<FlxKey>, state:FlxInputState)
static function addKeys(action:FlxActionDigital, keys:Array<FlxKey>, state:FlxInputState):Void
{
for (key in keys)
{
@ -790,7 +770,7 @@ class Controls extends FlxActionSet
}
}
static function removeKeys(action:FlxActionDigital, keys:Array<FlxKey>)
static function removeKeys(action:FlxActionDigital, keys:Array<FlxKey>):Void
{
var i = action.inputs.length;
while (i-- > 0)
@ -951,7 +931,7 @@ class Controls extends FlxActionSet
return [];
}
function removeKeyboard()
function removeKeyboard():Void
{
for (action in this.digitalActions)
{
@ -971,16 +951,6 @@ class Controls extends FlxActionSet
fromSaveData(padData, Gamepad(id));
}
public function getGamepadIds():Array<Int>
{
return gamepadsAdded;
}
public function getGamepads():Array<FlxGamepad>
{
return [for (id in gamepadsAdded) FlxG.gamepads.getByID(id)];
}
inline function addGamepadLiteral(id:Int, ?buttonMap:Map<Control, Array<FlxGamepadInputID>>):Void
{
gamepadsAdded.push(id);
@ -1004,7 +974,7 @@ class Controls extends FlxActionSet
gamepadsAdded.remove(deviceID);
}
public function addDefaultGamepad(id):Void
public function addDefaultGamepad(id:Int):Void
{
addGamepadLiteral(id, [
Control.ACCEPT => getDefaultGamepadBinds(Control.ACCEPT),
@ -1048,32 +1018,32 @@ class Controls extends FlxActionSet
function getDefaultGamepadBinds(control:Control):Array<FlxGamepadInputID>
{
switch (control)
return switch (control)
{
case Control.ACCEPT:
return [#if switch B #else A #end];
[A];
case Control.BACK:
return [#if switch A #else B #end];
[B];
case Control.UI_UP:
return [DPAD_UP, LEFT_STICK_DIGITAL_UP];
[DPAD_UP, LEFT_STICK_DIGITAL_UP];
case Control.UI_DOWN:
return [DPAD_DOWN, LEFT_STICK_DIGITAL_DOWN];
[DPAD_DOWN, LEFT_STICK_DIGITAL_DOWN];
case Control.UI_LEFT:
return [DPAD_LEFT, LEFT_STICK_DIGITAL_LEFT];
[DPAD_LEFT, LEFT_STICK_DIGITAL_LEFT];
case Control.UI_RIGHT:
return [DPAD_RIGHT, LEFT_STICK_DIGITAL_RIGHT];
[DPAD_RIGHT, LEFT_STICK_DIGITAL_RIGHT];
case Control.NOTE_UP:
return [DPAD_UP, Y, LEFT_STICK_DIGITAL_UP, RIGHT_STICK_DIGITAL_UP];
[DPAD_UP, Y, LEFT_STICK_DIGITAL_UP, RIGHT_STICK_DIGITAL_UP];
case Control.NOTE_DOWN:
return [DPAD_DOWN, A, LEFT_STICK_DIGITAL_DOWN, RIGHT_STICK_DIGITAL_DOWN];
[DPAD_DOWN, A, LEFT_STICK_DIGITAL_DOWN, RIGHT_STICK_DIGITAL_DOWN];
case Control.NOTE_LEFT:
return [DPAD_LEFT, X, LEFT_STICK_DIGITAL_LEFT, RIGHT_STICK_DIGITAL_LEFT];
[DPAD_LEFT, X, LEFT_STICK_DIGITAL_LEFT, RIGHT_STICK_DIGITAL_LEFT];
case Control.NOTE_RIGHT:
return [DPAD_RIGHT, B, LEFT_STICK_DIGITAL_RIGHT, RIGHT_STICK_DIGITAL_RIGHT];
[DPAD_RIGHT, B, LEFT_STICK_DIGITAL_RIGHT, RIGHT_STICK_DIGITAL_RIGHT];
case Control.PAUSE:
return [START];
[START];
case Control.RESET:
return [FlxGamepadInputID.BACK]; // Back (i.e. Select)
[FlxGamepadInputID.BACK]; // Back (i.e. Select)
case Control.WINDOW_FULLSCREEN:
[];
#if FEATURE_SCREENSHOTS
@ -1081,19 +1051,19 @@ class Controls extends FlxActionSet
[];
#end
case Control.CUTSCENE_ADVANCE:
return [A];
[A];
case Control.FREEPLAY_FAVORITE:
return [Y]; // Back (i.e. Select)
[Y]; // Back (i.e. Select)
case Control.FREEPLAY_LEFT:
return [LEFT_SHOULDER];
[LEFT_SHOULDER];
case Control.FREEPLAY_RIGHT:
return [RIGHT_SHOULDER];
[RIGHT_SHOULDER];
case Control.FREEPLAY_CHAR_SELECT:
return [X];
[X];
case Control.FREEPLAY_JUMP_TO_TOP:
return [RIGHT_STICK_DIGITAL_UP];
[RIGHT_STICK_DIGITAL_UP];
case Control.FREEPLAY_JUMP_TO_BOTTOM:
return [RIGHT_STICK_DIGITAL_DOWN];
[RIGHT_STICK_DIGITAL_DOWN];
case Control.VOLUME_UP:
[];
case Control.VOLUME_DOWN:
@ -1115,16 +1085,15 @@ class Controls extends FlxActionSet
case Control.DEBUG_DISPLAY:
[];
default:
// Fallthrough.
[];
}
return [];
}
/**
* Sets all actions that pertain to the binder to trigger when the supplied keys are used.
* If binder is a literal you can inline this
*/
public function bindButtons(control:Control, id, buttons)
public function bindButtons(control:Control, id:Int, buttons):Void
{
forEachBound(control, function(action, state) addButtons(action, buttons, state, id));
}
@ -1133,12 +1102,12 @@ class Controls extends FlxActionSet
* Sets all actions that pertain to the binder to trigger when the supplied keys are used.
* If binder is a literal you can inline this
*/
public function unbindButtons(control:Control, gamepadID:Int, buttons)
public function unbindButtons(control:Control, gamepadID:Int, buttons):Void
{
forEachBound(control, function(action, _) removeButtons(action, gamepadID, buttons));
}
inline static function addButtons(action:FlxActionDigital, buttons:Array<FlxGamepadInputID>, state, id)
inline static function addButtons(action:FlxActionDigital, buttons:Array<FlxGamepadInputID>, state, id:Int):Void
{
for (button in buttons)
{
@ -1147,7 +1116,7 @@ class Controls extends FlxActionSet
}
}
static function removeButtons(action:FlxActionDigital, gamepadID:Int, buttons:Array<FlxGamepadInputID>)
static function removeButtons(action:FlxActionDigital, gamepadID:Int, buttons:Array<FlxGamepadInputID>):Void
{
var i = action.inputs.length;
while (i-- > 0)
@ -1177,17 +1146,6 @@ class Controls extends FlxActionSet
return list;
}
public function removeDevice(device:Device)
{
switch (device)
{
case Keys:
setKeyboardScheme(None);
case Gamepad(id):
removeGamepad(id);
}
}
/**
* NOTE: When loading controls:
* An EMPTY array means the control is uninitialized and needs to be reset to default.
@ -1270,7 +1228,7 @@ class Controls extends FlxActionSet
return isEmpty ? null : data;
}
static function isDevice(input:FlxActionInput, device:Device)
static function isDevice(input:FlxActionInput, device:Device):Bool
{
return switch (device)
{
@ -1279,7 +1237,7 @@ class Controls extends FlxActionSet
}
}
inline static function isGamepad(input:FlxActionInput, deviceID:Int)
inline static function isGamepad(input:FlxActionInput, deviceID:Int):Bool
{
return input.device == GAMEPAD && (deviceID == FlxInputDeviceID.ALL || input.deviceID == deviceID);
}
@ -1393,14 +1351,8 @@ class FunkinAction extends FlxActionDigital
public function checkMultiFiltered(?filterTriggers:Array<FlxInputState>, ?filterDevices:Array<FlxInputDevice>):Bool
{
if (filterTriggers == null)
{
filterTriggers = [PRESSED, JUST_PRESSED];
}
if (filterDevices == null)
{
filterDevices = [];
}
filterTriggers ??= [PRESSED, JUST_PRESSED];
filterDevices ??= [];
// Perform checkFiltered for each combination.
for (i in filterTriggers)
@ -1431,6 +1383,7 @@ class FunkinAction extends FlxActionDigital
* @param action The action to check for.
* @param filterTrigger Optionally filter by trigger condition (`JUST_PRESSED`, `PRESSED`, `JUST_RELEASED`, `RELEASED`).
* @param filterDevice Optionally filter by device (`KEYBOARD`, `MOUSE`, `GAMEPAD`, `OTHER`).
* @return bool if our input has been triggered
*/
public function checkFiltered(?filterTrigger:FlxInputState, ?filterDevice:FlxInputDevice):Bool
{
@ -1442,20 +1395,22 @@ class FunkinAction extends FlxActionDigital
{
return cacheEntry.value;
}
// Use a for loop instead so we can remove inputs while iterating.
// We don't return early because we need to call check() on ALL inputs.
var result = false;
var len = inputs != null ? inputs.length : 0;
for (i in 0...len)
_x = null;
_y = null;
_timestamp = FlxG.game.ticks;
triggered = false;
var i = inputs?.length ?? 0;
while (i-- > 0) // Iterate backwards, since we may remove items
{
var j = len - i - 1;
var input = inputs[j];
var input = inputs[i];
// Filter out dead inputs.
if (input.destroyed)
{
inputs.splice(j, 1);
inputs.remove(input);
continue;
}
@ -1477,14 +1432,13 @@ class FunkinAction extends FlxActionDigital
// Check whether the input has triggered.
if (input.check(this))
{
result = true;
triggered = true;
}
}
// We need to cache this result.
cache.set(key, {timestamp: FlxG.game.ticks, value: result});
cache.set(key, {timestamp: FlxG.game.ticks, value: triggered});
return result;
return triggered;
}
}
@ -1505,10 +1459,10 @@ enum Control
UI_DOWN;
UI_UP;
UI_RIGHT;
RESET;
ACCEPT;
BACK;
PAUSE;
RESET;
// CUTSCENE
CUTSCENE_ADVANCE;
// FREEPLAY
@ -1539,27 +1493,11 @@ enum abstract Action(String) to String from String
var NOTE_LEFT = "note_left";
var NOTE_RIGHT = "note_right";
var NOTE_DOWN = "note_down";
var NOTE_UP_P = "note_up-press";
var NOTE_LEFT_P = "note_left-press";
var NOTE_RIGHT_P = "note_right-press";
var NOTE_DOWN_P = "note_down-press";
var NOTE_UP_R = "note_up-release";
var NOTE_LEFT_R = "note_left-release";
var NOTE_RIGHT_R = "note_right-release";
var NOTE_DOWN_R = "note_down-release";
// UI
var UI_UP = "ui_up";
var UI_LEFT = "ui_left";
var UI_RIGHT = "ui_right";
var UI_DOWN = "ui_down";
var UI_UP_P = "ui_up-press";
var UI_LEFT_P = "ui_left-press";
var UI_RIGHT_P = "ui_right-press";
var UI_DOWN_P = "ui_down-press";
var UI_UP_R = "ui_up-release";
var UI_LEFT_R = "ui_left-release";
var UI_RIGHT_R = "ui_right-release";
var UI_DOWN_R = "ui_down-release";
var ACCEPT = "accept";
var BACK = "back";
var PAUSE = "pause";

View file

@ -10,6 +10,9 @@ import funkin.mobile.ui.FunkinHitbox;
import funkin.play.notes.NoteDirection;
import openfl.events.KeyboardEvent;
import openfl.events.TouchEvent;
#if android
import funkin.external.android.KeyboardUtil;
#end
/**
* Handles setting up and managing input controls for the game.
@ -125,7 +128,7 @@ class ControlsHandler
@:noCompletion
private static function get_hasExternalInputDevice():Bool
{
return FlxG.gamepads.numActiveGamepads > 0 #if android || extension.androidtools.Tools.isChromebook() #end;
return FlxG.gamepads.numActiveGamepads > 0 #if android || KeyboardUtil.keyboardConnected || extension.androidtools.Tools.isChromebook() #end;
}
@:noCompletion

View file

@ -811,6 +811,10 @@ class PlayState extends MusicBeatSubState
// This state receives draw calls even when a substate is active.
this.persistentDraw = true;
// Make the player unable to pause if they're moving from the chart editor while the focus is still on since the input persists.
@:privateAccess
justUnpaused = isChartingMode && !FlxG.game._lostFocus;
// Stop any pre-existing music.
if (!overrideMusic)
{
@ -1185,14 +1189,17 @@ class PlayState extends MusicBeatSubState
if (health > Constants.HEALTH_MAX) health = Constants.HEALTH_MAX;
if (health < Constants.HEALTH_MIN) health = Constants.HEALTH_MIN;
// Apply camera zoom + multipliers.
if (subState == null && cameraZoomRate > 0.0) // && !isInCutscene)
{
cameraBopMultiplier = FlxMath.lerp(1.0, cameraBopMultiplier, 0.95); // Lerp bop multiplier back to 1.0x
var zoomPlusBop = currentCameraZoom * cameraBopMultiplier; // Apply camera bop multiplier.
if (!debugUnbindCameraZoom) FlxG.camera.zoom = zoomPlusBop; // Actually apply the zoom to the camera.
var decayRate:Float = 0.95;
var dt:Float = elapsed * 60; //
camHUD.zoom = FlxMath.lerp(defaultHUDCameraZoom, camHUD.zoom, 0.95);
if (subState == null && cameraZoomRate > 0.0)
{
cameraBopMultiplier = FlxMath.lerp(1.0, cameraBopMultiplier, Math.pow(decayRate, dt));
var zoomPlusBop = currentCameraZoom * cameraBopMultiplier;
if (!debugUnbindCameraZoom) FlxG.camera.zoom = zoomPlusBop;
camHUD.zoom = FlxMath.lerp(defaultHUDCameraZoom, camHUD.zoom, Math.pow(decayRate, dt));
}
if (currentStage != null && currentStage.getBoyfriend() != null)
@ -2679,8 +2686,6 @@ class PlayState extends MusicBeatSubState
*/
function onKeyRelease(event:PreciseInputEvent):Void
{
if (isGamePaused) return;
// Do the minimal possible work here.
inputReleaseQueue.push(event);
}

View file

@ -47,13 +47,13 @@ class AnimateAtlasCharacter extends BaseCharacter
originalSizes.set(this.width, this.height);
}
function loadAtlas()
function loadAtlas():Void
{
trace('[ATLASCHAR] Loading sprite atlas for ${characterId}.');
var assetLibrary:String = Paths.getLibrary(_data.assetPath);
var assetPath:String = Paths.stripLibrary(_data.assetPath);
loadTextureAtlas(assetPath, assetLibrary, cast _data.atlasSettings);
loadTextureAtlas(assetPath, assetLibrary, getAtlasSettings());
if (_data.isPixel)
{
@ -69,7 +69,7 @@ class AnimateAtlasCharacter extends BaseCharacter
this.setScale(_data.scale);
}
function loadAnimations()
function loadAnimations():Void
{
trace('[ATLASCHAR] Loading ${_data.animations.length} animations for ${characterId}');
@ -100,4 +100,13 @@ class AnimateAtlasCharacter extends BaseCharacter
{
return originalSizes.y;
}
/**
* Get the configuration for the texture atlas.
* @return The configuration for the texture atlas.
*/
public function getAtlasSettings():AtlasSpriteSettings
{
return cast _data.atlasSettings;
}
}

View file

@ -49,7 +49,7 @@ class MultiAnimateAtlasCharacter extends BaseCharacter
originalSizes.set(this.width, this.height);
}
function loadAtlases()
function loadAtlases():Void
{
trace('[MULTIATLASCHAR] Loading sprite atlases for ${characterId}.');
@ -65,7 +65,7 @@ class MultiAnimateAtlasCharacter extends BaseCharacter
var baseAssetLibrary:String = Paths.getLibrary(_data.assetPath);
var baseAssetPath:String = Paths.stripLibrary(_data.assetPath);
loadTextureAtlas(baseAssetPath, baseAssetLibrary, cast _data.atlasSettings);
loadTextureAtlas(baseAssetPath, baseAssetLibrary, getAtlasSettings());
for (asset in assetList)
{
@ -95,7 +95,7 @@ class MultiAnimateAtlasCharacter extends BaseCharacter
this.setScale(_data.scale);
}
function loadAnimations()
function loadAnimations():Void
{
trace('[MULTIATLASCHAR] Loading ${_data.animations.length} animations for ${characterId}');
@ -126,4 +126,13 @@ class MultiAnimateAtlasCharacter extends BaseCharacter
{
return originalSizes.y;
}
/**
* Get the configuration for the texture atlas.
* @return The configuration for the texture atlas.
*/
public function getAtlasSettings():AtlasSpriteSettings
{
return cast _data.atlasSettings;
}
}

View file

@ -224,7 +224,12 @@ class HealthIcon extends FunkinSprite
{
super.update(elapsed);
if (bopEvery != 0) this.angle = MathUtil.smoothLerpPrecision(this.angle, 0, elapsed, 0.512);
if (bopEvery != 0)
{
var dt:Float = elapsed * 60;
this.angle = MathUtil.smoothLerpPrecision(this.angle, 0, dt, 0.512);
}
this.updatePosition();
}

View file

@ -193,15 +193,15 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
* @param validScore Whether the song is elegible for highscores.
* @return The constructed song object.
*/
public static function buildRaw(songId:String, metadata:Array<SongMetadata>, variations:Array<String>, charts:Map<String, SongChartData>,
includeScript:Bool = true, validScore:Bool = false):Song
public static function buildRaw(songId:String, metadata:Array<SongMetadata>, variation:String, charts:Map<String, SongChartData>, includeScript:Bool = true,
validScore:Bool = false):Song
{
@:privateAccess
var result:Null<Song> = null;
if (includeScript && SongRegistry.instance.isScriptedEntry(songId))
if (includeScript && SongRegistry.instance.isScriptedEntry(songId, {variation: variation}))
{
var songClassName:Null<String> = SongRegistry.instance.getScriptedEntryClassName(songId);
var songClassName:Null<String> = SongRegistry.instance.getScriptedEntryClassName(songId, {variation: variation});
@:privateAccess
if (songClassName != null) result = SongRegistry.instance.createScriptedEntry(songClassName);
}

View file

@ -0,0 +1,136 @@
package funkin.ui.charSelect;
import funkin.graphics.FunkinSprite;
import flixel.tweens.FlxTween;
import flixel.tweens.FlxEase;
import flixel.math.FlxPoint;
import openfl.display.BlendMode;
import flixel.group.FlxSpriteContainer.FlxTypedSpriteContainer;
import funkin.util.MathUtil;
class CharSelectCursors extends FlxTypedSpriteContainer<FunkinSprite>
{
/**
* The main cursor sprite for this class.
*/
public var main:FunkinSprite;
var lightBlue:FunkinSprite;
var darkBlue:FunkinSprite;
var cursorConfirmed:FunkinSprite;
var cursorDenied:FunkinSprite;
public function new()
{
super();
darkBlue = new FunkinSprite(0, 0);
lightBlue = new FunkinSprite(0, 0);
main = new FunkinSprite(0, 0);
cursorConfirmed = new FunkinSprite(0, 0);
cursorDenied = new FunkinSprite(0, 0);
darkBlue.loadGraphic(Paths.image('charSelect/charSelector'));
lightBlue.loadGraphic(Paths.image('charSelect/charSelector'));
main.loadGraphic(Paths.image('charSelect/charSelector'));
darkBlue.color = 0xFF3C74F7;
lightBlue.color = 0xFF3EBBFF;
main.color = 0xFFFFFF00;
FlxTween.color(main, 0.2, 0xFFFFFF00, 0xFFFFCC00, {type: PINGPONG});
darkBlue.blend = BlendMode.SCREEN;
lightBlue.blend = BlendMode.SCREEN;
add(darkBlue);
add(lightBlue);
add(main);
cursorConfirmed.frames = Paths.getSparrowAtlas("charSelect/charSelectorConfirm");
cursorConfirmed.animation.addByPrefix("idle", "cursor ACCEPTED instance 1", 24, true);
cursorConfirmed.visible = false;
add(cursorConfirmed);
cursorDenied.frames = Paths.getSparrowAtlas("charSelect/charSelectorDenied");
cursorDenied.animation.addByPrefix("idle", "cursor DENIED instance 1", 24, false);
cursorDenied.visible = false;
add(cursorDenied);
scrollFactor.set();
directAlpha = true;
}
public function confirm():Void
{
cursorConfirmed.visible = true;
cursorConfirmed.animation.play("idle", true);
main.visible = lightBlue.visible = darkBlue.visible = false;
}
public function resetDeny():Void
{
cursorDenied.visible = false;
}
public function deny():Void
{
cursorDenied.visible = true;
cursorDenied.animation.play('idle', true);
cursorDenied.animation.onFinish.add((_) -> {
cursorDenied.visible = false;
});
}
public function unconfirm():Void
{
cursorConfirmed.visible = false;
main.visible = lightBlue.visible = darkBlue.visible = true;
}
/**
* Snaps the cursors to the given position.
* @param intendedPosition The position to snap to as a `FlxPoint`.
*/
public function snapToLocation(intendedPosition:FlxPoint):Void
{
main.x = intendedPosition.x;
main.y = intendedPosition.y;
lightBlue.x = main.x;
lightBlue.y = main.y;
darkBlue.x = intendedPosition.x;
darkBlue.y = intendedPosition.y;
cursorConfirmed.x = main.x - 2;
cursorConfirmed.y = main.y - 4;
cursorDenied.x = main.x - 2;
cursorDenied.y = main.y - 4;
}
/**
* Lerps the cursors to the given position.
* @param intendedPosition The position to lerp to as a `FlxPoint`.
*/
public function lerpToLocation(intendedPosition:FlxPoint):Void
{
main.x = MathUtil.snap(MathUtil.smoothLerpPrecision(main.x, intendedPosition.x, FlxG.elapsed, 0.1), intendedPosition.x, 1);
main.y = MathUtil.snap(MathUtil.smoothLerpPrecision(main.y, intendedPosition.y, FlxG.elapsed, 0.1), intendedPosition.y, 1);
lightBlue.x = MathUtil.smoothLerpPrecision(lightBlue.x, main.x, FlxG.elapsed, 0.202);
lightBlue.y = MathUtil.smoothLerpPrecision(lightBlue.y, main.y, FlxG.elapsed, 0.202);
darkBlue.x = MathUtil.smoothLerpPrecision(darkBlue.x, intendedPosition.x, FlxG.elapsed, 0.404);
darkBlue.y = MathUtil.smoothLerpPrecision(darkBlue.y, intendedPosition.y, FlxG.elapsed, 0.404);
cursorConfirmed.x = main.x - 2;
cursorConfirmed.y = main.y - 4;
cursorDenied.x = main.x - 2;
cursorDenied.y = main.y - 4;
}
}

View file

@ -1,8 +1,10 @@
package funkin.ui.charSelect;
import flixel.util.FlxDirectionFlags;
import flixel.FlxObject;
import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.group.FlxSpriteGroup;
import flixel.math.FlxMath;
import flixel.math.FlxPoint;
import flixel.sound.FlxSound;
import flixel.system.debug.watch.Tracker.TrackerProfile;
@ -13,7 +15,6 @@ import flixel.util.FlxColor;
import funkin.audio.FunkinSound;
import funkin.data.freeplay.player.PlayerData.PlayerCharSelectData;
import funkin.data.freeplay.player.PlayerRegistry;
import funkin.graphics.FunkinCamera;
import funkin.graphics.FunkinSprite;
import funkin.graphics.shaders.BlueFade;
import funkin.modding.events.ScriptEvent;
@ -42,22 +43,20 @@ import funkin.util.TouchUtil;
@:nullSafety
class CharSelectSubState extends MusicBeatSubState
{
// what the actual hell
// having a hard time trying to make my changes work so i chose to be less stubborn and just remove them for now. - Zack
// Left this here so somebody can remind me
var cursor:FunkinSprite;
/**
* The default index for the cursor.
*/
final DEFAULT_CURSOR_INDEX:Int = 4;
var cursors:CharSelectCursors;
var cursorBlue:FunkinSprite;
var cursorDarkBlue:FunkinSprite;
var grpCursors:FlxTypedGroup<FunkinSprite>;
var cursorConfirmed:FunkinSprite;
var cursorDenied:FunkinSprite;
var cursorX:Int = 0;
var cursorY:Int = 0;
var cursorFactor:Float = 110;
var cursorOffsetX:Float = -16;
var cursorOffsetY:Float = -48;
var cursorLocIntended:FlxPoint = new FlxPoint(0, 0);
var playerChill:CharSelectPlayer;
var playerChillOut:CharSelectPlayer;
var gfChill:CharSelectGF;
@ -82,7 +81,6 @@ class CharSelectSubState extends MusicBeatSubState
var introSound:FunkinSound = new FunkinSound();
var staticSound:FunkinSound = new FunkinSound();
// var charSelectCam:FunkinCamera;
var selectedBizz:Array<BitmapFilter> = [
new DropShadowFilter(0, 0, 0xFFFFFF, 1, 2, 2, 19, 1, false, false, false),
new DropShadowFilter(5, 45, 0x000000, 1, 2, 2, 1, 1, false, false, false)
@ -104,7 +102,7 @@ class CharSelectSubState extends MusicBeatSubState
cutoutSize = FullScreenScaleMode.gameCutoutSize.x / 2;
grpCursors = new FlxTypedGroup<FunkinSprite>();
cursors = new CharSelectCursors();
grpHitboxes = new FlxTypedGroup<FlxObject>();
gfChill = new CharSelectGF(cutoutSize, 0);
@ -115,13 +113,7 @@ class CharSelectSubState extends MusicBeatSubState
dipshitBacking = new FunkinSprite(cutoutSize + 423, -17);
chooseDipshit = new FunkinSprite(cutoutSize + 426, -13);
nametag = new Nametag(curChar);
cursor = new FunkinSprite(0, 0);
cursorBlue = new FunkinSprite(0, 0);
cursorDarkBlue = new FunkinSprite(0, 0);
cursorConfirmed = new FunkinSprite(0, 0);
cursorDenied = new FunkinSprite(0, 0);
nametag = new Nametag(rememberedChar);
charHitbox = new FlxObject(FlxG.width * 0.65, FlxG.height * 0.2, 300, 500);
@ -248,14 +240,17 @@ class CharSelectSubState extends MusicBeatSubState
{
if (charId == rememberedChar)
{
setCursorPosition(pos);
setCursorPosition(pos, true);
break;
}
}
@:bypassAccessor curChar = rememberedChar;
}
else
{
setupPlayerChill(Constants.DEFAULT_CHARACTER);
setCursorPosition(DEFAULT_CURSOR_INDEX, true);
}
var speakers:FunkinSprite = FunkinSprite.createTextureAtlas(cutoutSize - 10, 0, "charSelect/charSelectSpeakers",
{
@ -314,49 +309,18 @@ class CharSelectSubState extends MusicBeatSubState
FlxG.debugger.addTrackerProfile(new TrackerProfile(FunkinSprite, ["x", "y", "alpha", "scale", "blend"]));
FlxG.debugger.addTrackerProfile(new TrackerProfile(FlxSound, ["pitch", "volume"]));
add(grpCursors);
cursor.loadGraphic(Paths.image('charSelect/charSelector'));
cursor.color = 0xFFFFFF00;
cursorBlue.loadGraphic(Paths.image('charSelect/charSelector'));
cursorBlue.color = 0xFF3EBBFF;
cursorDarkBlue.loadGraphic(Paths.image('charSelect/charSelector'));
cursorDarkBlue.color = 0xFF3C74F7;
cursorBlue.blend = BlendMode.SCREEN;
cursorDarkBlue.blend = BlendMode.SCREEN;
cursorConfirmed.scrollFactor.set();
cursorConfirmed.frames = Paths.getSparrowAtlas("charSelect/charSelectorConfirm");
cursorConfirmed.animation.addByPrefix("idle", "cursor ACCEPTED instance 1", 24, true);
cursorConfirmed.visible = false;
add(cursorConfirmed);
cursorDenied.scrollFactor.set();
cursorDenied.frames = Paths.getSparrowAtlas("charSelect/charSelectorDenied");
cursorDenied.animation.addByPrefix("idle", "cursor DENIED instance 1", 24, false);
cursorDenied.visible = false;
add(cursorDenied);
grpCursors.add(cursorDarkBlue);
grpCursors.add(cursorBlue);
grpCursors.add(cursor);
add(cursors);
charHitbox.active = false;
charHitbox.scrollFactor.set();
selectSound.loadEmbedded(Paths.sound('CS_select'));
selectSound.pitch = 1;
selectSound.volume = 0.7;
FlxG.sound.defaultSoundGroup.add(selectSound);
FlxG.sound.list.add(selectSound);
unlockSound.loadEmbedded(Paths.sound('CS_unlock'));
unlockSound.pitch = 1;
unlockSound.volume = 0;
unlockSound.play(true);
@ -364,18 +328,13 @@ class CharSelectSubState extends MusicBeatSubState
FlxG.sound.list.add(unlockSound);
lockedSound.loadEmbedded(Paths.sound('CS_locked'));
lockedSound.pitch = 1;
lockedSound.volume = 1.;
FlxG.sound.defaultSoundGroup.add(lockedSound);
FlxG.sound.list.add(lockedSound);
staticSound.loadEmbedded(Paths.sound('static loop'));
staticSound.pitch = 1;
staticSound.looped = true;
staticSound.volume = 0.6;
FlxG.sound.defaultSoundGroup.add(staticSound);
@ -398,12 +357,6 @@ class CharSelectSubState extends MusicBeatSubState
FlxTween.tween(member, {y: member.y - 300}, 1, {ease: FlxEase.expoOut});
}
cursor.scrollFactor.set();
cursorBlue.scrollFactor.set();
cursorDarkBlue.scrollFactor.set();
FlxTween.color(cursor, 0.2, 0xFFFFFF00, 0xFFFFCC00, {type: PINGPONG});
FlxG.debugger.addTrackerProfile(new TrackerProfile(CharSelectSubState, ["curChar", "grpXSpread", "grpYSpread"]));
FlxG.debugger.track(this);
@ -462,7 +415,6 @@ class CharSelectSubState extends MusicBeatSubState
introSound = new FunkinSound();
introSound.loadEmbedded(Paths.sound('CS_Lights'));
introSound.pitch = 1;
introSound.volume = 0;
FlxG.sound.defaultSoundGroup.add(introSound);
@ -594,9 +546,6 @@ class CharSelectSubState extends MusicBeatSubState
}
var xThing = (copy - index - 2) * -1;
// Look, I'd write better code but I had better aneurysms, my bad - Cheems
// felt - Zack
// Krue - Abnormal
cursorY = yThing;
cursorX = xThing;
@ -733,10 +682,7 @@ class CharSelectSubState extends MusicBeatSubState
}
#end
FlxTween.tween(cursor, {alpha: 0}, 0.8, {ease: FlxEase.expoOut});
FlxTween.tween(cursorBlue, {alpha: 0}, 0.8, {ease: FlxEase.expoOut});
FlxTween.tween(cursorDarkBlue, {alpha: 0}, 0.8, {ease: FlxEase.expoOut});
FlxTween.tween(cursorConfirmed, {alpha: 0}, 0.8, {ease: FlxEase.expoOut});
FlxTween.tween(cursors, {alpha: 0}, 0.8, {ease: FlxEase.expoOut});
FlxTween.tween(barthing, {y: barthing.y + 80}, 0.8, {ease: FlxEase.backIn});
FlxTween.tween(nametag, {y: nametag.y + 80}, 0.8, {ease: FlxEase.backIn});
@ -774,10 +720,8 @@ class CharSelectSubState extends MusicBeatSubState
var holdTmrDown:Float = 0;
var holdTmrLeft:Float = 0;
var holdTmrRight:Float = 0;
var spamUp:Bool = false;
var spamDown:Bool = false;
var spamLeft:Bool = false;
var spamRight:Bool = false;
var spamDirections:FlxDirectionFlags = NONE;
var initSpam = 0.5;
var mobileDeny:Bool = false;
var mobileAccept:Bool = false;
@ -792,9 +736,6 @@ class CharSelectSubState extends MusicBeatSubState
mobileAccept = false;
if (controls.UI_UP_R || controls.UI_DOWN_R || controls.UI_LEFT_R || controls.UI_RIGHT_R #if FEATURE_TOUCH_CONTROLS || TouchUtil.justReleased #end)
selectSound.pitch = 1;
if (allowInput && !pressedSelect)
{
#if FEATURE_TOUCH_CONTROLS
@ -811,7 +752,7 @@ class CharSelectSubState extends MusicBeatSubState
{
cursorX = indexCX;
cursorY = indexCY;
cursorDenied.visible = false;
cursors.resetDeny();
selectSound.play(true);
}
else if (TouchUtil.justPressed)
@ -830,45 +771,10 @@ class CharSelectSubState extends MusicBeatSubState
}
#end
if (controls.UI_UP) holdTmrUp += elapsed;
if (controls.UI_UP_R)
{
holdTmrUp = 0;
spamUp = false;
}
if (controls.UI_DOWN) holdTmrDown += elapsed;
if (controls.UI_DOWN_R)
{
holdTmrDown = 0;
spamDown = false;
}
if (controls.UI_LEFT) holdTmrLeft += elapsed;
if (controls.UI_LEFT_R)
{
holdTmrLeft = 0;
spamLeft = false;
}
if (controls.UI_RIGHT) holdTmrRight += elapsed;
if (controls.UI_RIGHT_R)
{
holdTmrRight = 0;
spamRight = false;
}
var initSpam = 0.5;
if (holdTmrUp >= initSpam) spamUp = true;
if (holdTmrDown >= initSpam) spamDown = true;
if (holdTmrLeft >= initSpam) spamLeft = true;
if (holdTmrRight >= initSpam) spamRight = true;
if (controls.UI_UP_P)
{
cursorY -= 1;
cursorDenied.visible = false;
cursors.resetDeny();
holdTmrUp = 0;
@ -877,14 +783,14 @@ class CharSelectSubState extends MusicBeatSubState
if (controls.UI_DOWN_P)
{
cursorY += 1;
cursorDenied.visible = false;
cursors.resetDeny();
holdTmrDown = 0;
selectSound.play(true);
}
if (controls.UI_LEFT_P)
{
cursorX -= 1;
cursorDenied.visible = false;
cursors.resetDeny();
holdTmrLeft = 0;
selectSound.play(true);
@ -892,30 +798,49 @@ class CharSelectSubState extends MusicBeatSubState
if (controls.UI_RIGHT_P)
{
cursorX += 1;
cursorDenied.visible = false;
cursors.resetDeny();
holdTmrRight = 0;
selectSound.play(true);
}
if (controls.UI_UP) holdTmrUp += elapsed;
if (controls.UI_UP_R || !controls.UI_UP)
{
holdTmrUp = 0;
spamDirections = spamDirections.without(UP);
}
if (controls.UI_DOWN) holdTmrDown += elapsed;
if (controls.UI_DOWN_R || !controls.UI_DOWN)
{
holdTmrDown = 0;
spamDirections = spamDirections.without(DOWN);
}
if (controls.UI_LEFT) holdTmrLeft += elapsed;
if (controls.UI_LEFT_R || !controls.UI_LEFT)
{
holdTmrLeft = 0;
spamDirections = spamDirections.without(LEFT);
}
if (controls.UI_RIGHT) holdTmrRight += elapsed;
if (controls.UI_RIGHT_R || !controls.UI_RIGHT)
{
holdTmrRight = 0;
spamDirections = spamDirections.without(RIGHT);
}
if (holdTmrUp >= initSpam) spamDirections = spamDirections.with(UP);
if (holdTmrDown >= initSpam) spamDirections = spamDirections.with(DOWN);
if (holdTmrLeft >= initSpam) spamDirections = spamDirections.with(LEFT);
if (holdTmrRight >= initSpam) spamDirections = spamDirections.with(RIGHT);
if (controls.BACK_P) goBack();
}
if (cursorX < -1)
{
cursorX = 1;
}
if (cursorX > 1)
{
cursorX = -1;
}
if (cursorY < -1)
{
cursorY = 1;
}
if (cursorY > 1)
{
cursorY = -1;
}
cursorX = FlxMath.wrap(cursorX, -1, 1);
cursorY = FlxMath.wrap(cursorY, -1, 1);
var currentCharacter:String = availableChars[getCurrentSelected()] ?? Constants.DEFAULT_CHARACTER;
if (availableChars.exists(getCurrentSelected()) && PlayerRegistry.instance.isCharacterSeen(currentCharacter))
@ -926,8 +851,7 @@ class CharSelectSubState extends MusicBeatSubState
if (allowInput && pressedSelect && (controls.BACK_P #if FEATURE_TOUCH_CONTROLS || (mobileDeny && TouchUtil.justReleased) #end))
{
mobileDeny = false;
cursorConfirmed.visible = false;
grpCursors.visible = true;
cursors.unconfirm();
dispatchEvent(new CharacterSelectScriptEvent(CHARACTER_DESELECTED, curChar));
@ -962,17 +886,11 @@ class CharSelectSubState extends MusicBeatSubState
if (allowInput && !pressedSelect && (controls.ACCEPT_P || mobileAccept))
{
mobileDeny = false;
spamUp = false;
spamDown = false;
spamLeft = false;
spamRight = false;
spamDirections = NONE;
cursorConfirmed.visible = true;
cursorConfirmed.animation.play("idle", true);
cursors.confirm();
grpCursors.visible = false;
FlxG.sound.play(Paths.sound('CS_confirm'));
FunkinSound.playOnce(Paths.sound('CS_confirm'));
dispatchEvent(new CharacterSelectScriptEvent(CHARACTER_CONFIRMED, curChar));
@ -985,9 +903,11 @@ class CharSelectSubState extends MusicBeatSubState
FlxTween.tween(FlxG.sound.music, {pitch: 0.1}, 1, {ease: FlxEase.quadInOut});
FlxTween.tween(FlxG.sound.music, {volume: 0.0}, 1.5, {ease: FlxEase.quadInOut});
playerChill.anim.play("select");
gfChill.anim.play("confirm", true);
gfChill.anim.curAnim.looped = true;
pressedSelect = true;
selectTimer.start(1.5, (_) -> {
goToFreeplay();
@ -1007,50 +927,30 @@ class CharSelectSubState extends MusicBeatSubState
if (allowInput && (controls.ACCEPT_P || mobileAccept))
{
cursorDenied.visible = true;
playerChill.anim.play("cannot select Label", true);
lockedSound.play(true);
HapticUtil.vibrate(0, 0.2);
cursorDenied.animation.play('idle', true);
cursorDenied.animation.onFinish.add((_) -> {
cursorDenied.visible = false;
});
cursors.deny();
}
}
updateLockAnims();
if (autoFollow == true)
if (autoFollow)
{
camFollow.screenCenter();
camFollow.x += cursorX * 10;
camFollow.y += cursorY * 10;
}
cursorLocIntended.x = (cursorFactor * cursorX) + (FlxG.width / 2) - cursor.width / 2;
cursorLocIntended.y = (cursorFactor * cursorY) + (FlxG.height / 2) - cursor.height / 2;
cursorLocIntended.x = (cursorFactor * cursorX) + (FlxG.width / 2) - cursors.main.width / 2;
cursorLocIntended.y = (cursorFactor * cursorY) + (FlxG.height / 2) - cursors.main.height / 2;
cursorLocIntended.x += cursorOffsetX;
cursorLocIntended.y += cursorOffsetY;
cursor.x = MathUtil.snap(MathUtil.smoothLerpPrecision(cursor.x, cursorLocIntended.x, elapsed, 0.1), cursorLocIntended.x, 1);
cursor.y = MathUtil.snap(MathUtil.smoothLerpPrecision(cursor.y, cursorLocIntended.y, elapsed, 0.1), cursorLocIntended.y, 1);
cursorBlue.x = MathUtil.smoothLerpPrecision(cursorBlue.x, cursor.x, elapsed, 0.202);
cursorBlue.y = MathUtil.smoothLerpPrecision(cursorBlue.y, cursor.y, elapsed, 0.202);
cursorDarkBlue.x = MathUtil.smoothLerpPrecision(cursorDarkBlue.x, cursorLocIntended.x, elapsed, 0.404);
cursorDarkBlue.y = MathUtil.smoothLerpPrecision(cursorDarkBlue.y, cursorLocIntended.y, elapsed, 0.404);
cursorConfirmed.x = cursor.x - 2;
cursorConfirmed.y = cursor.y - 4;
cursorDenied.x = cursor.x - 2;
cursorDenied.y = cursor.y - 4;
cursors.lerpToLocation(cursorLocIntended);
}
function goBack():Void
@ -1120,29 +1020,29 @@ class CharSelectSubState extends MusicBeatSubState
function spamOnStep():Void
{
if (spamUp || spamDown || spamLeft || spamRight)
if (spamDirections.hasAny(ANY))
{
if (selectSound.pitch > 5) selectSound.pitch = 5;
selectSound.play(true);
cursorDenied.visible = false;
cursors.resetDeny();
if (spamUp)
if (spamDirections.has(UP))
{
cursorY -= 1;
holdTmrUp = 0;
}
if (spamDown)
if (spamDirections.has(DOWN))
{
cursorY += 1;
holdTmrDown = 0;
}
if (spamLeft)
if (spamDirections.has(LEFT))
{
cursorX -= 1;
holdTmrLeft = 0;
}
if (spamRight)
if (spamDirections.has(RIGHT))
{
cursorX += 1;
holdTmrRight = 0;
@ -1219,8 +1119,7 @@ class CharSelectSubState extends MusicBeatSubState
return gridPosition;
}
// Moved this code into a function because is now used twice
function setCursorPosition(index:Int)
function setCursorPosition(index:Int, instant:Bool = false):Void
{
var copy = 3;
var yThing = -1;
@ -1236,6 +1135,17 @@ class CharSelectSubState extends MusicBeatSubState
// Look, I'd write better code but I had better aneurysms, my bad - Cheems
cursorY = yThing;
cursorX = xThing;
if (instant)
{
cursorLocIntended.x = (cursorFactor * cursorX) + (FlxG.width / 2) - cursors.main.width / 2;
cursorLocIntended.y = (cursorFactor * cursorY) + (FlxG.height / 2) - cursors.main.height / 2;
cursorLocIntended.x += cursorOffsetX;
cursorLocIntended.y += cursorOffsetY;
cursors.snapToLocation(cursorLocIntended);
}
}
function set_curChar(value:String):String

View file

@ -175,27 +175,26 @@ class DebugBoundingState extends FlxState
function updateOnionSkin():Void
{
if (swagChar == null) return;
if (swagChar.hasAnimation("idle")) swagChar.playAnimation("idle", true);
onionSkinChar.alpha = 0.6;
onionSkinChar.flipX = swagChar.flipX;
if (onionSkinChar.hasAnimation("idle"))
{
onionSkinChar.playAnimation("idle", true);
}
else if (onionSkinChar.hasAnimation("danceLeft"))
{
onionSkinChar.playAnimation("danceLeft", true);
}
else if (onionSkinChar.hasAnimation("danceRight"))
{
onionSkinChar.playAnimation("danceRight", true);
}
else
{
onionSkinChar.playAnimation(currentAnimationName, true);
}
onionSkinChar.animation.pause();
onionSkinChar.flipX = swagChar.flipX;
onionSkinChar.scale.set(swagChar.scale.x, swagChar.scale.y);
onionSkinChar.updateHitbox();
onionSkinChar.offset.x = swagChar.offset.x + (swagChar.animOffsets[0] - swagChar.globalOffsets[0]) * swagChar.scale.x;
onionSkinChar.offset.y = swagChar.offset.y + (swagChar.animOffsets[1] - swagChar.globalOffsets[1]) * swagChar.scale.y;
swagChar.playAnimation(currentAnimationName, true);
}
function initOffsetView():Void

View file

@ -73,6 +73,7 @@ import funkin.ui.debug.charting.commands.RemoveStackedNotesCommand;
import funkin.ui.debug.charting.commands.SelectAllItemsCommand;
import funkin.ui.debug.charting.commands.SelectItemsCommand;
import funkin.ui.debug.charting.commands.SetItemSelectionCommand;
import funkin.ui.debug.charting.commands.SwitchDifficultyCommand;
import funkin.ui.debug.charting.components.ChartEditorEventSprite;
import funkin.ui.debug.charting.components.ChartEditorHoldNoteSprite;
import funkin.ui.debug.charting.components.ChartEditorMeasureTicks;
@ -385,12 +386,10 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
if (isViewDownscroll)
{
gridTiledSprite.y = -scrollPositionInPixels + (GRID_INITIAL_Y_POS);
measureTicks.y = gridTiledSprite.y;
}
else
{
gridTiledSprite.y = -scrollPositionInPixels + (GRID_INITIAL_Y_POS);
measureTicks.y = gridTiledSprite.y;
for (member in audioWaveforms.members)
{
@ -1335,12 +1334,17 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
function get_availableDifficulties():Array<String>
{
var m:Null<SongMetadata> = songMetadata.get(selectedVariation);
return getAvailableDifficulties(selectedVariation);
}
function getAvailableDifficulties(variation:String):Array<String>
{
var m:Null<SongMetadata> = songMetadata.get(variation);
return m?.playData?.difficulties ?? [Constants.DEFAULT_DIFFICULTY];
}
/**
* Retrieves the list of difficulties for ALL variations of the current song.
* Retrieves the list of (suffixed) difficulties for ALL variations of the current song.
*/
var allDifficulties(get, never):Array<String>;
@ -1738,6 +1742,10 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
noteTooltipsDirty = true;
notePreviewViewportBoundsDirty = true;
// Switching difficulties should automatically clear the selection.
currentNoteSelection = [];
currentEventSelection = [];
switchToCurrentInstrumental();
postLoadInstrumental();
@ -1761,6 +1769,10 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
noteTooltipsDirty = true;
notePreviewViewportBoundsDirty = true;
// Switching difficulties should automatically clear the selection.
currentNoteSelection = [];
currentEventSelection = [];
return selectedDifficulty;
}
@ -1939,6 +1951,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
*/
var menubarItemViewSubtitles:MenuCheckBox;
/**
* The `View -> Waveforms` menu item.
*/
var menubarItemViewWaveforms:MenuCheckBox;
/**
* The `View -> Increase Difficulty` menu item.
*/
@ -2154,11 +2171,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
*/
var notePreviewViewportBitmap:Null<BitmapData> = null;
/**
* The IMAGE used for the measure ticks. Updated by ChartEditorThemeHandler.
*/
var measureTickBitmap:Null<BitmapData> = null;
/**
* The IMAGE used for the offset ticks. Updated by ChartEditorThemeHandler.
*/
@ -2389,7 +2401,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
// Setup the onClick listeners for the UI after it's been created.
setupUIListeners();
setupContextMenu();
setupTurboKeyHandlers();
setupAutoSave();
@ -2717,10 +2728,10 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
measureTicks = new ChartEditorMeasureTicks(this);
var measureTicksWidth = (GRID_SIZE);
measureTicks.x = gridTiledSprite.x - measureTicksWidth;
measureTicks.y = MENU_BAR_HEIGHT + GRID_TOP_PAD;
measureTicks.zIndex = 20;
add(measureTicks);
handleMeasureTickPosition();
}
function buildNotePreview():Void
@ -3240,6 +3251,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
menubarItemViewSubtitles.onClick = event -> showSubtitles = menubarItemViewSubtitles.selected;
menubarItemViewSubtitles.selected = showSubtitles;
menubarItemViewWaveforms.onClick = event -> audioWaveforms.visible = menubarItemViewWaveforms.selected;
menubarItemViewWaveforms.selected = audioWaveforms.visible;
menubarItemDifficultyUp.onClick = _ -> incrementDifficulty(1);
menubarItemDifficultyDown.onClick = _ -> incrementDifficulty(-1);
@ -3354,21 +3368,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
// registerContextMenu(null, Paths.ui('chart-editor/context/test'));
}
function setupContextMenu():Void
{
Screen.instance.registerEvent(MouseEvent.RIGHT_MOUSE_UP, function(e:MouseEvent) {
var xPos = e.screenX;
var yPos = e.screenY;
onContextMenu(xPos, yPos);
});
}
function onContextMenu(xPos:Float, yPos:Float)
{
trace('User right clicked to open menu at (${xPos}, ${yPos})');
// this.openDefaultContextMenu(xPos, yPos);
}
function copySelection():Void
{
// Doesn't use a command because it's not undoable.
@ -3586,6 +3585,20 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
handlePostUpdate();
}
/**
* Function called when the game window loses focus.
*/
public override function onFocusLost():Void
{
super.onFocusLost();
// Stop the song upon tabbing out.
if (Preferences.autoPause)
{
stopAudioPlayback();
}
}
/**
* Beat hit while the song is playing.
*/
@ -4431,8 +4444,17 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
// TODO: TBH some of this should be using FlxMouseEventManager...
if (shouldHandleCursor)
// early return if we shouldn't handle the cursor at all
if (!shouldHandleCursor)
{
if (gridGhostNote != null) gridGhostNote.visible = false;
if (gridGhostHoldNote != null) gridGhostHoldNote.visible = false;
if (gridGhostEvent != null) gridGhostEvent.visible = false;
// Do not set Cursor.cursorMode here, because it will be set by the HaxeUI.
return;
}
// Over the course of this big conditional block,
// we determine what the cursor should look like,
// and fall back to the default cursor if none of the conditions are met.
@ -4932,7 +4954,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
}
var dragDistanceColumns:Int = cursorGridPos - noteGridPos;
if (dragTargetCurrentStep != dragDistanceSteps || dragTargetCurrentColumn != dragDistanceColumns)
if ((dragTargetCurrentColumn != dragDistanceColumns && overlapsGrid) || dragTargetCurrentStep != dragDistanceSteps)
{
// Play a sound as we drag.
this.playSound(Paths.sound('chartingSounds/noteLay'));
@ -5381,15 +5403,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
// Actually set the cursor mode to the one we specified earlier.
Cursor.cursorMode = targetCursorMode ?? Default;
}
else
{
if (gridGhostNote != null) gridGhostNote.visible = false;
if (gridGhostHoldNote != null) gridGhostHoldNote.visible = false;
if (gridGhostEvent != null) gridGhostEvent.visible = false;
// Do not set Cursor.cursorMode here, because it will be set by the HaxeUI.
}
}
function handleToolboxes():Void
{
@ -6202,7 +6215,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
var targetSong:Song;
try
{
targetSong = Song.buildRaw(currentSongId, songMetadata.values(), availableVariations, songChartData, playtestSongScripts, false);
targetSong = Song.buildRaw(currentSongId, songMetadata.values(), selectedVariation, songChartData, playtestSongScripts, false);
}
catch (e)
{
@ -6411,6 +6424,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
if (gridTiledSprite != null)
{
gridTiledSprite.height = songLengthInPixels;
measureTicks.setHeight(gridTiledSprite.height);
}
// Remove any notes past the end of the song.
@ -6472,6 +6486,46 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
difficultySelectDirty = true; // Force the Difficulty toolbox to update.
}
function cloneDifficulty(variation:String, difficulty:String, newVariation:String, newDifficulty:String, scrollSpeed:Float = 1.0):Void
{
var newVariationMetadata:Null<SongMetadata> = songMetadata.get(newVariation);
if (newVariationMetadata == null) return;
var oldChartData:Null<SongChartData> = songChartData.get(variation);
if (oldChartData == null)
{
// If we can't access the difficulty to copy, just create a blank one.
createDifficulty(newVariation, newDifficulty, scrollSpeed);
return;
};
var newNoteData:Null<Array<SongNoteData>> = oldChartData.notes.get(difficulty)?.clone();
if (newNoteData == null || newNoteData.length == 0)
{
// If we can't access the difficulty to copy, just create a blank one.
createDifficulty(newVariation, newDifficulty, scrollSpeed);
return;
};
// Actually add the new difficulty.
newVariationMetadata.playData.difficulties.push(newDifficulty);
var newChartData = songChartData.get(newVariation);
if (newChartData == null)
{
newChartData = new SongChartData([newDifficulty => scrollSpeed], [], [newDifficulty => newNoteData]);
songChartData.set(newVariation, newChartData);
}
else
{
newChartData.scrollSpeed.set(newDifficulty, scrollSpeed);
newChartData.notes.set(newDifficulty, newNoteData);
}
difficultySelectDirty = true; // Force the Difficulty toolbox to update.
}
function removeDifficulty(variation:String, difficulty:String):Void
{
var variationMetadata:Null<SongMetadata> = songMetadata.get(variation);
@ -6486,6 +6540,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
resultChartData.notes.remove(difficulty);
}
// Remove the variation if it is empty.
if (songMetadata.size() > 1)
{
if (variation != Constants.DEFAULT_VARIATION && variationMetadata.playData.difficulties.length == 0)
@ -6497,11 +6552,13 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
if (variation == selectedVariation)
{
var firstVariation = songMetadata.keyValues()[0];
// Directly set the variation, since you can't undo removing the difficulty.
if (firstVariation != null) selectedVariation = firstVariation;
variationMetadata = songMetadata.get(selectedVariation);
}
}
// Directly set the difficulty rather than using a command, since you can't undo removing the difficulty.
if (selectedDifficulty == difficulty
|| !variationMetadata.playData.difficulties.contains(selectedDifficulty)) selectedDifficulty = variationMetadata.playData.difficulties[0];
@ -6550,10 +6607,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
// Go to the previous variation, then last difficulty in that variation.
var currentVariationIndex:Int = availableVariations.indexOf(selectedVariation);
var prevVariation = availableVariations[currentVariationIndex - 1];
selectedVariation = prevVariation;
var prevVariationDifficulties:Array<String> = getAvailableDifficulties(prevVariation);
var prevDifficulty = prevVariationDifficulties[prevVariationDifficulties.length - 1];
var prevDifficulty = availableDifficulties[availableDifficulties.length - 1];
selectedDifficulty = prevDifficulty;
trace('${selectedVariation}:${selectedDifficulty} -> ${prevVariation}:${prevDifficulty}');
performCommand(new SwitchDifficultyCommand(selectedDifficulty, prevDifficulty, selectedVariation, prevVariation));
Conductor.instance.mapTimeChanges(this.currentSongMetadata.timeChanges);
updateTimeSignature();
@ -6565,7 +6623,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
{
// Go to previous difficulty in this variation.
var prevDifficulty = availableDifficulties[currentDifficultyIndex - 1];
selectedDifficulty = prevDifficulty;
trace('${selectedVariation}:${selectedDifficulty} -> ${selectedVariation}:${prevDifficulty}');
performCommand(new SwitchDifficultyCommand(selectedDifficulty, prevDifficulty, selectedVariation, selectedVariation));
this.refreshToolbox(CHART_EDITOR_TOOLBOX_METADATA_LAYOUT);
this.refreshToolbox(CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT);
@ -6581,10 +6640,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
// Go to next variation, then first difficulty in that variation.
var currentVariationIndex:Int = availableVariations.indexOf(selectedVariation);
var nextVariation = availableVariations[currentVariationIndex + 1];
selectedVariation = nextVariation;
var nextVariationDifficulties:Array<String> = getAvailableDifficulties(nextVariation);
var nextDifficulty = nextVariationDifficulties[0];
var nextDifficulty = availableDifficulties[0];
selectedDifficulty = nextDifficulty;
trace('${selectedVariation}:${selectedDifficulty} -> ${nextVariation}:${nextDifficulty}');
performCommand(new SwitchDifficultyCommand(selectedDifficulty, nextDifficulty, selectedVariation, nextVariation));
this.refreshToolbox(CHART_EDITOR_TOOLBOX_METADATA_LAYOUT);
this.refreshToolbox(CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT);
@ -6593,7 +6653,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
{
// Go to next difficulty in this variation.
var nextDifficulty = availableDifficulties[currentDifficultyIndex + 1];
selectedDifficulty = nextDifficulty;
trace('${selectedVariation}:${selectedDifficulty} -> ${selectedVariation}:${nextDifficulty}');
performCommand(new SwitchDifficultyCommand(selectedDifficulty, nextDifficulty, selectedVariation, selectedVariation));
this.refreshToolbox(CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT);
this.refreshToolbox(CHART_EDITOR_TOOLBOX_METADATA_LAYOUT);
@ -6719,32 +6780,16 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
/**
* Updates the measure tick bitmap forcibly to make sure it's correct.
*/
function updateTimeSignature():Void
{
this.updateMeasureTicks(true);
gridTiledSprite.loadGraphic(gridBitmap);
}
function updateTimeSignature():Void {}
/**
* Handle positioning the measure ticks sprite.
*/
function handleMeasureTickPosition():Void
{
this.updateMeasureTicks();
var currentMeasureTime = Conductor.instance.getMeasureTimeInMs(Math.floor(Conductor.instance.getTimeInMeasures(scrollPositionInMs)));
var currentMeasurePos = currentMeasureTime < 0 ? 0 : Conductor.instance.getTimeInSteps(currentMeasureTime) * GRID_SIZE;
measureTicks.y = gridTiledSprite?.y + currentMeasurePos;
// Make sure the measure tick bitmap does not past the grid itself.
var totalMeasureTicksHeight:Float = currentMeasurePos + measureTickBitmap.height;
if (totalMeasureTicksHeight >= songLengthInPixels)
{
var spillOver:Float = totalMeasureTicksHeight - songLengthInPixels;
measureTicks.setClipRect(new FlxRect(0, 0, measureTickBitmap.width, measureTickBitmap.height - spillOver));
}
else
{
measureTicks.setClipRect(null);
}
// var currentMeasureTime = Conductor.instance.getMeasureTimeInMs(Math.floor(Conductor.instance.getTimeInMeasures(scrollPositionInMs)));
// var currentMeasurePos = currentMeasureTime < 0 ? 0 : Conductor.instance.getTimeInSteps(currentMeasureTime) * GRID_SIZE;
measureTicks.y = gridTiledSprite?.y;
}
/**

View file

@ -21,24 +21,42 @@ class SwitchDifficultyCommand implements ChartEditorCommand
this.newVariation = newVariation;
}
/**
* Perform the difficulty switch.
* @param state The ChartEditorState to perform the action on.
*/
public function execute(state:ChartEditorState):Void
{
state.selectedVariation = newVariation != null ? newVariation : prevVariation;
state.selectedDifficulty = newDifficulty != null ? newDifficulty : prevDifficulty;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
markDirty(state);
}
/**
* Reverse the difficulty switch.
* @param state The ChartEditorState to perform the action on.
*/
public function undo(state:ChartEditorState):Void
{
state.selectedVariation = prevVariation != null ? prevVariation : newVariation;
state.selectedDifficulty = prevDifficulty != null ? prevDifficulty : newDifficulty;
markDirty(state);
}
function markDirty(state:ChartEditorState):Void
{
state.refreshToolbox(ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT);
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
}
/**
* @param state The ChartEditorState to perform the action on.
* @return Whether or not this instance of the command should be added to the history.
* If the command didn't actually change anything, return `false` to prevent polluting the history.
*/
public function shouldAddToHistory(state:ChartEditorState):Bool
{
// Add to the history if we actually performed an action.
@ -47,7 +65,7 @@ class SwitchDifficultyCommand implements ChartEditorCommand
public function toString():String
{
return 'Switch Difficulty';
return 'Switch Difficulty to $newDifficulty ($newVariation)';
}
}
#end

View file

@ -1,29 +1,63 @@
package funkin.ui.debug.charting.components;
#if FEATURE_CHART_EDITOR
import flixel.addons.display.FlxTiledSprite;
import flixel.FlxSprite;
import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
import flixel.math.FlxRect;
import flixel.text.FlxText;
import flixel.util.FlxColor;
import funkin.graphics.FunkinSprite;
import openfl.display.BitmapData;
import openfl.geom.Rectangle;
/**
* Handles the display of the measure ticks and numbers on the left side.
*/
@:nullSafety
@:access(funkin.ui.debug.charting.ChartEditorState)
class ChartEditorMeasureTicks extends FlxTypedSpriteGroup<FlxSprite>
{
/**
* The owning ChartEditorState.
*/
var chartEditorState:ChartEditorState;
var measureTicksSprite:FlxSprite;
var measureNumbers:Array<FlxText> = [];
/**
* The measure ticks underneath the numbers.
*/
var measureTicksSprite:FlxTiledSprite = new FlxTiledSprite(ChartEditorState.GRID_SIZE, ChartEditorState.GRID_SIZE * 16);
public var measureLengthsInPixels:Array<Int> = [];
/**
* The numbers that display the current measure number.
* This is a group so we can kill and recycle its members.
*/
var measureNumbers:FlxTypedSpriteGroup<FlxText> = new FlxTypedSpriteGroup<FlxText>();
/**
* The horizontal bars over the grid at each measure tick.
*/
var measureDividers:FlxTypedSpriteGroup<FlxSprite> = new FlxTypedSpriteGroup<FlxSprite>();
/**
* The positions of each measure tick, in pixels, relative to the start of the song.
*/
var measurePositions:Array<Float> = [];
/**
* A map of the
* @param value
* @return Float
*/
override function set_y(value:Float):Float
{
var result = super.set_y(value);
// Don't update if the value hasn't changed.
if (this.y == value) return value;
updateMeasureNumber();
super.set_y(value);
return result;
updateMeasureNumbers();
return this.y;
}
public function new(chartEditorState:ChartEditorState)
@ -32,59 +66,182 @@ class ChartEditorMeasureTicks extends FlxTypedSpriteGroup<FlxSprite>
this.chartEditorState = chartEditorState;
measureTicksSprite = new FlxSprite(0, 0);
add(measureTicksSprite);
add(measureNumbers);
add(measureDividers);
for (i in 0...5)
buildMeasureTicksSprite();
updateMeasureNumbers(true);
}
/**
* Set the overall height of the measure ticks.
* @param height The desired height in pixels.
*/
public function setHeight(height:Float):Void
{
measureTicksSprite.height = height;
}
public function updateTheme():Void
{
buildMeasureTicksSprite();
updateMeasureNumbers(true);
}
function buildMeasureTicksSprite():Void
{
var backingColor:FlxColor = switch (chartEditorState.currentTheme)
{
case Light: ChartEditorThemeHandler.MEASTURE_TICKS_BACKING_COLOR_LIGHT;
case Dark: ChartEditorThemeHandler.MEASTURE_TICKS_BACKING_COLOR_DARK;
default: ChartEditorThemeHandler.MEASTURE_TICKS_BACKING_COLOR_LIGHT;
};
var dividerColor:FlxColor = switch (chartEditorState.currentTheme)
{
case Light: ChartEditorThemeHandler.GRID_MEASURE_DIVIDER_COLOR_LIGHT;
case Dark: ChartEditorThemeHandler.GRID_MEASURE_DIVIDER_COLOR_DARK;
default: ChartEditorThemeHandler.GRID_MEASURE_DIVIDER_COLOR_LIGHT;
};
// TODO: This does NOT account for time signature, and always assumes 4/4!
// Better to have the little lines not line up than be required to redraw the image every frame,
// but we need to fix this eventually.
var stepsPerMeasure:Int = Constants.STEPS_PER_BEAT * 4;
// Start the bitmap with the basic gray color.
var measureTickBitmap = new BitmapData(ChartEditorState.GRID_SIZE, ChartEditorState.GRID_SIZE * 16, true, backingColor);
// Draw the measure ticks at the top and bottom.
measureTickBitmap.fillRect(new Rectangle(0, 0, ChartEditorState.GRID_SIZE, ChartEditorThemeHandler.MEASURE_TICKS_MEASURE_WIDTH / 2), dividerColor);
var bottomTickY:Float = measureTickBitmap.height - (ChartEditorThemeHandler.MEASURE_TICKS_MEASURE_WIDTH / 2);
measureTickBitmap.fillRect(new Rectangle(0, bottomTickY, ChartEditorState.GRID_SIZE, ChartEditorThemeHandler.MEASURE_TICKS_MEASURE_WIDTH / 2),
dividerColor);
// Draw the beat ticks and dividers, and step ticks. No need for two seperate loops thankfully.
for (i in 1...stepsPerMeasure)
{
if ((i % Constants.STEPS_PER_BEAT) == 0) // If we're on a beat, draw a beat tick and divider.
{
var beatTickY:Float = ChartEditorState.GRID_SIZE * i - (ChartEditorThemeHandler.MEASURE_TICKS_BEAT_WIDTH / 2);
var beatTickLength:Float = ChartEditorState.GRID_SIZE * 2 / 3;
measureTickBitmap.fillRect(new Rectangle(0, beatTickY, beatTickLength, ChartEditorThemeHandler.MEASURE_TICKS_BEAT_WIDTH), dividerColor);
}
else
{
// Draw a step tick.
var stepTickY:Float = ChartEditorState.GRID_SIZE * i - (ChartEditorThemeHandler.MEASURE_TICKS_STEP_WIDTH / 2);
var stepTickLength:Float = ChartEditorState.GRID_SIZE * 1 / 3;
measureTickBitmap.fillRect(new Rectangle(0, stepTickY, stepTickLength, ChartEditorThemeHandler.MEASURE_TICKS_STEP_WIDTH), dividerColor);
}
}
// Finally, set the sprite to use the image.
measureTicksSprite.loadGraphic(measureTickBitmap);
// Destroy these so they get rebuilt with the right theme later.
measureNumbers.forEach(function(measureNumber:FlxText) {
measureNumber.destroy();
});
measureNumbers.clear();
// Destroy these so they get rebuilt with the right theme later.
measureDividers.forEach(function(measureDivider:FlxSprite) {
measureDivider.destroy();
});
measureDividers.clear();
}
// The last measure number we updated the ticks on.
var previousMeasure:Int = 0;
function updateMeasureNumbers(force:Bool = false):Void
{
if (chartEditorState == null || Conductor.instance == null) return;
// Get the time at the top of the screen, in measures, rounded down.
// This is the earliest measure we'll need to display a tick for.
var currentMeasure:Int = Math.floor(Conductor.instance.getTimeInMeasures(chartEditorState.scrollPositionInMs));
if (previousMeasure == currentMeasure && !force) return;
if (currentMeasure < 0) currentMeasure = previousMeasure = 0;
// Remove existing measure numbers.
measureNumbers.forEachAlive(function(measureNumber:FlxText) {
measureNumber.kill();
});
measureDividers.forEachAlive(function(measureDivider:FlxSprite) {
measureDivider.kill();
});
final ARBITRARY_LIMIT = 5;
for (i in 0...ARBITRARY_LIMIT)
{
var targetMeasure:Int = currentMeasure + i - 1;
if (targetMeasure < 0) continue;
// TODO: This math is kinda awkward but DOES account for time signatures,
// might want some cleanup though? Maybe add a `getMeasureTimeInSteps` method?
var measureTimeInMs:Float = Conductor.instance.getMeasureTimeInMs(targetMeasure);
var measureTimeInSteps:Float = Conductor.instance.getTimeInSteps(measureTimeInMs);
var measureTimeInPixels:Float = measureTimeInSteps * ChartEditorState.GRID_SIZE;
var relativeMeasureTimeInPixels:Float = measureTimeInPixels + this.y;
final SCREEN_PADDING:Float = ChartEditorState.GRID_SIZE / 2;
// Above the visible area, keep going.
if (relativeMeasureTimeInPixels < 0 - SCREEN_PADDING)
{
continue;
}
// Below the visible area, quit it.
if (relativeMeasureTimeInPixels > FlxG.height + SCREEN_PADDING)
{
break;
}
// Else, display a number.
// Reuse an existing number. If we need a new number, create one with makeMeasureNumber().
final REVIVE:Bool = true;
var measureNumber = measureNumbers.recycle(makeMeasureNumber, false, REVIVE);
trace('Placing measure number. $relativeMeasureTimeInPixels -> $targetMeasure');
// Measures are base ZERO gah!
final OFFSET = 2;
measureNumber.text = '${targetMeasure + 1}';
measureNumber.y = relativeMeasureTimeInPixels + OFFSET;
measureNumber.x = this.x;
// Place a measure divider too.
var measureDivider = measureDividers.recycle(makeMeasureDivider, false, REVIVE);
measureDivider.y = relativeMeasureTimeInPixels - (ChartEditorThemeHandler.MEASURE_TICKS_MEASURE_WIDTH / 2);
measureDivider.x = this.x + (measureTicksSprite.width);
}
}
function makeMeasureNumber():FlxText
{
var measureNumber = new FlxText(0, 0, ChartEditorState.GRID_SIZE, "1");
measureNumber.setFormat(Paths.font('vcr.ttf'), 20, FlxColor.WHITE);
measureNumber.borderStyle = FlxTextBorderStyle.OUTLINE;
measureNumber.borderColor = FlxColor.BLACK;
add(measureNumber);
measureNumbers.push(measureNumber);
}
// Need these two lines or the ticks don't render before loading a chart!
chartEditorState.updateMeasureTicks(true);
reloadTickBitmap();
return measureNumber;
}
public function reloadTickBitmap():Void
function makeMeasureDivider():FlxSprite
{
measureTicksSprite.loadGraphic(chartEditorState.measureTickBitmap);
}
public function setClipRect(rect:Null<FlxRect>):Void
var dividerColor:FlxColor = switch (chartEditorState.currentTheme)
{
measureTicksSprite.clipRect = rect;
}
case Light: ChartEditorThemeHandler.GRID_MEASURE_DIVIDER_COLOR_LIGHT;
case Dark: ChartEditorThemeHandler.GRID_MEASURE_DIVIDER_COLOR_DARK;
default: ChartEditorThemeHandler.GRID_MEASURE_DIVIDER_COLOR_LIGHT;
};
/**
* Update all 5 measure numbers, since that's the most we can really see at a time, even if barely.
* Please excuse the horror you're about to witness.
* Welp, you gotta go back in commits to see it now.
*/
function updateMeasureNumber()
{
if (measureLengthsInPixels.length == 0 || measureLengthsInPixels == null) return;
var currentMeasure:Int = Math.floor(Conductor.instance?.getTimeInMeasures(chartEditorState.scrollPositionInMs));
for (i in 0...measureNumbers.length)
{
var measureNumber:FlxText = measureNumbers[i];
if (measureNumber == null) continue;
var measureNumberPosition = measureLengthsInPixels[i];
measureNumber.y = this.y + measureNumberPosition;
// Show the measure number only if it isn't beneath the end of the note grid.
// Using measureNumber + 1 because the cut-off bar at the bottom is technically a bar, but it looks bad if a measure number shows up there.
var fixedMeasureNumberValue = currentMeasure + i + 1;
if (fixedMeasureNumberValue < Math.ceil(Conductor.instance?.getTimeInMeasures(chartEditorState.songLengthInMs)))
measureNumber.text = '${fixedMeasureNumberValue}';
else
measureNumber.text = '';
}
var measureDivider = new FunkinSprite().makeSolidColor(ChartEditorState.GRID_SIZE * ChartEditorThemeHandler.TOTAL_COLUMN_COUNT,
ChartEditorThemeHandler.MEASURE_TICKS_MEASURE_WIDTH, dividerColor);
return measureDivider;
}
}
#end

View file

@ -63,6 +63,8 @@ class ChartEditorDialogHandler
static final CHART_EDITOR_DIALOG_USER_GUIDE_LAYOUT:String = Paths.ui('chart-editor/dialogs/user-guide');
static final CHART_EDITOR_DIALOG_ADD_VARIATION_LAYOUT:String = Paths.ui('chart-editor/dialogs/add-variation');
static final CHART_EDITOR_DIALOG_ADD_DIFFICULTY_LAYOUT:String = Paths.ui('chart-editor/dialogs/add-difficulty');
static final CHART_EDITOR_DIALOG_CLONE_DIFFICULTY_LAYOUT:String = Paths.ui('chart-editor/dialogs/clone-difficulty');
static final CHART_EDITOR_DIALOG_MOVE_DIFFICULTY_LAYOUT:String = Paths.ui('chart-editor/dialogs/move-difficulty');
static final CHART_EDITOR_DIALOG_BACKUP_AVAILABLE_LAYOUT:String = Paths.ui('chart-editor/dialogs/backup-available');
/**
@ -1386,6 +1388,86 @@ class ChartEditorDialogHandler
return dialog;
}
/**
* Builds and opens a dialog where the user can copy an existing difficulty for a song.
* @param state The current chart editor state.
* @param deleteOriginal Whether to delete the original difficulty after copying.
* This essentially turns the copy into a move.
* @param closable Whether the dialog can be closed by the user.
* @return The dialog that was opened.
*/
public static function openCloneDifficultyDialog(state:ChartEditorState, deleteOriginal:Bool, closable:Bool = true):Dialog
{
var layout = deleteOriginal ? CHART_EDITOR_DIALOG_MOVE_DIFFICULTY_LAYOUT : CHART_EDITOR_DIALOG_CLONE_DIFFICULTY_LAYOUT;
var dialog:Null<Dialog> = openDialog(state, layout, true, false);
if (dialog == null) throw 'Could not locate Clone/Move Difficulty dialog';
var difficultyForm:Null<Form> = dialog.findComponent('difficultyForm', Form);
if (difficultyForm == null) throw 'Could not locate difficultyForm Form in Clone Difficulty dialog';
var buttonCancel:Null<Button> = dialog.findComponent('dialogCancel', Button);
if (buttonCancel == null) throw 'Could not locate dialogCancel button in Clone Difficulty dialog';
buttonCancel.onClick = function(_) {
dialog.hideDialog(DialogButton.CANCEL);
}
var dialogClone:Null<Button> = dialog.findComponent('dialogClone', Button);
if (dialogClone == null) throw 'Could not locate dialogClone button in Clone Difficulty dialog';
dialogClone.onClick = function(_) {
// This performs validation before the onSubmit callback is called.
difficultyForm.submit();
}
var dialogVariation:Null<DropDown> = dialog.findComponent('dialogVariation', DropDown);
if (dialogVariation == null) throw 'Could not locate dialogVariation DropDown in Clone Variation dialog';
dialogVariation.value = ChartEditorDropdowns.populateDropdownWithVariations(dialogVariation, state, true);
var labelScrollSpeed:Null<Label> = dialog.findComponent('labelScrollSpeed', Label);
if (labelScrollSpeed == null) throw 'Could not find labelScrollSpeed component.';
var inputScrollSpeed:Null<Slider> = dialog.findComponent('inputScrollSpeed', Slider);
if (inputScrollSpeed == null) throw 'Could not find inputScrollSpeed component.';
inputScrollSpeed.onChange = function(event:UIEvent) {
labelScrollSpeed.text = 'Scroll Speed: ${inputScrollSpeed.value}x';
};
inputScrollSpeed.value = state.currentSongChartScrollSpeed;
labelScrollSpeed.text = 'Scroll Speed: ${inputScrollSpeed.value}x';
difficultyForm.onSubmit = function(_) {
trace('Clone Difficulty dialog submitted, validation succeeded!');
var dialogDifficultyName:Null<TextField> = dialog.findComponent('dialogDifficultyName', TextField);
if (dialogDifficultyName == null) throw 'Could not locate dialogDifficultyName TextField in Add Difficulty dialog';
var variationToClone:String = state.selectedVariation;
var difficultyToClone:String = state.selectedDifficulty;
var targetVariation:String = dialogVariation.value.id;
var targetDifficulty:String = dialogDifficultyName.text.toLowerCase();
state.cloneDifficulty(variationToClone, difficultyToClone, targetVariation, targetDifficulty, inputScrollSpeed.value ?? 1.0);
if (deleteOriginal)
{
state.removeDifficulty(variationToClone, difficultyToClone);
state.selectedDifficulty = targetDifficulty;
state.selectedVariation = targetVariation;
state.refreshToolbox(ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT);
state.refreshToolbox(ChartEditorState.CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT);
state.success('Move Difficulty', 'Moved difficulty "$difficultyToClone" to "${dialogDifficultyName.text.toLowerCase()}"');
}
else
{
state.success('Clone Difficulty', 'Cloned difficulty "$difficultyToClone" to "${dialogDifficultyName.text.toLowerCase()}"');
}
dialog.hideDialog(DialogButton.APPLY);
}
return dialog;
}
/**
* Builds and opens a dialog where the user can confirm to leave the chart editor if they have unsaved changes.
* @param state The current chart editor state.

View file

@ -25,35 +25,37 @@ class ChartEditorThemeHandler
static final BACKGROUND_COLOR_DARK:FlxColor = 0xFF361E60;
// Color 1 of the grid pattern. Alternates with Color 2.
static final GRID_COLOR_1_LIGHT:FlxColor = 0xFFE7E6E6;
static final GRID_COLOR_1_DARK:FlxColor = 0xFF181919;
public static final GRID_COLOR_1_LIGHT:FlxColor = 0xFFE7E6E6;
public static final GRID_COLOR_1_DARK:FlxColor = 0xFF181919;
// Color 2 of the grid pattern. Alternates with Color 1.
static final GRID_COLOR_2_LIGHT:FlxColor = 0xFFF8F8F8;
static final GRID_COLOR_2_DARK:FlxColor = 0xFF202020;
public static final GRID_COLOR_2_LIGHT:FlxColor = 0xFFF8F8F8;
public static final GRID_COLOR_2_DARK:FlxColor = 0xFF202020;
// Color 3 of the grid pattern. Borders the other colors.
static final GRID_COLOR_3_LIGHT:FlxColor = 0xFFD9D5D5;
static final GRID_COLOR_3_DARK:FlxColor = 0xFF262A2A;
public static final GRID_COLOR_3_LIGHT:FlxColor = 0xFFD9D5D5;
public static final GRID_COLOR_3_DARK:FlxColor = 0xFF262A2A;
// Vertical divider between characters.
static final GRID_STRUMLINE_DIVIDER_COLOR_LIGHT:FlxColor = 0xFF111111;
static final GRID_STRUMLINE_DIVIDER_COLOR_DARK:FlxColor = 0xFFC4C4C4;
public static final GRID_STRUMLINE_DIVIDER_COLOR_LIGHT:FlxColor = 0xFF111111;
public static final GRID_STRUMLINE_DIVIDER_COLOR_DARK:FlxColor = 0xFFC4C4C4;
static final GRID_STRUMLINE_DIVIDER_WIDTH:Float = ChartEditorState.GRID_SELECTION_BORDER_WIDTH;
// Horizontal divider between measures.
static final GRID_MEASURE_DIVIDER_COLOR_LIGHT:FlxColor = 0xFF111111;
static final GRID_MEASURE_DIVIDER_COLOR_DARK:FlxColor = 0xFFC4C4C4;
public static final GRID_MEASURE_DIVIDER_COLOR_LIGHT:FlxColor = 0xFF111111;
public static final GRID_MEASURE_DIVIDER_COLOR_DARK:FlxColor = 0xFFC4C4C4;
static final GRID_MEASURE_DIVIDER_WIDTH:Float = ChartEditorState.GRID_SELECTION_BORDER_WIDTH;
// Horizontal divider between beats.
static final GRID_BEAT_DIVIDER_COLOR_LIGHT:FlxColor = 0xFFC1C1C1;
static final GRID_BEAT_DIVIDER_COLOR_DARK:FlxColor = 0xFF848484;
static final GRID_BEAT_DIVIDER_WIDTH:Float = ChartEditorState.GRID_SELECTION_BORDER_WIDTH;
public static final MEASTURE_TICKS_BACKING_COLOR_LIGHT:FlxColor = 0xFFC1C1C1;
public static final MEASTURE_TICKS_BACKING_COLOR_DARK:FlxColor = 0xFF484848;
// 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;
/**
* The width of the opaque border around the square highlighting selected notes.
*/
public static final SELECTION_SQUARE_BORDER_WIDTH:Int = 1;
// Fill on the square highlighting selected notes.
@ -65,6 +67,11 @@ class ChartEditorThemeHandler
static final PLAYHEAD_BLOCK_BORDER_COLOR:FlxColor = 0xFF9D0011;
static final PLAYHEAD_BLOCK_FILL_COLOR:FlxColor = 0xFFBD0231;
// Lines on the measure ticks.
public static final MEASURE_TICKS_MEASURE_WIDTH:Int = 6;
public static final MEASURE_TICKS_BEAT_WIDTH:Int = 4;
public static final MEASURE_TICKS_STEP_WIDTH:Int = 2;
// Border on the square over the note preview.
static final NOTE_PREVIEW_VIEWPORT_BORDER_COLOR_LIGHT = 0xFFF8A657;
static final NOTE_PREVIEW_VIEWPORT_BORDER_COLOR_DARK = 0xFFF8A657;
@ -73,10 +80,7 @@ class ChartEditorThemeHandler
static final NOTE_PREVIEW_VIEWPORT_FILL_COLOR_LIGHT = 0x80F8A657;
static final NOTE_PREVIEW_VIEWPORT_FILL_COLOR_DARK = 0x80F8A657;
static final TOTAL_COLUMN_COUNT:Int = ChartEditorState.STRUMLINE_SIZE * 2 + 1;
// Used for checking in updateMeasureTicks() so we don't have to redraw it everytime if the current measure is the same as before.
static var previousMeasure:Int = -69;
public static final TOTAL_COLUMN_COUNT:Int = ChartEditorState.STRUMLINE_SIZE * 2 + 1;
/**
* When the theme is changed, this function updates all of the UI elements to match the new theme.
@ -86,10 +90,10 @@ class ChartEditorThemeHandler
{
updateBackground(state);
updateGridBitmap(state);
updateMeasureTicks(state);
updateOffsetTicks(state);
updateSelectionSquare(state);
updateNotePreview(state);
updateMeasureTicks(state);
}
/**
@ -127,6 +131,13 @@ class ChartEditorThemeHandler
default: GRID_COLOR_2_LIGHT;
};
var dividerColor:FlxColor = switch (state.currentTheme)
{
case Light: GRID_STRUMLINE_DIVIDER_COLOR_LIGHT;
case Dark: GRID_STRUMLINE_DIVIDER_COLOR_DARK;
default: GRID_STRUMLINE_DIVIDER_COLOR_LIGHT;
}
// Draw the base grid.
// 2 * (Strumline Size) + 1 grid squares wide, by (4 * quarter notes per measure) grid squares tall.
@ -161,7 +172,7 @@ class ChartEditorThemeHandler
ChartEditorState.GRID_SELECTION_BORDER_WIDTH),
selectionBorderColor);
// Selection border at left.
// Selection border at far left.
state.gridBitmap.fillRect(new Rectangle(-(ChartEditorState.GRID_SELECTION_BORDER_WIDTH / 2), 0, ChartEditorState.GRID_SELECTION_BORDER_WIDTH,
state.gridBitmap.height),
selectionBorderColor);
@ -169,12 +180,14 @@ class ChartEditorThemeHandler
// Selection borders vertically along the middle.
for (i in 1...TOTAL_COLUMN_COUNT)
{
var isStrumlineColumn:Bool = (i % ChartEditorState.STRUMLINE_SIZE == 0) && (i > 0);
state.gridBitmap.fillRect(new Rectangle((ChartEditorState.GRID_SIZE * i) - (ChartEditorState.GRID_SELECTION_BORDER_WIDTH / 2), 0,
ChartEditorState.GRID_SELECTION_BORDER_WIDTH, state.gridBitmap.height),
selectionBorderColor);
isStrumlineColumn ? dividerColor : selectionBorderColor);
}
// Selection border at right.
// Selection border at far right.
state.gridBitmap.fillRect(new Rectangle(state.gridBitmap.width - (ChartEditorState.GRID_SELECTION_BORDER_WIDTH / 2), 0,
ChartEditorState.GRID_SELECTION_BORDER_WIDTH, state.gridBitmap.height),
selectionBorderColor);
@ -186,115 +199,6 @@ class ChartEditorThemeHandler
// Else, gridTiledSprite will be built later.
}
/**
* Draw the measure ticks left of the grid. This also includes the horizontal beat and measure dividers as well as the vertical strumline dividers.
*/
public static function updateMeasureTicks(state:ChartEditorState, force:Bool = false):Void
{
var measureTickWidth:Int = 6;
var beatTickWidth:Int = 4;
var stepTickWidth:Int = 2;
var measuresToCheck:Int = 5;
var currentMeasure:Int = Math.floor(Conductor.instance.getTimeInMeasures(state.scrollPositionInMs));
if (previousMeasure == currentMeasure && !force) return;
if (currentMeasure < 0) currentMeasure = previousMeasure = 0;
// Make the bitmap.
var ticksWidth:Int = Std.int(ChartEditorState.GRID_SIZE);
var linesWidth:Int = Std.int(ChartEditorState.GRID_SIZE * TOTAL_COLUMN_COUNT);
state.measureTickBitmap = new BitmapData(ticksWidth + linesWidth, 6144, true, 0x00FFFFFF);
// Draw the vertical grey sidebar.
state.measureTickBitmap.fillRect(new Rectangle(0, 0, ticksWidth, 6144), GRID_BEAT_DIVIDER_COLOR_DARK);
// Then draw the ticks and dividers themselves.
var totalTicksHeight:Int = 0;
var measureLengthsInPixels:Array<Int> = [];
for (measure in 0...measuresToCheck)
{
var currentTimeInMs:Float = Conductor.instance.getMeasureTimeInMs(currentMeasure + measure)
+ 10; // Manual adjustment as it can get the wrong time change without this.
var currentTimeChange:SongTimeChange = Conductor.instance.getTimeChange(currentTimeInMs);
var stepsPerMeasure:Int = Constants.STEPS_PER_BEAT * currentTimeChange.timeSignatureNum;
var ticksHeight:Int = Std.int(ChartEditorState.GRID_SIZE * stepsPerMeasure);
// Draw horizontal dividers between the measures.
var gridMeasureDividerColor:FlxColor = switch (state.currentTheme)
{
case Light: GRID_MEASURE_DIVIDER_COLOR_LIGHT;
case Dark: GRID_MEASURE_DIVIDER_COLOR_DARK;
default: GRID_MEASURE_DIVIDER_COLOR_LIGHT;
};
// Divider at top
state.measureTickBitmap.fillRect(new Rectangle(ticksWidth, totalTicksHeight, linesWidth, GRID_MEASURE_DIVIDER_WIDTH / 2), gridMeasureDividerColor);
// Divider at bottom
var dividerLineBY:Float = ticksHeight - (GRID_MEASURE_DIVIDER_WIDTH / 2);
state.measureTickBitmap.fillRect(new Rectangle(ticksWidth, totalTicksHeight + dividerLineBY, linesWidth, GRID_MEASURE_DIVIDER_WIDTH / 2),
gridMeasureDividerColor);
// Measure ticks.
state.measureTickBitmap.fillRect(new Rectangle(0, totalTicksHeight, ticksWidth, measureTickWidth / 2), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
var bottomTickY:Float = ticksHeight - (measureTickWidth / 2);
state.measureTickBitmap.fillRect(new Rectangle(0, totalTicksHeight + bottomTickY, ticksWidth, measureTickWidth / 2), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
// Draw horizontal dividers between the beats, this is done inside the following loop.
var gridBeatDividerColor:FlxColor = switch (state.currentTheme)
{
case Light: GRID_BEAT_DIVIDER_COLOR_LIGHT;
case Dark: GRID_BEAT_DIVIDER_COLOR_DARK;
default: GRID_BEAT_DIVIDER_COLOR_LIGHT;
};
// Draw the beat ticks and dividers, and step ticks. No need for two seperate loops thankfully.
for (i in 1...stepsPerMeasure)
{
if ((i % Constants.STEPS_PER_BEAT) == 0) // If we're on a beat, draw a beat tick and divider.
{
var beatTickY:Float = totalTicksHeight + ChartEditorState.GRID_SIZE * i - (beatTickWidth / 2);
var beatTickLength:Float = ticksWidth * 2 / 3;
state.measureTickBitmap.fillRect(new Rectangle(0, beatTickY, beatTickLength, beatTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
// Horizontal divider.
state.measureTickBitmap.fillRect(new Rectangle(ticksWidth, totalTicksHeight + (ChartEditorState.GRID_SIZE * i) - (GRID_BEAT_DIVIDER_WIDTH / 2),
linesWidth, GRID_BEAT_DIVIDER_WIDTH),
gridBeatDividerColor);
}
else // Else, draw a step tick.
{
var stepTickY:Float = totalTicksHeight + ChartEditorState.GRID_SIZE * i - (stepTickWidth / 2);
var stepTickLength:Float = ticksWidth * 1 / 3;
state.measureTickBitmap.fillRect(new Rectangle(0, stepTickY, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
}
}
measureLengthsInPixels.push(totalTicksHeight);
totalTicksHeight += ticksHeight;
}
previousMeasure = currentMeasure;
// Finally, draw vertical dividers between the strumlines.
var gridStrumlineDividerColor:FlxColor = switch (state.currentTheme)
{
case Light: GRID_STRUMLINE_DIVIDER_COLOR_LIGHT;
case Dark: GRID_STRUMLINE_DIVIDER_COLOR_DARK;
default: GRID_STRUMLINE_DIVIDER_COLOR_LIGHT;
};
// Divider at 1 * (Strumline Size)
var dividerLineAX:Float = ticksWidth + ChartEditorState.GRID_SIZE * (ChartEditorState.STRUMLINE_SIZE) - (GRID_STRUMLINE_DIVIDER_WIDTH / 2);
state.measureTickBitmap.fillRect(new Rectangle(dividerLineAX, 0, GRID_STRUMLINE_DIVIDER_WIDTH, state.measureTickBitmap.height), gridStrumlineDividerColor);
// Divider at 2 * (Strumline Size)
var dividerLineBX:Float = ticksWidth + ChartEditorState.GRID_SIZE * (ChartEditorState.STRUMLINE_SIZE * 2) - (GRID_STRUMLINE_DIVIDER_WIDTH / 2);
state.measureTickBitmap.fillRect(new Rectangle(dividerLineBX, 0, GRID_STRUMLINE_DIVIDER_WIDTH, state.measureTickBitmap.height), gridStrumlineDividerColor);
if (state.measureTicks != null)
{
state.measureTicks.measureLengthsInPixels = measureLengthsInPixels;
state.measureTicks.reloadTickBitmap();
}
}
/**
* Horizontal offset ticks.
*/
@ -306,7 +210,7 @@ class ChartEditorThemeHandler
var ticksWidth:Int = Std.int(ChartEditorState.GRID_SIZE * Conductor.instance.stepsPerMeasure); // 10 minor ticks wide.
var ticksHeight:Int = Std.int(ChartEditorState.GRID_SIZE); // 1 grid squares tall.
state.offsetTickBitmap = new BitmapData(ticksWidth, ticksHeight, true);
state.offsetTickBitmap.fillRect(new Rectangle(0, 0, ticksWidth, ticksHeight), GRID_BEAT_DIVIDER_COLOR_DARK);
state.offsetTickBitmap.fillRect(new Rectangle(0, 0, ticksWidth, ticksHeight), 0xFFC4C4C4);
// Draw the major ticks.
var leftTickX:Float = 0;
@ -420,6 +324,11 @@ class ChartEditorThemeHandler
}
}
static function updateMeasureTicks(state:ChartEditorState):Void
{
state.measureTicks?.updateTheme();
}
public static function buildPlayheadBlock():FlxSprite
{
var playheadBlock:FlxSprite = new FlxSprite();

View file

@ -1,5 +1,6 @@
package funkin.ui.debug.charting.toolboxes;
import funkin.ui.debug.charting.commands.SwitchDifficultyCommand;
#if FEATURE_CHART_EDITOR
import funkin.data.song.SongData.SongChartData;
import funkin.data.song.SongData.SongMetadata;
@ -71,6 +72,14 @@ class ChartEditorDifficultyToolbox extends ChartEditorBaseToolbox
chartEditorState.openAddDifficultyDialog(true);
};
difficultyToolboxCloneDifficulty.onClick = function(_:UIEvent) {
chartEditorState.openCloneDifficultyDialog(false, true);
};
difficultyToolboxMoveDifficulty.onClick = function(_:UIEvent) {
chartEditorState.openCloneDifficultyDialog(true, true);
};
difficultyToolboxRemoveDifficulty.onClick = function(_:UIEvent) {
var currentVariation:String = chartEditorState.selectedVariation;
var currentDifficulty:String = chartEditorState.selectedDifficulty;
@ -277,9 +286,10 @@ class ChartEditorDifficultyToolbox extends ChartEditorBaseToolbox
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);
chartEditorState.performCommand(new SwitchDifficultyCommand(chartEditorState.selectedDifficulty, difficulty, chartEditorState.selectedVariation,
variation));
refreshTreeSelection();
}
// case 'song':

View file

@ -5,6 +5,7 @@ import funkin.ui.MenuList.MenuTypedList;
import funkin.ui.MenuList.MenuTypedItem;
import flixel.text.FlxText;
import funkin.data.freeplay.player.PlayerRegistry;
import funkin.ui.freeplay.charselect.PlayableCharacter;
import funkin.ui.options.items.CheckboxPreferenceItem;
import flixel.util.FlxTimer;
import flixel.tweens.FlxTween;
@ -55,7 +56,8 @@ class ResultsDebugSubState extends MusicBeatSubState
});
});
createToggleListItem("Character", PlayerRegistry.instance.listEntryIds(), function(result:String) {
resultsParams.characterId = result;
var playableCharacter:PlayableCharacter = PlayerRegistry.instance.fetchEntry(result);
resultsParams.characterId = playableCharacter.getOwnedCharacterIds()[0];
});
createToggleListItem("Results Mode", ["Debug", "Story", "Freeplay"], function(result:String) {
returnToDebugScreen = result == "Debug"; // We will create the ResultsState as a Substate, that we will just close and return back to here

View file

@ -1,125 +1,95 @@
package funkin.ui.freeplay;
import flixel.FlxObject;
import flixel.group.FlxSpriteGroup;
import flixel.math.FlxPoint;
import flixel.text.FlxText;
import flixel.util.FlxSort;
import flixel.FlxSprite;
import flixel.util.FlxDestroyUtil;
import flixel.util.FlxColor;
import flixel.util.FlxSort;
// its kinda like marqeee html lol!
@:nullSafety
class BGScrollingText extends FlxSpriteGroup
class BGScrollingText extends FlxText
{
var grpTexts:FlxTypedSpriteGroup<FlxSprite>;
var sourceText:FlxText;
var _textPositions:Array<FlxPoint> = [];
var _positionCache:FlxPoint = FlxPoint.get();
public var widthShit:Float = FlxG.width;
public var placementOffset:Float = 20;
public var speed:Float = 1;
public var size(default, set):Int = 48;
public var funnyColor(default, set):FlxColor = 0xFFFFFFFF;
@:deprecated("Use color instead")
public var funnyColor(get, set):FlxColor;
function get_funnyColor():FlxColor
return color;
function set_funnyColor(c:FlxColor):FlxColor
{
return color = c;
}
public function new(x:Float, y:Float, text:String, widthShit:Float = 100, ?bold:Bool = false, ?size:Int = 48)
{
super(x, y);
grpTexts = new FlxTypedSpriteGroup<FlxSprite>();
super(x, y, 0, text, size);
_positionCache = FlxPoint.get(x, y);
font = "5by7";
this.bold = bold ?? false;
this.widthShit = widthShit;
// Only keep one FlxText graphic at a time for batching
sourceText = new FlxText(0, 0, 0, text, size ?? this.size);
sourceText.font = "5by7";
sourceText.bold = bold ?? false;
@:privateAccess
sourceText.regenGraphic();
regenGraphic();
var needed:Int = Math.ceil(widthShit / sourceText.frameWidth) + 1;
var needed:Int = Math.ceil(widthShit / frameWidth) + 1;
for (i in 0...needed)
{
var coolText = new FlxSprite((i * sourceText.frameWidth) + (i * 20), 0);
grpTexts.add(coolText);
}
if (size != null) this.size = size;
add(grpTexts);
}
function reloadGraphics()
{
if (grpTexts != null)
{
@:privateAccess
sourceText.regenGraphic();
grpTexts.forEach(function(txt:FlxSprite) {
txt.loadGraphic(sourceText.graphic);
txt.updateHitbox();
});
_textPositions.push(FlxPoint.get((i * frameWidth) + (i * 20), 0));
}
}
function set_size(value:Int):Int
override public function update(elapsed:Float):Void
{
sourceText.size = value;
reloadGraphics();
this.size = value;
return value;
}
function set_funnyColor(value:FlxColor):FlxColor
{
sourceText.color = value;
reloadGraphics();
this.funnyColor = value;
return value;
}
override public function update(elapsed:Float)
{
for (txt in grpTexts.group)
{
if (txt == null) continue;
txt.x -= 1 * (speed * (elapsed / (1 / 60)));
if (speed > 0)
{
if (txt.x < -txt.frameWidth)
{
txt.x = grpTexts.group.members[grpTexts.length - 1].x + grpTexts.group.members[grpTexts.length - 1].frameWidth + placementOffset;
sortTextShit();
}
}
else
{
if (txt.x > txt.frameWidth * 2)
{
txt.x = grpTexts.group.members[0].x - grpTexts.group.members[0].frameWidth - placementOffset;
sortTextShit();
}
}
}
super.update(elapsed);
for (txtPosition in _textPositions)
{
if (txtPosition == null) continue;
txtPosition.x -= 1 * (speed * (elapsed / (1 / 60)));
if (speed > 0) // Going left
{
if (txtPosition.x < -frameWidth)
{
txtPosition.x = _textPositions[_textPositions.length - 1].x + frameWidth + placementOffset;
sortTextShit();
}
}
else // Going right
{
if (txtPosition.x > frameWidth * 2)
{
txtPosition.x = _textPositions[0].x - frameWidth - placementOffset;
sortTextShit();
}
}
}
}
override public function draw():Void
{
_positionCache.set(x, y);
for (position in _textPositions)
{
setPosition(_positionCache.x + position.x, _positionCache.y + position.y);
super.draw();
}
setPosition(_positionCache.x, _positionCache.y);
}
function sortTextShit():Void
{
grpTexts.sort(function(Order:Int, Obj1:FlxObject, Obj2:FlxObject) {
return FlxSort.byValues(Order, Obj1.x, Obj2.x);
_textPositions.sort(function(Obj1:FlxPoint, Obj2:FlxPoint) {
return FlxSort.byValues(FlxSort.ASCENDING, Obj1.x, Obj2.x);
});
}
override function destroy():Void
{
super.destroy();
sourceText = FlxDestroyUtil.destroy(sourceText);
}
}

View file

@ -2441,7 +2441,7 @@ class FreeplayState extends MusicBeatSubState
if (!capsuleAnim) generateSongList(currentFilter, false, true, true);
}
if (intendedCompletion == Math.POSITIVE_INFINITY || intendedCompletion == Math.NEGATIVE_INFINITY || Math.isNaN(intendedCompletion))
if (!Math.isFinite(intendedCompletion) || Math.isNaN(intendedCompletion))
{
intendedCompletion = 0;
}

View file

@ -131,27 +131,27 @@ class NewCharacterCard extends BackingCard
darkBg = new FlxSprite(0, 0).loadGraphic(bitmap);
add(darkBg);
friendFoe.funnyColor = 0xFF139376;
friendFoe.color = 0xFF139376;
friendFoe.speed = -4;
add(friendFoe);
newUnlock1.funnyColor = 0xFF99BDF2;
newUnlock1.color = 0xFF99BDF2;
newUnlock1.speed = 2;
add(newUnlock1);
waiting.funnyColor = 0xFF40EA84;
waiting.color = 0xFF40EA84;
waiting.speed = -2;
add(waiting);
newUnlock2.funnyColor = 0xFF99BDF2;
newUnlock2.color = 0xFF99BDF2;
newUnlock2.speed = 2;
add(newUnlock2);
friendFoe2.funnyColor = 0xFF139376;
friendFoe2.color = 0xFF139376;
friendFoe2.speed = -4;
add(friendFoe2);
newUnlock3.funnyColor = 0xFF99BDF2;
newUnlock3.color = 0xFF99BDF2;
newUnlock3.speed = 2;
add(newUnlock3);

View file

@ -315,7 +315,7 @@ class StickerTransitionSprite extends openfl.display.Sprite
scaleY = 1;
// Adjusting camera and container cropping to the game resolution
__scrollRect.setTo(0, 0, FlxG.scaleMode.gameSize.x, FlxG.scaleMode.gameSize.y);
__scrollRect.setTo(0, 0, FlxG.camera._scrollRect.scrollRect.width, FlxG.camera._scrollRect.scrollRect.height);
stickersCamera.onResize();
stickersCamera._scrollRect.scrollRect = scrollRect;

View file

@ -28,7 +28,7 @@ class MathUtil
if (x <= 0.0) return 0.0;
if (x >= 1.0) return 1.0;
var result:Float = (x < 0.5) ? (1 - Math.sqrt(1 - 4 * x * x)) / 2 : (Math.sqrt(1 - 4 * (1 - x) * (1 - x)) + 1) / 2;
return (result == Math.NaN) ? 1.0 : result;
return Math.isNaN(result) ? 1.0 : result;
}
public static function easeInOutBack(x:Float, c:Float = 1.70158):Float
@ -36,7 +36,7 @@ class MathUtil
if (x <= 0.0) return 0.0;
if (x >= 1.0) return 1.0;
var result:Float = (x < 0.5) ? (2 * x * x * ((c + 1) * 2 * x - c)) / 2 : (1 - 2 * (1 - x) * (1 - x) * ((c + 1) * 2 * (1 - x) - c)) / 2;
return (result == Math.NaN) ? 1.0 : result;
return Math.isNaN(result) ? 1.0 : result;
}
public static function easeInBack(x:Float, c:Float = 1.70158):Float

View file

@ -86,7 +86,7 @@ class MemoryUtil
final kb:Float = Std.parseFloat(regex.matched(1));
if (kb != Math.NaN)
if (!Math.isNaN(kb))
{
return kb * 1024.0;
}

View file

@ -195,7 +195,7 @@ class FNFCUtil
}
// Combine into a Song object that can be played in PlayState.
var song = Song.buildRaw(songId, songMetadatas.values(), variationList, songChartDatas, false, false);
var song = Song.buildRaw(songId, songMetadatas.values(), Constants.DEFAULT_VARIATION, songChartDatas, false, false);
return song;
}