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`. - Characters are located in `data/weeks/characters`.
- Weeks are located in `data/weeks/weeks/` - 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. - 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 - Undos/Redos supported
- Warning on closing unsaved work - Warning on closing unsaved work
- Clean UI (for ocd freaks) - Clean UI (for ocd freaks)
- Mature Chart editor (Character editor rework soon) - Mature Chart editor
- Features not found in other editors! - Features not found in other editors!
- Every single state & substate can be modified via HScript (`data/states/StateName.hx`) - 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.** - **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 test
tutorial 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, # THIS FILE ISN'T NECESSARY,
# BUT YOU CAN USE IT TO SORT WEEKS # BUT YOU CAN USE IT TO SORT AND WHITELIST WEEKS
tutorial tutorial
week1 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" /> <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="funkin-modchart" version="1.2.4" skipDeps="true" />
<lib name="hxvlc" version="1.9.3" 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 --> <!-- Documentation and other features -->
<git name="away3d" url="https://github.com/CodenameCrew/away3d" /> <git name="away3d" url="https://github.com/CodenameCrew/away3d" />
+6 -8
View File
@@ -3,19 +3,16 @@
<!-- _________________________ Application Settings _________________________ --> <!-- _________________________ Application Settings _________________________ -->
<app <app
title="Friday Night Funkin' - Codename Engine" main="funkin.backend.system.Main" title="Friday Night Funkin' - Codename Engine" main="funkin.backend.system.Main"
file="CodenameEngine" packageName="com.yoshman29.codenameengine" file="CodenameEngine" packageName="com.codenamecrew.codenameengine"
package="com.yoshman29.codenameengine" package="com.codenamecrew.codenameengine"
version="1.0.1" company="Yoshman29" /> 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_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--> <!--Switch Export with Unique ApplicationID and Icon-->
<set name="APP_ID" value="0x0100f6c013bbc000" /> <set name="APP_ID" value="0x0100f6c013bbc000" />
@@ -131,6 +128,7 @@
<haxelib name="away3d" if="THREE_D_SUPPORT" /> <haxelib name="away3d" if="THREE_D_SUPPORT" />
<haxelib name="format" /> <haxelib name="format" />
<haxelib name="flxanimate" /> <haxelib name="flxanimate" />
<haxelib name="bin-serializer" />
<haxelib name="nape-haxe4" if="NAPE_ENABLED"/> <haxelib name="nape-haxe4" if="NAPE_ENABLED"/>
<haxelib name="hscript-improved" /> <haxelib name="hscript-improved" />
+35
View File
@@ -97,6 +97,41 @@ class Chart {
data.setFieldDefault("vocalsSuffix", ""); data.setFieldDefault("vocalsSuffix", "");
data.setFieldDefault("needsVoices", true); data.setFieldDefault("needsVoices", true);
data.setFieldDefault("difficulties", []); 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("variants", []);
data.setFieldDefault("metas", []); data.setFieldDefault("metas", []);
+1 -2
View File
@@ -32,8 +32,7 @@ typedef ChartMetaData = {
public var ?icon:String; public var ?icon:String;
public var ?color:FlxColor; public var ?color:FlxColor;
public var ?coopAllowed:Bool; public var ?excludedGameModes:Array<String>;
public var ?opponentModeAllowed:Bool;
public var ?metas:Map<String, ChartMetaData>; public var ?metas:Map<String, ChartMetaData>;
public var ?instSuffix:String; public var ?instSuffix:String;
@@ -1,5 +1,7 @@
package funkin.backend.scripting.events.menu.freeplay; package funkin.backend.scripting.events.menu.freeplay;
import funkin.menus.FreeplayState.FreeplayGameMode;
final class FreeplaySongSelectEvent extends CancellableEvent { final class FreeplaySongSelectEvent extends CancellableEvent {
/** /**
* Song name that is about to be played * Song name that is about to be played
@@ -14,11 +16,26 @@ final class FreeplaySongSelectEvent extends CancellableEvent {
*/ */
public var variant:String; public var variant:String;
/** /**
* Whenever opponent mode is enabled or not. * Used game mode's data.
*/ */
public var opponentMode:Bool; public var gameMode:FreeplayGameMode;
/**
* Whenever coop mode is enabled.
*/ // Backwards compat
public var coopMode:Bool; @: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;
}
} }
+6 -1
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). * 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 { class Flags {
public static var overridenFlags:Map<String, Bool> = []; public static var overridenFlags:Map<String, Bool> = [];
@@ -312,6 +312,11 @@ class Flags {
if (SUSTAINS_AS_ONE_NOTE == null) SUSTAINS_AS_ONE_NOTE = MOD_API_VERSION >= 2; 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> { public static function loadFromDatas(datas:Array<String>):Map<String, String> {
var flags:Map<String, String> = []; var flags:Map<String, String> = [];
for (data in datas) { for (data in datas) {
@@ -19,7 +19,7 @@ class FlagMacro {
return false; return false;
} }
public static function build():Array<Field> { public static function build(resetCallback:String):Array<Field> {
var fields = Context.getBuildFields(); var fields = Context.getBuildFields();
var clRef = Context.getLocalClass(); var clRef = Context.getLocalClass();
@@ -248,7 +248,10 @@ class FlagMacro {
access: [APublic, AStatic], access: [APublic, AStatic],
kind: FFun({ kind: FFun({
args: [], args: [],
expr: macro $b{resetExprs}, expr: macro {
$b{resetExprs};
$i{resetCallback}();
},
ret: macro: Void ret: macro: Void
}), }),
pos: Context.currentPos(), pos: Context.currentPos(),
@@ -1,5 +1,6 @@
package funkin.editors.charter; package funkin.editors.charter;
import funkin.game.HealthIcon;
import flixel.math.FlxPoint; import flixel.math.FlxPoint;
import funkin.backend.chart.ChartData.ChartMetaData; import funkin.backend.chart.ChartData.ChartMetaData;
import funkin.editors.extra.PropertyButton; import funkin.editors.extra.PropertyButton;
@@ -21,8 +22,7 @@ class CharterMetaDataScreen extends UISubstateWindow {
public var displayNameTextBox:UITextBox; public var displayNameTextBox:UITextBox;
public var iconTextBox:UITextBox; public var iconTextBox:UITextBox;
public var iconSprite:HealthIcon; public var iconSprite:HealthIcon;
public var opponentModeCheckbox:UICheckbox; public var excludedGameModesList:UIAutoCompleteButtonList;
public var coopAllowedCheckbox:UICheckbox;
public var needsVoicesCheckbox:UICheckbox; public var needsVoicesCheckbox:UICheckbox;
public var colorWheel:UIColorwheel; public var colorWheel:UIColorwheel;
public var difficultiesTextBox:UITextBox; public var difficultiesTextBox:UITextBox;
@@ -78,25 +78,29 @@ class CharterMetaDataScreen extends UISubstateWindow {
add(displayNameTextBox); add(displayNameTextBox);
addLabelOn(displayNameTextBox, translate("displayName")); 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 = 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); add(iconTextBox);
addLabelOn(iconTextBox, translate("icon")); addLabelOn(iconTextBox, translate("icon"));
updateIcon(metadata.icon); updateIcon(metadata.icon);
opponentModeCheckbox = new UICheckbox(displayNameTextBox.x, iconTextBox.y + 10 + 32 + 26, translate("opponentMode"), metadata.opponentModeAllowed); add(excludedGameModesList = new UIAutoCompleteButtonList(displayNameTextBox.x, iconTextBox.y + 62, Std.int(displayNameTextBox.bWidth), 100, "", [for (mode in funkin.menus.FreeplayState.FreeplayGameMode.get()) mode.modeID]));
add(opponentModeCheckbox); excludedGameModesList.frames = Paths.getFrames('editors/ui/inputbox');
addLabelOn(opponentModeCheckbox, translate("modesAllowed")); excludedGameModesList.cameraSpacing = 0;
addLabelOn(excludedGameModesList, "Excluded Game Modes (IDs)");
coopAllowedCheckbox = new UICheckbox(opponentModeCheckbox.x + 150 + 26, opponentModeCheckbox.y, translate("coopAllowed"), metadata.coopAllowed); for (mode in metadata.excludedGameModes)
add(coopAllowedCheckbox); excludedGameModesList.add(new UIAutoCompleteButtonList.UIAutoCompleteButton(0, 0, excludedGameModesList, excludedGameModesList.suggestItems, mode));
colorWheel = new UIColorwheel(iconTextBox.x, coopAllowedCheckbox.y, metadata.color); colorWheel = new UIColorwheel(iconTextBox.x, coopAllowedCheckbox.y, metadata.color);
add(colorWheel); add(colorWheel);
addLabelOn(colorWheel, translate("color")); 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); add(difficultiesTextBox);
addLabelOn(difficultiesTextBox, translate("difficulties")); addLabelOn(difficultiesTextBox, translate("difficulties"));
@@ -111,9 +115,6 @@ class CharterMetaDataScreen extends UISubstateWindow {
add(customPropertiesButtonList); add(customPropertiesButtonList);
addLabelOn(customPropertiesButtonList, translate("customValues")); 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() { saveButton = new UIButton(windowSpr.x + windowSpr.bWidth - 20, windowSpr.y + windowSpr.bHeight - 20, TU.translate("editor.saveClose"), function() {
saveMeta(); saveMeta();
close(); close();
@@ -161,8 +162,7 @@ class CharterMetaDataScreen extends UISubstateWindow {
meta.displayName = displayNameTextBox.label.text; meta.displayName = displayNameTextBox.label.text;
meta.icon = iconTextBox.label.text; meta.icon = iconTextBox.label.text;
meta.color = colorWheel.curColor; meta.color = colorWheel.curColor;
meta.opponentModeAllowed = opponentModeCheckbox.checked; meta.excludedGameModes = [for (button in excludedGameModesList.buttons.members) button.textBox.label.text.trim()],
meta.coopAllowed = coopAllowedCheckbox.checked;
meta.difficulties = [for (diff in difficultiesTextBox.label.text.split(",")) diff.trim()]; meta.difficulties = [for (diff in difficultiesTextBox.label.text.split(",")) diff.trim()];
meta.customValues = customVals; meta.customValues = customVals;
@@ -1,5 +1,6 @@
package funkin.editors.charter; package funkin.editors.charter;
import funkin.game.HealthIcon;
import flixel.group.FlxGroup; import flixel.group.FlxGroup;
import flixel.text.FlxText.FlxTextFormat; import flixel.text.FlxText.FlxTextFormat;
import flixel.text.FlxText.FlxTextFormatMarkerPair; import flixel.text.FlxText.FlxTextFormatMarkerPair;
@@ -37,8 +38,7 @@ class SongCreationScreen extends UISubstateWindow {
public var displayNameTextBox:UITextBox; public var displayNameTextBox:UITextBox;
public var iconTextBox:UITextBox; public var iconTextBox:UITextBox;
public var iconSprite:HealthIcon; public var iconSprite:HealthIcon;
public var opponentModeCheckbox:UICheckbox; public var excludedGameModesList:UIAutoCompleteButtonList;
public var coopAllowedCheckbox:UICheckbox;
public var colorWheel:UIColorwheel; public var colorWheel:UIColorwheel;
public var difficultiesTextBox:UITextBox; public var difficultiesTextBox:UITextBox;
@@ -166,24 +166,19 @@ class SongCreationScreen extends UISubstateWindow {
updateIcon("face"); updateIcon("face");
opponentModeCheckbox = new UICheckbox(displayNameTextBox.x, iconTextBox.y + 10 + 32 + 26, translateMeta("opponentMode"), true); 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]));
menuDataGroup.add(opponentModeCheckbox); excludedGameModesList.frames = Paths.getFrames('editors/ui/inputbox');
addLabelOn(opponentModeCheckbox, translateMeta("modesAllowed")); excludedGameModesList.cameraSpacing = 0;
addLabelOn(excludedGameModesList, "Excluded Game Modes (IDs)");
coopAllowedCheckbox = new UICheckbox(opponentModeCheckbox.x + 150 + 26, opponentModeCheckbox.y, translateMeta("coopAllowed"), true); colorWheel = new UIColorwheel(iconTextBox.x, excludedGameModesList.y, 0xFFFFFF);
menuDataGroup.add(coopAllowedCheckbox);
colorWheel = new UIColorwheel(iconTextBox.x, coopAllowedCheckbox.y, 0xFFFFFF);
menuDataGroup.add(colorWheel); menuDataGroup.add(colorWheel);
addLabelOn(colorWheel, translateMeta("color")); 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); menuDataGroup.add(difficultiesTextBox);
addLabelOn(difficultiesTextBox, translateMeta("difficulties")); addLabelOn(difficultiesTextBox, translateMeta("difficulties"));
for (checkbox in [opponentModeCheckbox, coopAllowedCheckbox])
{checkbox.y += 6; checkbox.x += 4;}
var menuTitle:UIText; var menuTitle:UIText;
selectFormatGroup.add(menuTitle = new UIText(windowSpr.x + 20, windowSpr.y + 30 + 16, 0, translate("importSource"), 28)); 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, displayName: displayNameTextBox.label.text,
icon: iconTextBox.label.text, icon: iconTextBox.label.text,
color: colorWheel.curColor, color: colorWheel.curColor,
opponentModeAllowed: opponentModeCheckbox.checked, excludedGameModes: [for (button in excludedGameModesList.buttons.members) button.textBox.label.text.trim()],
coopAllowed: coopAllowedCheckbox.checked,
difficulties: [for (diff in difficultiesTextBox.label.text.split(",")) if (diff.length > 0) diff.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);
}
}
+161 -66
View File
@@ -9,6 +9,7 @@ import funkin.backend.scripting.events.menu.freeplay.*;
import funkin.backend.system.Conductor; import funkin.backend.system.Conductor;
import funkin.game.HealthIcon; import funkin.game.HealthIcon;
import funkin.savedata.FunkinSave; import funkin.savedata.FunkinSave;
import haxe.io.Path;
using StringTools; using StringTools;
@@ -34,6 +35,11 @@ class FreeplayState extends MusicBeatState
*/ */
public var curDiffMetaKeys:Array<String> = []; public var curDiffMetaKeys:Array<String> = [];
/*
* Array containing all labels for game modes.
*/
public var gameModeLabels:Array<FreeplayGameMode> = [];
/** /**
* Currently selected song * Currently selected song
*/ */
@@ -43,9 +49,9 @@ class FreeplayState extends MusicBeatState
*/ */
public var curDifficulty:Int = 1; 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) * Text containing the score info (PERSONAL BEST: 0)
@@ -58,9 +64,9 @@ class FreeplayState extends MusicBeatState
public var diffText:FlxText; 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`. * Currently lerped score. Is updated to go towards `intendedScore`.
@@ -130,6 +136,7 @@ class FreeplayState extends MusicBeatState
} }
updateCurSong(); updateCurSong();
gameModeLabels = FreeplayGameMode.get();
DiscordUtil.call("onMenuLoaded", ["Freeplay"]); DiscordUtil.call("onMenuLoaded", ["Freeplay"]);
@@ -177,14 +184,15 @@ class FreeplayState extends MusicBeatState
diffText.font = scoreText.font; diffText.font = scoreText.font;
add(diffText); add(diffText);
coopText = new FlxText(diffText.x, diffText.y + diffText.height + 2, 0, "", 24); gameModeText = new FlxText(diffText.x, diffText.y + diffText.height + 2, 0, "", 24);
coopText.font = scoreText.font; gameModeText.font = scoreText.font;
add(coopText); add(gameModeText);
add(scoreText); add(scoreText);
changeSelection(0, true); changeSelection(0, true);
changeCoopMode(0, true); changeDiff(0, true);
changeGameMode(0, true);
interpColor = new FlxInterpolateColor(bg.color); interpColor = new FlxInterpolateColor(bg.color);
} }
@@ -239,17 +247,17 @@ class FreeplayState extends MusicBeatState
if (canSelect) { if (canSelect) {
changeSelection((controls.UP_P ? -1 : 0) + (controls.DOWN_P ? 1 : 0) - FlxG.mouse.wheel); 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)); 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 // putting it before so that its actually smooth
updateOptionsAlpha(); updateOptionsAlpha();
} }
scoreText.text = TEXT_FREEPLAY_SCORE.format([Math.round(lerpScore)]); 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.updateHitbox();
scoreBG.x = FlxG.width - scoreBG.width; 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)); diffText.x = Std.int(scoreBG.x + ((scoreBG.width - diffText.width) / 2));
interpColor.fpsLerpTo(curSong.color, 0.0625); interpColor.fpsLerpTo(curSong.color, 0.0625);
@@ -310,31 +318,13 @@ class FreeplayState extends MusicBeatState
select(); 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. * Selects the current song.
*/ */
public function select() { public function select() {
updateCoopModes();
if (curDifficulties.length == 0) return; 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; if (event.cancelled) return;
@@ -393,7 +383,7 @@ class FreeplayState extends MusicBeatState
intendedScore = 0; intendedScore = 0;
return; return;
} }
updateCoopModes();
var changes:Array<HighscoreChange> = []; var changes:Array<HighscoreChange> = [];
if (__coopMode) changes.push(CCoopMode); if (__coopMode) changes.push(CCoopMode);
if (__opponentMode) changes.push(COpponentMode); if (__opponentMode) changes.push(COpponentMode);
@@ -401,47 +391,39 @@ class FreeplayState extends MusicBeatState
intendedScore = saveData.score; 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. * Change the current coop mode context.
* @param change How much to change * @param change How much to change
* @param force Force the change, even if `change` is equal to 0. * @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 (change == 0 && !force) return;
if (!curSong.coopAllowed && !curSong.opponentModeAllowed) return;
var bothEnabled = curSong.coopAllowed && curSong.opponentModeAllowed; var i = 0;
var event = event("onChangeCoopMode", EventManager.get(MenuChangeEvent).recycle(curCoopMode, FlxMath.wrap(curCoopMode + change, 0, bothEnabled ? 3 : 1), change)); var allowed = getAllowedGameModesID();
var wrapped = FlxMath.wrap(curGameMode + change, 0, gameModeLabels.length - 1);
if (event.cancelled) return; while (!allowed.contains(gameModeLabels[wrapped].modeID)) { // Skipping the blacklisted ones - Nex
wrapped = FlxMath.wrap(wrapped + change, 0, gameModeLabels.length - 1);
curCoopMode = event.value; if (i++ > allowed.length) {
curGameMode = 0; // Idk honestly if I should modify the other event's variables - Nex
updateScore(); break;
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 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];
}
/** /**
* Change the current selection. * Change the current selection.
* @param change How much to change * @param change How much to change
@@ -465,13 +447,12 @@ class FreeplayState extends MusicBeatState
} }
changeDiff(0, true); changeDiff(0, true);
changeGameMode(0, true);
#if PRELOAD_ALL #if PRELOAD_ALL
autoplayElapsed = 0; autoplayElapsed = 0;
songInstPlaying = false; songInstPlaying = false;
#end #end
coopText.visible = curSong.coopAllowed || curSong.opponentModeAllowed;
} }
function updateOptionsAlpha() { function updateOptionsAlpha() {
@@ -486,7 +467,7 @@ class FreeplayState extends MusicBeatState
iconArray[curSelected].alpha = selectedAlpha; iconArray[curSelected].alpha = selectedAlpha;
for (i=>item in grpSongs.members) for (i => item in grpSongs.members)
{ {
item.targetY = i - curSelected; item.targetY = i - curSelected;
@@ -517,6 +498,118 @@ class FreeplayState extends MusicBeatState
else if ((curSong = song.metas.get(curDiffMetaKeys[curDifficulty])) == null) else if ((curSong = song.metas.get(curDiffMetaKeys[curDifficulty])) == null)
curSong = song; 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 { class FreeplaySonglist {
@@ -553,7 +646,9 @@ class FreeplaySonglist {
case 'append': case 'append':
songList.getSongsFromSource(SOURCE, useTxt); songList.getSongsFromSource(SOURCE, useTxt);
songList.getSongsFromSource(MODS, useTxt); songList.getSongsFromSource(MODS, useTxt);
default /*case 'override'*/: case 'override':
songList.getSongsFromSource(BOTH, useTxt);
default /*case 'oneOFtwo'*/:
if (songList.getSongsFromSource(MODS, useTxt)) if (songList.getSongsFromSource(MODS, useTxt))
songList.getSongsFromSource(SOURCE, useTxt); songList.getSongsFromSource(SOURCE, useTxt);
} }
+11 -4
View File
@@ -342,10 +342,17 @@ class StoryWeeklist {
public function new() {} public function new() {}
public function getWeeksFromSource(source:funkin.backend.assets.AssetSource, useTxt:Bool = true, loadCharactersData:Bool = true) { 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> = null;
var weeksFound:Array<String> = useTxt && Paths.assetsTree.existsSpecific(path, "TEXT", source) ? CoolUtil.coolTextFile(path) : if (useTxt) {
[for (c in Paths.getFolderContent('data/weeks/weeks/', false, source)) if (Path.extension(c).toLowerCase() == "xml") Path.withoutExtension(c)]; 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) { if (weeksFound.length > 0) {
for (w in weeksFound) { for (w in weeksFound) {
var week = Week.loadWeek(w, loadCharactersData); var week = Week.loadWeek(w, loadCharactersData);