Author SHA1 Message Date
⍚~NexandFuro 0d9360f0ff Bring dev/scripted-game-modes 2026-01-21 18:17:48 +01:00
⍚~Nex 1cfe6ca468 forgor 2026-01-21 17:16:31 +01:00
⍚~Nex 3bbeafcc3e restarting again
i hate git
2026-01-21 17:09:15 +01:00
32 changed files with 389 additions and 129 deletions
+2 -2
View File
@@ -71,11 +71,11 @@ _**QOL = Quality of Life**_
- Characters are located in `data/weeks/characters`.
- Weeks are located in `data/weeks/weeks/`
- If you need to rearrange the weeks in-game, you can use the `data/weeks/weeks.txt` file.
- Editors for Charts and Characters (Stage coming soon)
- Editors for Charts and Characters
- Undos/Redos supported
- Warning on closing unsaved work
- Clean UI (for ocd freaks)
- Mature Chart editor (Character editor rework soon)
- Mature Chart editor
- Features not found in other editors!
- Every single state & substate can be modified via HScript (`data/states/StateName.hx`)
- **Instances launched via `lime test windows` will automatically use assets from source.**
+3
View File
@@ -1,3 +1,6 @@
# THIS FILE ISN'T NECESSARY,
# BUT YOU CAN USE IT TO SORT AND WHITELIST FREEPLAY SONGS
test
tutorial
+8
View File
@@ -0,0 +1,8 @@
# THIS FILE ISN'T NECESSARY,
# BUT YOU CAN USE IT TO SORT AND WHITELIST GAME MODES
solo
opponent
coop
coop-switched
botplay
@@ -1,5 +1,5 @@
# THIS FILE ISN'T NECESSARY,
# BUT YOU CAN USE IT TO SORT WEEKS
# BUT YOU CAN USE IT TO SORT AND WHITELIST WEEKS
tutorial
week1
+4
View File
@@ -0,0 +1,4 @@
{
"displayName": "Botplay Mode",
"modeID": "codename.botplay"
}
+2
View File
@@ -0,0 +1,2 @@
<!DOCTYPE codename-engine-game-mode>
<mode name="Botplay Mode" id="codename.botplay"/>
+5
View File
@@ -0,0 +1,5 @@
{
"displayName": "Co-Op Mode (Switched)",
"modeID": "codename.coop-opponent",
"scripts": ["opponent", "coop"]
}
+11
View File
@@ -0,0 +1,11 @@
<!DOCTYPE codename-engine-game-mode>
<mode name="Co-Op Mode (Switched)" id="codename.coop-opponent">
<script>coop</script>
<script>opponent</script>
<!--
just a small example of what you could do with fields!!
<field name="switchStrums" type="bool" val="true"/>
<field name="strumSize" type="float" val="0.6"/>
-->
</mode>
+4
View File
@@ -0,0 +1,4 @@
{
"displayName": "Co-Op Mode",
"modeID": "codename.coop"
}
+2
View File
@@ -0,0 +1,2 @@
<!DOCTYPE codename-engine-game-mode>
<mode name="Co-Op Mode" id="codename.coop"/>
+5
View File
@@ -0,0 +1,5 @@
solo
opponent
coop
coop-switched
botplay
+4
View File
@@ -0,0 +1,4 @@
{
"displayName": "Opponent Mode",
"modeID": "codename.opponent"
}
+2
View File
@@ -0,0 +1,2 @@
<!DOCTYPE codename-engine-game-mode>
<mode name="Opponent Mode" id="codename.opponent"/>
+4
View File
@@ -0,0 +1,4 @@
{
"displayName": "Solo Mode",
"modeID": "codename.solo"
}
+2
View File
@@ -0,0 +1,2 @@
<!DOCTYPE codename-engine-game-mode>
<mode name="Solo Mode" id="codename.solo"/>
+1
View File
@@ -18,6 +18,7 @@
<git name="hxdiscord_rpc" url="https://github.com/CodenameCrew/cne-hxdiscord_rpc" skipDeps="true" />
<lib name="funkin-modchart" version="1.2.4" skipDeps="true" />
<lib name="hxvlc" version="1.9.3" skipDeps="true" />
<git name="bin-serializer" url="https://github.com/CodenameCrew/bin-serializer" />
<!-- Documentation and other features -->
<git name="away3d" url="https://github.com/CodenameCrew/away3d" />
+6 -8
View File
@@ -3,19 +3,16 @@
<!-- _________________________ Application Settings _________________________ -->
<app
title="Friday Night Funkin' - Codename Engine" main="funkin.backend.system.Main"
file="CodenameEngine" packageName="com.yoshman29.codenameengine"
package="com.yoshman29.codenameengine"
version="1.0.1" company="Yoshman29" />
file="CodenameEngine" packageName="com.codenamecrew.codenameengine"
package="com.codenamecrew.codenameengine"
version="1.0.1" company="CodenameCrew" />
<!--
CHANGE THE SAVE PATH & NAME FOR YOUR MOD HERE!
CHANGE THE SAVE PATH & NAME FOR YOUR HARDCODED MOD HERE!
-->
<haxedef name="SAVE_PATH" value="CodenameEngine"/>
<haxedef name="SAVE_PATH" value="CodenameCrew/CodenameEngine"/>
<haxedef name="SAVE_NAME" value="save-default"/>
<haxedef name="SAVE_OPTIONS_PATH" value="CodenameEngine"/>
<haxedef name="SAVE_OPTIONS_NAME" value="options"/>
<!--Switch Export with Unique ApplicationID and Icon-->
<set name="APP_ID" value="0x0100f6c013bbc000" />
@@ -131,6 +128,7 @@
<haxelib name="away3d" if="THREE_D_SUPPORT" />
<haxelib name="format" />
<haxelib name="flxanimate" />
<haxelib name="bin-serializer" />
<haxelib name="nape-haxe4" if="NAPE_ENABLED"/>
<haxelib name="hscript-improved" />
+35
View File
@@ -97,6 +97,41 @@ class Chart {
data.setFieldDefault("vocalsSuffix", "");
data.setFieldDefault("needsVoices", true);
data.setFieldDefault("difficulties", []);
var songNameLower = songName.toLowerCase();
var metaPath = Paths.file('songs/${songNameLower}/meta.json');
var metaDiffPath = Paths.file('songs/${songNameLower}/meta-${difficulty.toLowerCase()}.json');
var blacklist:Array<String> = [];
var data:ChartMetaData = null;
var fromMods:Bool = fromMods;
for (path in [metaDiffPath, metaPath]) {
if (Assets.exists(path)) {
fromMods = Paths.assetsTree.existsSpecific(path, "TEXT", MODS);
try {
var temp = CoolUtil.parseJson(path);
// Backwards compatibility (they both can be null so != true) - Nex
var coopExlc = false;
if (Reflect.hasField(temp, "coopAllowed") && Reflect.field(temp, "coopAllowed") != true) {
blacklist = blacklist.concat(["codename.coop", "codename.coop-opponent"]);
Reflect.deleteField(temp, "coopAllowed");
coopExlc = true;
}
if (Reflect.hasField(temp, "opponentModeAllowed") && Reflect.field(temp, "opponentModeAllowed") != true) {
if (!coopExlc) blacklist.push("codename.coop-opponent");
blacklist.push("codename.opponent");
Reflect.deleteField(temp, "opponentModeAllowed");
}
data = temp;
} catch(e) {
Logs.trace('Failed to load song metadata for ${songName} ($path): ${Std.string(e)}', ERROR);
}
if (data != null) break;
}
}
data.setFieldDefault("excludedGameModes", blacklist);
data.setFieldDefault("variants", []);
data.setFieldDefault("metas", []);
+1 -2
View File
@@ -32,8 +32,7 @@ typedef ChartMetaData = {
public var ?icon:String;
public var ?color:FlxColor;
public var ?coopAllowed:Bool;
public var ?opponentModeAllowed:Bool;
public var ?excludedGameModes:Array<String>;
public var ?metas:Map<String, ChartMetaData>;
public var ?instSuffix:String;
+1 -1
View File
@@ -402,4 +402,4 @@ class Script extends FlxBasic implements IFlxDestroyable {
* Called when the script is loaded.
*/
public function onLoad() {}
}
}
@@ -1,5 +1,7 @@
package funkin.backend.scripting.events.menu.freeplay;
import funkin.menus.FreeplayState.FreeplayGameMode;
final class FreeplaySongSelectEvent extends CancellableEvent {
/**
* Song name that is about to be played
@@ -14,11 +16,26 @@ final class FreeplaySongSelectEvent extends CancellableEvent {
*/
public var variant:String;
/**
* Whenever opponent mode is enabled or not.
* Used game mode's data.
*/
public var opponentMode:Bool;
/**
* Whenever coop mode is enabled.
*/
public var coopMode:Bool;
public var gameMode:FreeplayGameMode;
// Backwards compat
@:noCompletion public var opponentMode(get, set):Bool;
@:noCompletion private function get_opponentMode() return gameMode.modeID == "codename.opponent" || gameMode.modeID == "codename.coop-opponent";
@:noCompletion private function set_opponentMode(val:Bool) {
gameMode = coopMode ?
(val ? new FreeplayGameMode("Co-Op Mode (Switched)", "codename.coop-opponent", ["coop-switched", "opponent", "coop"]) : new FreeplayGameMode("Opponent Mode", "codename.opponent", ["opponent"])) :
(val ? new FreeplayGameMode("Opponent Mode", "codename.opponent", ["opponent"]) : null);
return val;
}
@:noCompletion public var coopMode(get, set):Bool;
@:noCompletion private function get_coopMode() return gameMode.modeID == "codename.coop" || gameMode.modeID == "codename.coop-opponent";
@:noCompletion private function set_coopMode(val:Bool) {
gameMode = opponentMode ?
(val ? new FreeplayGameMode("Co-Op Mode (Switched)", "codename.coop-opponent", ["coop-switched", "opponent", "coop"]) : new FreeplayGameMode("Co-Op Mode", "codename.coop", ["coop"])) :
(val ? new FreeplayGameMode("Co-Op Mode", "codename.coop", ["coop"]) : null);
return val;
}
}
+8 -3
View File
@@ -13,7 +13,7 @@ import lime.utils.AssetType;
/**
* A class that reads the `flags.ini` file, allowing to read settable Flags (customs too).
*/
@:build(funkin.backend.system.macros.FlagMacro.build())
@:build(funkin.backend.system.macros.FlagMacro.build("onReset"))
class Flags {
public static var overridenFlags:Map<String, Bool> = [];
@@ -157,7 +157,7 @@ class Flags {
public static var DEFAULT_HUD_ZOOM_LERP:Float = 0.05;
public static var USE_LEGACY_ZOOM_FACTOR:Null<Bool> = null;
// Font configuration
public static var DEFAULT_FONT:String = "vcr.ttf";
public static var DEFAULT_FONT_SIZE:Int = 16;
@@ -312,6 +312,11 @@ class Flags {
if (SUSTAINS_AS_ONE_NOTE == null) SUSTAINS_AS_ONE_NOTE = MOD_API_VERSION >= 2;
}
private static function onReset() {
var currentModFolder = ModsFolder.currentModFolder;
if (currentModFolder != null) SAVE_NAME = currentModFolder;
}
public static function loadFromDatas(datas:Array<String>):Map<String, String> {
var flags:Map<String, String> = [];
for (data in datas) {
@@ -384,4 +389,4 @@ class Flags {
}
loadPost();
}
}
}
@@ -19,7 +19,7 @@ class FlagMacro {
return false;
}
public static function build():Array<Field> {
public static function build(resetCallback:String):Array<Field> {
var fields = Context.getBuildFields();
var clRef = Context.getLocalClass();
@@ -248,7 +248,10 @@ class FlagMacro {
access: [APublic, AStatic],
kind: FFun({
args: [],
expr: macro $b{resetExprs},
expr: macro {
$b{resetExprs};
$i{resetCallback}();
},
ret: macro: Void
}),
pos: Context.currentPos(),
+1 -1
View File
@@ -1495,4 +1495,4 @@ enum abstract CoolSfx(Int) from Int {
var CHECKED = 3;
var UNCHECKED = 4;
var WARNING = 5;
}
}
+1 -1
View File
@@ -541,4 +541,4 @@ typedef DEvents =
var ?joinGame:String->Void;
var ?spectateGame:String->Void;
var ?joinRequest:DUser->Void;
}
}
@@ -1,5 +1,6 @@
package funkin.editors.charter;
import funkin.game.HealthIcon;
import flixel.math.FlxPoint;
import funkin.backend.chart.ChartData.ChartMetaData;
import funkin.editors.extra.PropertyButton;
@@ -21,8 +22,7 @@ class CharterMetaDataScreen extends UISubstateWindow {
public var displayNameTextBox:UITextBox;
public var iconTextBox:UITextBox;
public var iconSprite:HealthIcon;
public var opponentModeCheckbox:UICheckbox;
public var coopAllowedCheckbox:UICheckbox;
public var excludedGameModesList:UIAutoCompleteButtonList;
public var needsVoicesCheckbox:UICheckbox;
public var colorWheel:UIColorwheel;
public var difficultiesTextBox:UITextBox;
@@ -78,25 +78,29 @@ class CharterMetaDataScreen extends UISubstateWindow {
add(displayNameTextBox);
addLabelOn(displayNameTextBox, translate("displayName"));
add(iconSprite = new HealthIcon(metadata.icon));
iconSprite.scale.set(0.5, 0.5);
iconSprite.updateHitbox();
iconTextBox = new UITextBox(displayNameTextBox.x + 320 + 26, displayNameTextBox.y, metadata.icon, 150);
iconTextBox.onChange = (newIcon:String) -> {updateIcon(newIcon);}
iconTextBox.onChange = (newIcon:String) -> iconSprite.setIcon(newIcon);
add(iconTextBox);
addLabelOn(iconTextBox, translate("icon"));
updateIcon(metadata.icon);
opponentModeCheckbox = new UICheckbox(displayNameTextBox.x, iconTextBox.y + 10 + 32 + 26, translate("opponentMode"), metadata.opponentModeAllowed);
add(opponentModeCheckbox);
addLabelOn(opponentModeCheckbox, translate("modesAllowed"));
coopAllowedCheckbox = new UICheckbox(opponentModeCheckbox.x + 150 + 26, opponentModeCheckbox.y, translate("coopAllowed"), metadata.coopAllowed);
add(coopAllowedCheckbox);
add(excludedGameModesList = new UIAutoCompleteButtonList(displayNameTextBox.x, iconTextBox.y + 62, Std.int(displayNameTextBox.bWidth), 100, "", [for (mode in funkin.menus.FreeplayState.FreeplayGameMode.get()) mode.modeID]));
excludedGameModesList.frames = Paths.getFrames('editors/ui/inputbox');
excludedGameModesList.cameraSpacing = 0;
addLabelOn(excludedGameModesList, "Excluded Game Modes (IDs)");
for (mode in metadata.excludedGameModes)
excludedGameModesList.add(new UIAutoCompleteButtonList.UIAutoCompleteButton(0, 0, excludedGameModesList, excludedGameModesList.suggestItems, mode));
colorWheel = new UIColorwheel(iconTextBox.x, coopAllowedCheckbox.y, metadata.color);
add(colorWheel);
addLabelOn(colorWheel, translate("color"));
difficultiesTextBox = new UITextBox(opponentModeCheckbox.x, opponentModeCheckbox.y + 6 + 32 + 26, metadata.difficulties.join(", "));
difficultiesTextBox = new UITextBox(excludedGameModesList.x, excludedGameModesList.y + excludedGameModesList.bHeight + 32, metadata.difficulties.join(", "));
add(difficultiesTextBox);
addLabelOn(difficultiesTextBox, translate("difficulties"));
@@ -111,9 +115,6 @@ class CharterMetaDataScreen extends UISubstateWindow {
add(customPropertiesButtonList);
addLabelOn(customPropertiesButtonList, translate("customValues"));
for (checkbox in [opponentModeCheckbox, coopAllowedCheckbox])
{checkbox.y += 6; checkbox.x += 4;}
saveButton = new UIButton(windowSpr.x + windowSpr.bWidth - 20, windowSpr.y + windowSpr.bHeight - 20, TU.translate("editor.saveClose"), function() {
saveMeta();
close();
@@ -161,8 +162,7 @@ class CharterMetaDataScreen extends UISubstateWindow {
meta.displayName = displayNameTextBox.label.text;
meta.icon = iconTextBox.label.text;
meta.color = colorWheel.curColor;
meta.opponentModeAllowed = opponentModeCheckbox.checked;
meta.coopAllowed = coopAllowedCheckbox.checked;
meta.excludedGameModes = [for (button in excludedGameModesList.buttons.members) button.textBox.label.text.trim()],
meta.difficulties = [for (diff in difficultiesTextBox.label.text.split(",")) diff.trim()];
meta.customValues = customVals;
@@ -287,4 +287,4 @@ class CharacterButton extends UIButton {
super.update(elapsed);
}
}
}
@@ -1,5 +1,6 @@
package funkin.editors.charter;
import funkin.game.HealthIcon;
import flixel.group.FlxGroup;
import flixel.text.FlxText.FlxTextFormat;
import flixel.text.FlxText.FlxTextFormatMarkerPair;
@@ -37,8 +38,7 @@ class SongCreationScreen extends UISubstateWindow {
public var displayNameTextBox:UITextBox;
public var iconTextBox:UITextBox;
public var iconSprite:HealthIcon;
public var opponentModeCheckbox:UICheckbox;
public var coopAllowedCheckbox:UICheckbox;
public var excludedGameModesList:UIAutoCompleteButtonList;
public var colorWheel:UIColorwheel;
public var difficultiesTextBox:UITextBox;
@@ -166,24 +166,19 @@ class SongCreationScreen extends UISubstateWindow {
updateIcon("face");
opponentModeCheckbox = new UICheckbox(displayNameTextBox.x, iconTextBox.y + 10 + 32 + 26, translateMeta("opponentMode"), true);
menuDataGroup.add(opponentModeCheckbox);
addLabelOn(opponentModeCheckbox, translateMeta("modesAllowed"));
menuDataGroup.add(excludedGameModesList = new UIAutoCompleteButtonList(displayNameTextBox.x, iconTextBox.y + 62, Std.int(displayNameTextBox.bWidth), 100, "", [for (mode in funkin.menus.FreeplayState.FreeplayGameMode.get()) mode.modeID]));
excludedGameModesList.frames = Paths.getFrames('editors/ui/inputbox');
excludedGameModesList.cameraSpacing = 0;
addLabelOn(excludedGameModesList, "Excluded Game Modes (IDs)");
coopAllowedCheckbox = new UICheckbox(opponentModeCheckbox.x + 150 + 26, opponentModeCheckbox.y, translateMeta("coopAllowed"), true);
menuDataGroup.add(coopAllowedCheckbox);
colorWheel = new UIColorwheel(iconTextBox.x, coopAllowedCheckbox.y, 0xFFFFFF);
colorWheel = new UIColorwheel(iconTextBox.x, excludedGameModesList.y, 0xFFFFFF);
menuDataGroup.add(colorWheel);
addLabelOn(colorWheel, translateMeta("color"));
difficultiesTextBox = new UITextBox(opponentModeCheckbox.x, opponentModeCheckbox.y + 6 + 32 + 26, "");
difficultiesTextBox = new UITextBox(excludedGameModesList.x, excludedGameModesList.y + excludedGameModesList.bHeight + 32, "");
menuDataGroup.add(difficultiesTextBox);
addLabelOn(difficultiesTextBox, translateMeta("difficulties"));
for (checkbox in [opponentModeCheckbox, coopAllowedCheckbox])
{checkbox.y += 6; checkbox.x += 4;}
var menuTitle:UIText;
selectFormatGroup.add(menuTitle = new UIText(windowSpr.x + 20, windowSpr.y + 30 + 16, 0, translate("importSource"), 28));
@@ -433,8 +428,7 @@ class SongCreationScreen extends UISubstateWindow {
displayName: displayNameTextBox.label.text,
icon: iconTextBox.label.text,
color: colorWheel.curColor,
opponentModeAllowed: opponentModeCheckbox.checked,
coopAllowed: coopAllowedCheckbox.checked,
excludedGameModes: [for (button in excludedGameModesList.buttons.members) button.textBox.label.text.trim()],
difficulties: [for (diff in difficultiesTextBox.label.text.split(",")) if (diff.length > 0) diff.trim()]
});
@@ -0,0 +1,50 @@
package funkin.editors.ui;
typedef UIAutoCompleteButtonList = UITypedAutoCompleteButtonList<UIAutoCompleteButton>;
class UITypedAutoCompleteButtonList<T:UIAutoCompleteButton> extends UIButtonList<T> {
public var suggestItems(default, set):Array<String> = [];
private function set_suggestItems(value:Array<String>) {
buttons.forEachAlive((button) -> button.textBox.suggestItems = value);
return suggestItems = value;
}
public function new(x:Float, y:Float, width:Int, height:Int, windowName:String, ?suggestItems:Array<String>, ?buttonSize:FlxPoint, ?buttonOffset:FlxPoint, buttonSpacing:Float = 0) {
super(x, y, width, height, windowName, buttonSize == null ? FlxPoint.get(width, width * 0.15) : buttonSize, buttonOffset, buttonSpacing);
if (suggestItems != null && suggestItems.length > 0) this.suggestItems = suggestItems;
addButton.callback = () -> add(cast new UIAutoCompleteButton(0, 0, cast this, this.suggestItems));
}
}
class UIAutoCompleteButton extends UIButton {
public var textBox:UIAutoCompleteTextBox;
public var deleteButton:UIButton;
public var deleteIcon:FlxSprite;
public function new (x:Float, y:Float, parent:UIAutoCompleteButtonList, ?suggestItems:Array<String>, text:String = "", ?w:Int, ?h:Int, multiline:Bool = false) {
super(x, y, "", null, w == null ? Std.int(parent.buttonSize.x) : w, h == null ? Std.int(parent.buttonSize.y) : h);
members.push(textBox = new UIAutoCompleteTextBox(16, bHeight/2 - (32/2), text, Std.int(bWidth - 32 - 36), 32, multiline));
if (suggestItems != null && suggestItems.length > 0) textBox.suggestItems = suggestItems;
textBox.antialiasing = false;
members.push(deleteButton = new UIButton(textBox.x + textBox.bWidth + 10, textBox.y, "", () -> { parent.remove(this); destroy(); }, 32));
deleteButton.color = 0xFFFF0000;
deleteButton.autoAlpha = false;
members.push(deleteIcon = new FlxSprite(deleteButton.x + (15/2), deleteButton.y + 8).loadGraphic(Paths.image('editors/delete-button')));
deleteIcon.antialiasing = false;
}
override function update(elapsed) {
deleteButton.y = y + bHeight / 2 - deleteButton.bHeight / 2;
textBox.y = y + bHeight/2 - 16;
deleteIcon.x = deleteButton.x + (15/2); deleteIcon.y = deleteButton.y + 8;
textBox.selectable = deleteButton.selectable = selectable;
deleteButton.shouldPress = shouldPress;
super.update(elapsed);
}
}
+1 -1
View File
@@ -588,4 +588,4 @@ class Character extends FunkinSprite implements IBeatReceiver implements IOffset
list.push(CoolUtil.getFilename(path));
return list;
}
}
}
+161 -66
View File
@@ -9,6 +9,7 @@ import funkin.backend.scripting.events.menu.freeplay.*;
import funkin.backend.system.Conductor;
import funkin.game.HealthIcon;
import funkin.savedata.FunkinSave;
import haxe.io.Path;
using StringTools;
@@ -34,6 +35,11 @@ class FreeplayState extends MusicBeatState
*/
public var curDiffMetaKeys:Array<String> = [];
/*
* Array containing all labels for game modes.
*/
public var gameModeLabels:Array<FreeplayGameMode> = [];
/**
* Currently selected song
*/
@@ -43,9 +49,9 @@ class FreeplayState extends MusicBeatState
*/
public var curDifficulty:Int = 1;
/**
* Currently selected coop/opponent mode
* Currently selected game mode
*/
public var curCoopMode:Int = 0;
public var curGameMode:Int = 0;
/**
* Text containing the score info (PERSONAL BEST: 0)
@@ -58,9 +64,9 @@ class FreeplayState extends MusicBeatState
public var diffText:FlxText;
/**
* Text containing the current coop/opponent mode ([KEYBINDS] Co-Op mode)
* Text containing the current game mode, for example: ([KEYBINDS] Co-Op mode)
*/
public var coopText:FlxText;
public var gameModeText:FlxText;
/**
* Currently lerped score. Is updated to go towards `intendedScore`.
@@ -130,6 +136,7 @@ class FreeplayState extends MusicBeatState
}
updateCurSong();
gameModeLabels = FreeplayGameMode.get();
DiscordUtil.call("onMenuLoaded", ["Freeplay"]);
@@ -177,14 +184,15 @@ class FreeplayState extends MusicBeatState
diffText.font = scoreText.font;
add(diffText);
coopText = new FlxText(diffText.x, diffText.y + diffText.height + 2, 0, "", 24);
coopText.font = scoreText.font;
add(coopText);
gameModeText = new FlxText(diffText.x, diffText.y + diffText.height + 2, 0, "", 24);
gameModeText.font = scoreText.font;
add(gameModeText);
add(scoreText);
changeSelection(0, true);
changeCoopMode(0, true);
changeDiff(0, true);
changeGameMode(0, true);
interpColor = new FlxInterpolateColor(bg.color);
}
@@ -239,17 +247,17 @@ class FreeplayState extends MusicBeatState
if (canSelect) {
changeSelection((controls.UP_P ? -1 : 0) + (controls.DOWN_P ? 1 : 0) - FlxG.mouse.wheel);
changeDiff((controls.LEFT_P ? -1 : 0) + (controls.RIGHT_P ? 1 : 0));
changeCoopMode((controls.CHANGE_MODE ? 1 : 0)); // TODO: make this configurable
changeGameMode((controls.CHANGE_MODE ? 1 : 0));
// putting it before so that its actually smooth
updateOptionsAlpha();
}
scoreText.text = TEXT_FREEPLAY_SCORE.format([Math.round(lerpScore)]);
scoreBG.scale.set(MathUtil.maxSmart(diffText.width, scoreText.width, coopText.width) + 8, (coopText.visible ? coopText.y + coopText.height : 66));
scoreBG.scale.set(MathUtil.maxSmart(diffText.width, scoreText.width, gameModeText.width) + 8, (gameModeText.visible ? gameModeText.y + gameModeText.height : 66));
scoreBG.updateHitbox();
scoreBG.x = FlxG.width - scoreBG.width;
scoreText.x = coopText.x = scoreBG.x + 4;
scoreText.x = gameModeText.x = scoreBG.x + 4;
diffText.x = Std.int(scoreBG.x + ((scoreBG.width - diffText.width) / 2));
interpColor.fpsLerpTo(curSong.color, 0.0625);
@@ -310,31 +318,13 @@ class FreeplayState extends MusicBeatState
select();
}
var __opponentMode:Bool = false;
var __coopMode:Bool = false;
function updateCoopModes() {
__opponentMode = false;
__coopMode = false;
if (curSong.coopAllowed && curSong.opponentModeAllowed) {
__opponentMode = curCoopMode % 2 == 1;
__coopMode = curCoopMode >= 2;
} else if (curSong.coopAllowed) {
__coopMode = curCoopMode == 1;
} else if (curSong.opponentModeAllowed) {
__opponentMode = curCoopMode == 1;
}
}
/**
* Selects the current song.
*/
public function select() {
updateCoopModes();
if (curDifficulties.length == 0) return;
var event = event("onSelect", EventManager.get(FreeplaySongSelectEvent).recycle(curSong.name, curDifficulties[curDifficulty], curSong.variant, __opponentMode, __coopMode));
var event = event("onSelect", EventManager.get(FreeplaySongSelectEvent).recycle(curSong.name, curDifficulties[curDifficulty], curSong.variant, gameModeLabels[curGameMode]));
if (event.cancelled) return;
@@ -393,7 +383,7 @@ class FreeplayState extends MusicBeatState
intendedScore = 0;
return;
}
updateCoopModes();
var changes:Array<HighscoreChange> = [];
if (__coopMode) changes.push(CCoopMode);
if (__opponentMode) changes.push(COpponentMode);
@@ -401,45 +391,37 @@ class FreeplayState extends MusicBeatState
intendedScore = saveData.score;
}
/**
* Array containing all labels for Co-Op / Opponent modes.
*/
public var coopLabels:Array<String> = [
TU.translate("freeplay.solo"),
TU.translate("freeplay.opponentMode"),
TU.translate("freeplay.coopMode"),
TU.translate("freeplay.coopModeSwitched")
];
/**
* Change the current coop mode context.
* @param change How much to change
* @param force Force the change, even if `change` is equal to 0.
*/
public function changeCoopMode(change:Int = 0, force:Bool = false) {
public function changeGameMode(change:Int = 0, force:Bool = false) {
if (change == 0 && !force) return;
if (!curSong.coopAllowed && !curSong.opponentModeAllowed) return;
var bothEnabled = curSong.coopAllowed && curSong.opponentModeAllowed;
var event = event("onChangeCoopMode", EventManager.get(MenuChangeEvent).recycle(curCoopMode, FlxMath.wrap(curCoopMode + change, 0, bothEnabled ? 3 : 1), change));
if (event.cancelled) return;
curCoopMode = event.value;
updateScore();
var coopBinds = [CoolUtil.keyToString(Options.P1_CHANGE_MODE[0]), CoolUtil.keyToString(Options.P2_CHANGE_MODE[0])].filter(x -> x != "---");
if (coopBinds.length == 2 && coopBinds[1] == coopBinds[0]) coopBinds.pop();
else if (coopBinds.length == 0) coopBinds.push("---");
var key = '[${coopBinds.join(" / ")}] ';
if (bothEnabled) {
coopText.text = key + coopLabels[curCoopMode];
} else {
coopText.text = key + coopLabels[curCoopMode * (curSong.coopAllowed ? 2 : 1)];
var i = 0;
var allowed = getAllowedGameModesID();
var wrapped = FlxMath.wrap(curGameMode + change, 0, gameModeLabels.length - 1);
while (!allowed.contains(gameModeLabels[wrapped].modeID)) { // Skipping the blacklisted ones - Nex
wrapped = FlxMath.wrap(wrapped + change, 0, gameModeLabels.length - 1);
if (i++ > allowed.length) {
curGameMode = 0; // Idk honestly if I should modify the other event's variables - Nex
break;
}
}
var e = EventManager.get(MenuChangeEvent).recycle(curGameMode, wrapped, change);
event("onChangeCoopMode", e); // Backwards compat - Nex
if (event("onChangeGameMode", e).cancelled) return;
// Getting from scratch the allowed game modes just in case they changed when the event got called - Nex
if (gameModeText.visible = getAllowedGameModesID().length > 0) gameModeText.text = "[TAB] " + gameModeLabels[curGameMode = e.value].modeName;
updateScore();
}
public inline function getAllowedGameModesID() {
var excluded = songs[curSelected].excludedGameModes;
return [for (mode in gameModeLabels) if (!excluded.contains(mode.modeID)) mode.modeID];
}
/**
@@ -465,13 +447,12 @@ class FreeplayState extends MusicBeatState
}
changeDiff(0, true);
changeGameMode(0, true);
#if PRELOAD_ALL
autoplayElapsed = 0;
songInstPlaying = false;
#end
coopText.visible = curSong.coopAllowed || curSong.opponentModeAllowed;
}
function updateOptionsAlpha() {
@@ -486,7 +467,7 @@ class FreeplayState extends MusicBeatState
iconArray[curSelected].alpha = selectedAlpha;
for (i=>item in grpSongs.members)
for (i => item in grpSongs.members)
{
item.targetY = i - curSelected;
@@ -517,6 +498,118 @@ class FreeplayState extends MusicBeatState
else if ((curSong = song.metas.get(curDiffMetaKeys[curDifficulty])) == null)
curSong = song;
}
// Backwards compat
@:noCompletion public function changeCoopMode(change:Int = 0, force:Bool = false) return changeGameMode(change, force);
@:noCompletion public var coopText(get, set):FlxText;
@:noCompletion private function get_coopText() return gameModeText;
@:noCompletion private function set_coopText(val:FlxText) return gameModeText = val;
@:noCompletion function updateCoopModes() {}
@:noCompletion var __opponentMode(get, default):Bool = false; // God this is so cursed - Nex
@:noCompletion private function get___opponentMode() return getAllowedGameModesID().contains("codename.opponent");
@:noCompletion var __coopMode(get, default):Bool = false;
@:noCompletion private function get___coopMode() return getAllowedGameModesID().contains("codename.coop");
@:noCompletion var curCoopMode(get, set):Int;
@:noCompletion private function get_curCoopMode() return curGameMode;
@:noCompletion private function set_curCoopMode(val:Int) return curGameMode = val;
@:noCompletion var coopLabels(get, set):Array<String>;
@:noCompletion private function get_coopLabels() return [for(mode in gameModeLabels) mode.modeName];
@:noCompletion private function set_coopLabels(names:Array<String>) {
gameModeLabels = [for(name in names) {
if (name.startsWith("[TAB] ")) name = name.substr("[TAB] ".length);
new FreeplayGameMode(name, switch(name) {
case "Solo": "codename.solo";
case "Opponent Mode": "codename.opponent";
case "Co-Op Mode": "codename.coop";
case "Co-Op Mode (Switched)": "codename.coop-opponent";
default: name.toLowerCase().replace(" ", "-");
}, switch(name) {
case "Solo": ["solo"];
case "Opponent Mode": ["opponent"];
case "Co-Op Mode": ["coop"];
case "Co-Op Mode (Switched)": ["coop-switched", "opponent", "coop"];
default: null;
});
}];
return names;
}
}
/**
* Class used for the Freeplay menu game modes.
*
* **NOTE**: `fields` is useable not just for boolean values!!
*/
class FreeplayGameMode {
public var modeName:String;
public var modeID:String;
public var scripts(default, set):Array<String>;
public var fields(default, set):Dynamic;
@:noCompletion function set_scripts(val:Array<String>) return scripts = (val == null ? [] : val);
@:noCompletion function set_fields(val:Dynamic) return fields = (val == null ? {} : val);
public function new(modeName:String, modeID:String, ?scripts:Array<String>, ?fields:Dynamic) {
this.modeName = modeName;
this.modeID = modeID;
this.scripts = scripts;
this.fields = fields;
}
public function toModifiers():Array<Dynamic> {
var fields = this.fields;
var modifiers:Array<Dynamic> = [];
for (field in {var _ = Reflect.fields(fields); _.sort(Reflect.compare); _;}) {
var val:Dynamic = Reflect.field(fields, field);
if (val != null && val != false) { // allow true, and floats/ints
modifiers.push(field);
modifiers.push(val);
}
}
return modifiers;
}
public static inline function generateDefault() return new FreeplayGameMode("Solo Mode", "solo");
public static inline function getGameModesFromSource(source:AssetSource = BOTH, useTxt:Bool = true):Array<FreeplayGameMode> {
var path = 'data/gamemodes';
var txt = Paths.txt('gamemodes/list');
var list = useTxt && Paths.assetsTree.existsSpecific(txt, "TEXT", source) ? CoolUtil.coolTextFile(txt) : Paths.getFolderContent(path, false, source);
return [for (file in list) {
if (useTxt) file += ".json";
else if (Path.extension(file) != "json") continue;
var meta:FreeplayGameMode = null;
try {
var data = CoolUtil.parseJson(Paths.file(path + "/" + file)); var id = data.modeID;
meta = new FreeplayGameMode(CoolUtil.getDefault(data.displayName, id), id, [Path.withoutExtension(file)].concat(CoolUtil.getDefault(data.scripts, [])), data.fields);
} catch(e) Logs.trace('Failed to load game mode metadata for $file ($path): ${Std.string(e)}', ERROR);
if (meta != null) meta;
}];
}
public static function get(useTxt:Bool = true):Array<FreeplayGameMode> {
var list:Array<FreeplayGameMode>;
switch(Flags.GAME_MODES_LIST_MOD_MODE) {
case 'prepend':
list = getGameModesFromSource(MODS, useTxt).concat(getGameModesFromSource(SOURCE, useTxt));
case 'append':
list = getGameModesFromSource(SOURCE, useTxt).concat(getGameModesFromSource(MODS, useTxt));
case 'override':
list = getGameModesFromSource(BOTH, useTxt);
default /*case 'oneOFtwo'*/:
if ((list = getGameModesFromSource(MODS, useTxt)).length == 0)
list = getGameModesFromSource(SOURCE, useTxt);
}
return list;
}
}
class FreeplaySonglist {
@@ -553,7 +646,9 @@ class FreeplaySonglist {
case 'append':
songList.getSongsFromSource(SOURCE, useTxt);
songList.getSongsFromSource(MODS, useTxt);
default /*case 'override'*/:
case 'override':
songList.getSongsFromSource(BOTH, useTxt);
default /*case 'oneOFtwo'*/:
if (songList.getSongsFromSource(MODS, useTxt))
songList.getSongsFromSource(SOURCE, useTxt);
}
+11 -4
View File
@@ -342,10 +342,17 @@ class StoryWeeklist {
public function new() {}
public function getWeeksFromSource(source:funkin.backend.assets.AssetSource, useTxt:Bool = true, loadCharactersData:Bool = true) {
var path:String = Paths.txt('weeks/weeks');
var weeksFound:Array<String> = useTxt && Paths.assetsTree.existsSpecific(path, "TEXT", source) ? CoolUtil.coolTextFile(path) :
[for (c in Paths.getFolderContent('data/weeks/weeks/', false, source)) if (Path.extension(c).toLowerCase() == "xml") Path.withoutExtension(c)];
var weeksFound:Array<String> = null;
if (useTxt) {
var oldPath:String = Paths.txt('weeks/weeks');
var newPath:String = Paths.txt('config/weeks');
if (Paths.assetsTree.existsSpecific(newPath, "TEXT", source)) weeksFound = CoolUtil.coolTextFile(newPath);
else if (Paths.assetsTree.existsSpecific(oldPath, "TEXT", source)) {
Logs.warn("data/weeks/weeks.txt is deprecated and will be removed in the future. Please move the file to data/config/", DARKYELLOW, "StoryWeeklist");
weeksFound = CoolUtil.coolTextFile(oldPath);
}
}
if (weeksFound == null) weeksFound = [for (c in Paths.getFolderContent('data/weeks/weeks/', false, source)) if (Path.extension(c).toLowerCase() == "xml") Path.withoutExtension(c)];
if (weeksFound.length > 0) {
for (w in weeksFound) {
var week = Week.loadWeek(w, loadCharactersData);