Merge branch 'Time-Signature-Changes' into new-conductor

# Conflicts:
#	.gitignore
#	source/funkin/backend/FunkinSprite.hx
#	source/funkin/backend/chart/ChartData.hx
#	source/funkin/backend/chart/EventsData.hx
#	source/funkin/backend/system/Conductor.hx
#	source/funkin/backend/utils/CoolUtil.hx
#	source/funkin/editors/charter/Charter.hx
#	source/funkin/editors/charter/CharterEvent.hx
#	source/funkin/editors/charter/CharterEventScreen.hx
#	source/funkin/editors/charter/CharterMetaDataScreen.hx
#	source/funkin/editors/charter/SongCreationScreen.hx
#	source/funkin/game/PlayState.hx
This commit is contained in:
Ralty
2025-07-16 19:27:05 +07:00
32 changed files with 1288 additions and 311 deletions
+35 -21
View File
@@ -613,6 +613,8 @@ class Charter extends UIState {
gridBackdrops.bottomLimitY = __endStep * 40;
eventsBackdrop.bottomSeparator.y = gridBackdrops.bottomLimitY-2;
CharterGridSeperatorBase.lastConductorSprY = Math.NEGATIVE_INFINITY;
updateWaveforms();
}
@@ -846,6 +848,7 @@ class Charter extends UIState {
s.cursor = BUTTON;
}
}
checkSelectionForBPMUpdates();
if (!(verticalChange == 0 && horizontalChange == 0)) {
notesGroup.sortNotes(); eventsGroup.sortEvents();
undos.addToUndo(CSelectionDrag(undoDrags));
@@ -1037,11 +1040,7 @@ class Charter extends UIState {
notesGroup.sortNotes();
notesGroup.autoSort = true;
for (s in selection)
if (s is CharterEvent) {
Charter.instance.updateBPMEvents();
break;
}
checkSelectionForBPMUpdates();
if (addToUndo)
undos.addToUndo(CCreateSelection(selection));
@@ -1065,11 +1064,7 @@ class Charter extends UIState {
notesGroup.sortNotes();
notesGroup.autoSort = true;
for (s in selection)
if (s is CharterEvent) {
Charter.instance.updateBPMEvents();
break;
}
checkSelectionForBPMUpdates();
if (addToUndo)
undos.addToUndo(CDeleteSelection(selection));
@@ -1256,12 +1251,13 @@ class Charter extends UIState {
for (strumLine in strumLines.members) strumLine.vocals.pause();
}
songPosInfo.text = '${CoolUtil.timeToStr(Conductor.songPosition)} / ${CoolUtil.timeToStr(songLength)}'
+ '\nStep: ${curStep}'
+ '\nBeat: ${curBeat}'
+ '\nMeasure: ${curMeasure}'
+ '\nBPM: ${Conductor.bpm}'
+ '\nTime Signature: ${Conductor.beatsPerMeasure}/${Conductor.stepsPerBeat}';
var curChange = Conductor.curChange;
songPosInfo.text = '${CoolUtil.timeToStr(songPos)} / ${CoolUtil.timeToStr(songLength)}'
+ '\nStep: $curStep'
+ '\nBeat: $curBeat'
+ '\nMeasure: $curMeasure'
+ '\nBPM: ${(curChange != null && curChange.continuous && curChange.endSongTime > songPos) ? FlxMath.roundDecimal(Conductor.bpm, 3) : Conductor.bpm}'
+ '\nTime Signature: ${Conductor.beatsPerMeasure}/${Conductor.denominator}';
if (charterCamera.zoom != (charterCamera.zoom = lerp(charterCamera.zoom, __camZoom, __firstFrame ? 1 : 0.125)))
updateDisplaySprites();
@@ -1441,6 +1437,8 @@ class Charter extends UIState {
selection = sObjects;
_edit_copy(_); // to fix stupid bugs
checkSelectionForBPMUpdates();
undos.addToUndo(CCreateSelection(sObjects.copy()));
}
@@ -1486,6 +1484,7 @@ class Charter extends UIState {
if (s.selectable.draggable) s.selectable.handleDrag(s.change * -1);
selection = [for (s in selectionDrags) s.selectable];
Charter.instance.updateBPMEvents();
case CEditSustains(changes):
for(n in changes)
n.note.updatePos(n.note.step, n.note.id, n.before, n.note.type);
@@ -1531,6 +1530,7 @@ class Charter extends UIState {
for (s in selectionDrags)
if (s.selectable.draggable) s.selectable.handleDrag(s.change);
//this.selection = selection;
Charter.instance.updateBPMEvents();
case CEditSustains(changes):
for(n in changes)
n.note.updatePos(n.note.step, n.note.id, n.after, n.note.type);
@@ -1643,11 +1643,9 @@ class Charter extends UIState {
}
function _view_showeventSecSeparator(t) {
t.icon = (Options.charterShowSections = !Options.charterShowSections) ? 1 : 0;
eventsBackdrop.eventSecSeparator.visible = gridBackdrops.sectionsVisible = Options.charterShowSections;
}
function _view_showeventBeatSeparator(t) {
t.icon = (Options.charterShowBeats = !Options.charterShowBeats) ? 1 : 0;
eventsBackdrop.eventBeatSeparator.visible = gridBackdrops.beatsVisible = Options.charterShowBeats;
}
function _view_switchWaveformDetail(t) {
t.icon = (Options.charterLowDetailWaveforms = !Options.charterLowDetailWaveforms) ? 1 : 0;
@@ -1847,14 +1845,30 @@ class Charter extends UIState {
}
public function updateBPMEvents() {
eventsGroup.sortEvents();
Conductor.mapCharterBPMChanges(PlayState.SONG);
buildEvents();
Conductor.mapBPMChanges(PlayState.SONG);
Conductor.changeBPM(PlayState.SONG.meta.bpm, cast PlayState.SONG.meta.beatsPerMeasure.getDefault(4), cast PlayState.SONG.meta.stepsPerBeat.getDefault(4));
for(e in eventsGroup.members) {
for(event in e.events) {
if (event.name == "BPM Change" || event.name == "Time Signature Change" || event.name == "Continuous BPM Change") {
e.refreshEventIcons();
break;
}
}
}
refreshBPMSensitive();
}
public inline function checkSelectionForBPMUpdates() {
for (s in selection)
if (s is CharterEvent) {
Charter.instance.updateBPMEvents();
break;
}
}
public inline function hitsoundsEnabled(id:Int)
return strumLines.members[id] != null && strumLines.members[id].hitsounds;
@@ -12,8 +12,6 @@ class CharterBackdropGroup extends FlxTypedGroup<CharterBackdrop> {
public var conductorSprY:Float = 0;
public var bottomLimitY:Float = 0;
public var sectionsVisible:Bool = true;
public var beatsVisible:Bool = true;
// Just here so you can update display sprites all dat and above
public var strumlinesAmount:Int = 0;
@@ -61,8 +59,6 @@ class CharterBackdropGroup extends FlxTypedGroup<CharterBackdrop> {
grid.conductorFollowerSpr.y = conductorSprY;
grid.bottomSeparator.y = (grid.bottomLimit.y = bottomLimitY)-2;
grid.sectionSeparator.visible = sectionsVisible;
grid.beatSeparator.visible = beatsVisible;
grid.waveformSprite.shader = strumLine.waveformShader;
@@ -119,8 +115,7 @@ class CharterBackdrop extends FlxTypedGroup<Dynamic> {
public var waveformSprite:FlxSprite;
public var conductorFollowerSpr:FlxSprite;
public var beatSeparator:FlxBackdrop;
public var sectionSeparator:FlxBackdrop;
public var beatSeparator:CharterGridSeperator;
public var notesGroup:FlxTypedGroup<CharterNote> = new FlxTypedGroup<CharterNote>();
public var strumLine:CharterStrumline;
@@ -136,23 +131,14 @@ class CharterBackdrop extends FlxTypedGroup<Dynamic> {
waveformSprite.updateHitbox();
add(waveformSprite);
sectionSeparator = new FlxBackdrop(null, Y, 0, 0);
sectionSeparator.y = -2;
sectionSeparator.visible = Options.charterShowSections;
beatSeparator = new FlxBackdrop(null, Y, 0, 0);
beatSeparator.y = -1;
beatSeparator.visible = Options.charterShowBeats;
for(sep in [sectionSeparator, beatSeparator]) {
sep.makeSolid(1, 1, -1);
sep.alpha = 0.5;
sep.scrollFactor.set(1, 1);
sep.scale.set((4 * 40), sep == sectionSeparator ? 4 : 2);
sep.updateHitbox();
}
beatSeparator = new CharterGridSeperator();
beatSeparator.makeSolid(1, 1, -1);
beatSeparator.alpha = 0.5;
beatSeparator.scrollFactor.set(1, 1);
beatSeparator.scale.set((4 * 40), 2);
beatSeparator.updateHitbox();
add(beatSeparator);
add(sectionSeparator);
add(notesGroup);
bottomSeparator = new FlxSprite(0,-2);
@@ -200,15 +186,12 @@ class CharterBackdrop extends FlxTypedGroup<Dynamic> {
alpha = strumLine.strumLine.visible ? 0.9 : 0.4;
} else alpha = 0.9;
for (spr in [gridBackDrop, sectionSeparator, beatSeparator, topLimit, bottomLimit,
for (spr in [gridBackDrop, beatSeparator, topLimit, bottomLimit,
topSeparator, bottomSeparator, conductorFollowerSpr, waveformSprite]) {
spr.x = x; if (spr != waveformSprite) spr.alpha = alpha;
spr.cameras = this.cameras;
}
sectionSeparator.spacing.y = (10 * Conductor.beatsPerMeasure * Conductor.stepsPerBeat) - 1;
beatSeparator.spacing.y = (20 * Conductor.stepsPerBeat) - 1;
topLimit.scale.set(4 * 40, Math.ceil(FlxG.height / cameras[0].zoom));
topLimit.updateHitbox();
topLimit.y = -topLimit.height;
@@ -237,6 +220,159 @@ class CharterBackdrop extends FlxTypedGroup<Dynamic> {
}
}
class CharterGridSeperatorBase extends FlxSprite {
private static var minStep:Float = 0;
private static var maxStep:Float = 0;
private static var minBeat:Float = 0;
private static var maxBeat:Float = 0;
private static var minMeasure:Float = 0;
private static var maxMeasure:Float = 0;
private static var lastMinBeat:Float = -1;
private static var lastMaxBeat:Float = -1;
private static var lastMinMeasure:Float = -1;
private static var lastMaxMeasure:Float = -1;
public static var lastConductorSprY:Float = Math.NEGATIVE_INFINITY;
private static var beatStepTimes:Array<Float> = [];
private static var measureStepTimes:Array<Float> = [];
private static var timeSignatureChangeGaps:Array<Float> = [];
private function recalculateBeats() {
var conductorSprY = Charter.instance.gridBackdrops.conductorSprY;
if (conductorSprY == lastConductorSprY) return;
var zoomOffset = ((FlxG.height * (1/cameras[0].zoom)) * 0.5);
minStep = (conductorSprY - zoomOffset)/40;
maxStep = (conductorSprY + zoomOffset)/40;
var minTime:Float = Conductor.getStepsInTime(minStep);
var maxTime:Float = Conductor.getStepsInTime(maxStep);
var minBpmChange = Conductor.bpmChangeMap[Conductor.getTimeInChangeIndex(minTime)];
var maxBpmChange = Conductor.bpmChangeMap[Conductor.getTimeInChangeIndex(maxTime)];
minBeat = Conductor.getTimeInBeats(minTime);
maxBeat = Conductor.getTimeInBeats(maxTime);
minMeasure = minBpmChange.measureTime + (minBeat - minBpmChange.beatTime) / minBpmChange.beatsPerMeasure;
maxMeasure = maxBpmChange.measureTime + (maxBeat - maxBpmChange.beatTime) / maxBpmChange.beatsPerMeasure;
//cap out the beats/measures at the end of the song
var endTime = Conductor.getStepsInTime(Charter.instance.__endStep);
var endBeat = Conductor.getTimeInBeats(endTime);
var endBpmChange = Conductor.bpmChangeMap[Conductor.getTimeInChangeIndex(endTime)];
var endMeasure = endBpmChange.measureTime + (endBeat - endBpmChange.beatTime) / endBpmChange.beatsPerMeasure;
if (maxBeat > endBeat) maxBeat = endBeat;
if (maxMeasure > endMeasure) maxMeasure = endMeasure;
if (minMeasure < 0) minMeasure = 0;
if (minBeat < 0) minBeat = 0;
//only calculate if needed
if ((minBeat != lastMinBeat) || (maxBeat != lastMaxBeat) || (minMeasure != lastMinMeasure) || (maxMeasure != lastMaxMeasure) || lastConductorSprY == Math.NEGATIVE_INFINITY) {
calculateTimeSignatureGaps();
calculateStepTimes();
lastMinBeat = minBeat;
lastMaxBeat = maxBeat;
lastMinMeasure = minMeasure;
lastMaxMeasure = maxMeasure;
}
lastConductorSprY = conductorSprY;
}
private inline function calculateTimeSignatureGaps() {
//for time signatures that start mid step
timeSignatureChangeGaps.splice(0, timeSignatureChangeGaps.length);
for (i => change in Conductor.bpmChangeMap) {
if (change.stepTime >= minStep && change.stepTime <= maxStep) {
//get step while ignoring the current change
var index:Int = CoolUtil.boundInt(i-1, 0, Conductor.bpmChangeMap.length - 1);
var step:Float = Conductor.getTimeWithBPMInSteps(change.songTime, index, Conductor.getTimeWithIndexInBPM(change.songTime, index));
if (Math.ceil(step) - step > 0 && (step - Math.floor(step)) > FlxMath.EPSILON) { //mid step change
timeSignatureChangeGaps.push(step);
}
}
}
}
private inline function calculateStepTimes() {
beatStepTimes.splice(0, beatStepTimes.length);
for (i in Math.floor(minBeat)...Math.ceil(maxBeat)) {
beatStepTimes.push(Conductor.getTimeInSteps(Conductor.getBeatsInTime(i)));
}
measureStepTimes.splice(0, measureStepTimes.length);
for (i in Math.floor(minMeasure)...Math.ceil(maxMeasure)) {
measureStepTimes.push(Conductor.getTimeInSteps(Conductor.getMeasuresInTime(i)));
}
}
override public function draw() {
//should only need to recalculate once per frame and will be shared across each instance
recalculateBeats();
drawTimeSignatureChangeGaps();
if (Options.charterShowBeats) drawBeats();
if (Options.charterShowSections) drawMeasures();
}
private function drawBeats(offset:Float = 0.0) {
for (i in beatStepTimes) {
y = (i*40)+offset;
super.draw();
}
}
private function drawMeasures(offset:Float = 0.0) {
for (i in measureStepTimes) {
y = (i*40)+offset;
super.draw();
}
}
private function drawTimeSignatureChangeGaps() {
if (timeSignatureChangeGaps.length == 0) return;
var prevColor = color;
var prevBlend = blend;
color = 0xFF888888;
blend = MULTIPLY;
for (step in timeSignatureChangeGaps) {
y = step*40;
var diff = Math.ceil(step) - step;
scale.y = diff*40;
updateHitbox();
super.draw();
}
color = prevColor;
blend = prevBlend;
}
}
class CharterGridSeperator extends CharterGridSeperatorBase {
override private function drawBeats(offset:Float = 0.0) {
scale.y = 2;
updateHitbox();
super.drawBeats(-2);
}
override private function drawMeasures(offset:Float = 0.0) {
scale.y = 4;
updateHitbox();
super.drawMeasures(-3);
}
}
class CharterBackdropDummy extends UISprite {
var parent:CharterBackdropGroup;
public function new(parent:CharterBackdropGroup) {
@@ -258,8 +394,7 @@ class CharterBackdropDummy extends UISprite {
}
class EventBackdrop extends FlxBackdrop {
public var eventBeatSeparator:FlxBackdrop;
public var eventSecSeparator:FlxBackdrop;
public var eventBeatSeparator:CharterEventGridSeperator;
public var topSeparator:FlxSprite;
public var bottomSeparator:FlxSprite;
@@ -269,25 +404,10 @@ class EventBackdrop extends FlxBackdrop {
alpha = 0.9;
// Separators
eventSecSeparator = new FlxBackdrop(null, Y, 0, 0);
eventSecSeparator.y = -2;
eventSecSeparator.visible = Options.charterShowSections;
eventBeatSeparator = new FlxBackdrop(null, Y, 0, 0);
eventBeatSeparator.y = -1;
eventBeatSeparator.visible = Options.charterShowBeats;
for(sep in [eventSecSeparator, eventBeatSeparator]) {
sep.makeSolid(1, 1, -1);
sep.alpha = 0.5;
sep.scrollFactor.set(1, 1);
}
eventSecSeparator.scale.set(20, 4);
eventSecSeparator.updateHitbox();
eventBeatSeparator.scale.set(10, 2);
eventBeatSeparator.updateHitbox();
eventBeatSeparator = new CharterEventGridSeperator();
eventBeatSeparator.makeSolid(1, 1, -1);
eventBeatSeparator.alpha = 0.5;
eventBeatSeparator.scrollFactor.set(1, 1);
bottomSeparator = new FlxSprite(0,-2);
bottomSeparator.makeSolid(1, 1, -1);
@@ -308,23 +428,32 @@ class EventBackdrop extends FlxBackdrop {
public override function draw() {
super.draw();
eventSecSeparator.spacing.y = (10 * Conductor.beatsPerMeasure * Conductor.stepsPerBeat) - 1;
eventBeatSeparator.spacing.y = (20 * Conductor.stepsPerBeat) - 1;
eventSecSeparator.cameras = cameras;
eventSecSeparator.x = (x+width) - 20;
if (eventSecSeparator.visible) eventSecSeparator.draw();
eventBeatSeparator.cameras = cameras;
eventBeatSeparator.x = (x+width) - 10;
if (eventBeatSeparator.visible) eventBeatSeparator.draw();
eventBeatSeparator.xPos = x+width;
eventBeatSeparator.draw();
topSeparator.x = (x+width) - 20;
topSeparator.cameras = this.cameras;
if (!eventSecSeparator.visible) topSeparator.draw();
if (!Options.charterShowSections) topSeparator.draw();
bottomSeparator.x = (x+width) - 20;
bottomSeparator.cameras = this.cameras;
bottomSeparator.draw();
}
}
class CharterEventGridSeperator extends CharterGridSeperatorBase {
public var xPos:Float = 0.0;
override private function drawBeats(offset:Float = 0.0) {
scale.set(10, 2);
updateHitbox();
x = xPos-10;
super.drawBeats(-2);
}
override private function drawMeasures(offset:Float = 0.0) {
scale.set(20, 4);
updateHitbox();
x = xPos-20;
super.drawMeasures(-3);
}
override private function drawTimeSignatureChangeGaps() {}
}
+379 -32
View File
@@ -1,12 +1,18 @@
package funkin.editors.charter;
import funkin.backend.shaders.CustomShader;
import funkin.backend.system.Conductor;
import flixel.group.FlxSpriteGroup;
import flixel.math.FlxPoint;
import flixel.system.FlxAssets.FlxGraphicAsset;
import funkin.backend.chart.ChartData.ChartEvent;
import funkin.backend.scripting.DummyScript;
import funkin.backend.scripting.Script;
import funkin.editors.charter.Charter.ICharterSelectable;
import funkin.editors.charter.CharterBackdropGroup.EventBackdrop;
import funkin.game.Character;
import funkin.game.HealthIcon;
import openfl.display.BitmapData;
class CharterEvent extends UISliceSprite implements ICharterSelectable {
public var events:Array<ChartEvent>;
@@ -45,19 +51,78 @@ class CharterEvent extends UISliceSprite implements ICharterSelectable {
sprite.colorTransform = colorTransform;
}
/**
* Pack data is a list of 4 strings separated by `________PACKSEP________`
* [0] Event Name
* [1] Event Script
* [2] Event JSON Info
* [3] Event Icon
* [4] Event UI Script / Icon Script
**/
@:dox(hide) public static function getPackData(name:String):Array<String> {
var packFile = Paths.pack('events/${name}');
if (Assets.exists(packFile)) {
return Assets.getText(packFile).split('________PACKSEP________');
}
return null;
}
@:dox(hide) public static function getUIScript(event:ChartEvent, caller:String):Script {
var uiScript = Paths.script('data/events/${event.name}.ui');
var script:Script = null;
if(Assets.exists(uiScript)) {
script = Script.create(uiScript);
} else {
var packData = getPackData(event.name);
if(packData != null) {
var scriptFile = packData[4];
if(scriptFile != null) {
script = Script.fromString(scriptFile, uiScript);
}
}
}
if(script != null && !(script is DummyScript)) {
// classes and functions
script.set("EventIconGroup", EventIconGroup); // automatically imported
script.set("EventNumber", EventNumber); // automatically imported
script.set("getIconFromStrumline", getIconFromStrumline);
script.set("getIconFromCharName", getIconFromCharName);
script.set("generateDefaultIcon", generateDefaultIcon);
script.set("generateEventIconDurationArrow", generateEventIconDurationArrow);
script.set("generateEventIconNumbers", generateEventIconNumbers);
script.set("generateEventIconWarning", generateEventIconWarning);
script.set("getPackData", getPackData);
script.set("getEventComponent", getEventComponent);
// data
script.set("event", event);
script.set("caller", caller);
script.load();
}
return script;
}
/**
* Generates the default event icon for the wanted event
* @param name The name of the event
* @return The icon
**/
private static function generateDefaultIcon(name:String) {
var isBase64:Bool = false;
var path:String = Paths.image('editors/charter/event-icons/$name');
var defaultPath = Paths.image('editors/charter/event-icons/Unknown');
if (!Assets.exists(path)) path = defaultPath;
if(!Assets.exists(path)) {
path = defaultPath;
var packPath = Paths.pack('events/$name');
if (Assets.exists(packPath)) {
var packText = Assets.getText(packPath).split('________PACKSEP________');
var packImg = packText[3];
if(packImg != null && packImg.length > 0) {
isBase64 = !packImg.startsWith("assets/");
path = packImg;
var packData = getPackData(name);
if(packData != null) {
var packImg = packData[3];
if(packImg != null && packImg.length > 0) {
isBase64 = !packImg.startsWith("assets/");
path = packImg;
}
}
}
path = path.trim();
@@ -75,27 +140,177 @@ class CharterEvent extends UISliceSprite implements ICharterSelectable {
graphic = defaultPath;
}
return new FlxSprite().loadGraphic(graphic);
return new FlxSprite(graphic);
}
public static function generateEventIcon(event:ChartEvent) {
return switch(event.name) {
default:
generateDefaultIcon(event.name);
case "Camera Movement":
// custom icon for camera movement
var state:Charter = cast FlxG.state;
if (event.params != null && event.params[0] != null && event.params[0] >= 0 && event.params[0] < state.strumLines.length) {
// camera movement, use health icon
var icon = Character.getIconFromCharName(state.strumLines.members[event.params[0]].strumLine.characters[0]);
var healthIcon = new HealthIcon(icon);
healthIcon.setUnstretchedGraphicSize(32, 32, false);
healthIcon.scrollFactor.set(1, 1);
healthIcon.active = false;
healthIcon;
} else
generateDefaultIcon(event.name);
/**
* Gets a component sprite from the editors/charter/event-icons/components folder
* If you wanna use a number, please use the EventNumber class instead
* @param type The type of component to get
* @param x The x position of the sprite (optional)
* @param y The y position of the sprite (optional)
* @return The component sprite
**/
public static function getEventComponent(type:String, x:Float = 0.0, y:Float = 0.0) {
var componentPath = Paths.image("editors/charter/event-icons/components/" + type);
if(Assets.exists(componentPath))
return new FlxSprite(x, y, componentPath);
Logs.trace('Could not find component $type', WARNING);
return null;
}
/**
* Expected to be called from inside of a ui script,
* calling this elsewhere might cause unexpected results or crashes
**/
public static function getIconFromStrumline(index:Null<Int>) {
var state = cast(FlxG.state, Charter);
if (index != null && index >= 0 && index < state.strumLines.length) {
return getIconFromCharName(state.strumLines.members[index].strumLine.characters[0]);
}
return null;
}
public static function getIconFromCharName(name:String) {
var icon = Character.getIconFromCharName(name);
var healthIcon = new HealthIcon(icon);
CoolUtil.setUnstretchedGraphicSize(healthIcon, 32, 32, false);
healthIcon.scrollFactor.set(1, 1);
healthIcon.active = false;
return healthIcon;
}
public static function generateEventIcon(event:ChartEvent, inMenu:Bool = true):FlxSprite {
var script = getUIScript(event, "event-icon");
if(script != null && !(script is DummyScript)) {
script.set("inMenu", inMenu);
if(script.get("generateIcon") != null) {
var res:FlxSprite = script.call("generateIcon");
if(res != null)
return res;
}
}
switch(event.name) {
case "Time Signature Change":
if(event.params != null && (event.params[0] >= 0 || event.params[1] >= 0)) {
var group = new EventIconGroup();
group.add(generateDefaultIcon(event.name));
group.add({ // top
var num = new EventNumber(9, -1, event.params[0], EventNumber.ALIGN_CENTER);
num.active = false;
num;
});
group.add({ // bottom
var num = new EventNumber(9, 10, event.params[1], EventNumber.ALIGN_CENTER);
num.active = false;
num;
});
if (Conductor.invalidEvents.contains(event)) generateEventIconWarning(group);
return group;
}
case "Continuous BPM Change":
if(event.params != null && event.params[1] != null) {
var group = new EventIconGroup();
group.add(generateDefaultIcon("BPM Change Start"));
if (!inMenu) {
generateEventIconDurationArrow(group, event.params[1]);
group.members[0].y -= 2;
generateEventIconNumbers(group, event.params[0], 3);
}
if (Conductor.invalidEvents.contains(event)) generateEventIconWarning(group);
return group;
} else {
return generateDefaultIcon("BPM Change Start");
}
case "BPM Change":
if(event.params != null && event.params[0] != null) {
var group = new EventIconGroup();
group.add(generateDefaultIcon(event.name));
if (!inMenu) {
group.members[0].y -= 2;
generateEventIconNumbers(group, event.params[0], 3);
}
if (Conductor.invalidEvents.contains(event)) generateEventIconWarning(group);
return group;
}
case "Scroll Speed Change":
if(event.params != null && !inMenu) {
var group = new EventIconGroup();
group.add(generateDefaultIcon(event.name));
if (event.params[0]) generateEventIconDurationArrow(group, event.params[2]);
group.members[0].y -= 2;
generateEventIconNumbers(group, event.params[1]);
return group;
}
case "Camera Movement":
// camera movement, use health icon
if(event.params != null) {
var icon = getIconFromStrumline(event.params[0]);
if(icon != null) return icon;
}
}
return generateDefaultIcon(event.name);
}
private static function generateEventIconNumbers(group:EventIconGroup, number:Float, x:Float = 4, y:Float = 15, spacing:Float = 5, precision:Int = 3) {
group.add({
var num = new EventNumber(x, y, number, EventNumber.ALIGN_CENTER, spacing, precision);
if (num.numWidth > 20) {
num.scale.x = num.scale.y = 20 / num.numWidth;
}
num.active = false;
num;
});
}
private static function generateEventIconDurationArrow(group:EventIconGroup, stepDuration:Float) {
//var group = new EventIconGroup();
//group.add(generateDefaultIcon(startIcon));
var xOffset = 4;
var yGap = 24;
var endGap = 2;
if (stepDuration >= 0.55) { //min time for showing arrow
var tail = new FlxSprite(xOffset, yGap);
var arrow = new FlxSprite(xOffset, (stepDuration * 40) + endGap);
var arrowSegment = new FlxSprite(xOffset, yGap);
tail.frames = arrow.frames = arrowSegment.frames = Paths.getSparrowAtlas("editors/charter/event-icons/components/arrow-down");
group.add({
tail.animation.addByPrefix("tail", "tail");
tail.animation.play("tail");
tail;
});
group.add({
arrowSegment.animation.addByPrefix("segment", "segment");
arrowSegment.animation.play("segment");
arrowSegment.scale.y = endGap + (stepDuration * 40) - (tail.height + yGap);
arrowSegment.updateHitbox();
arrowSegment.y += tail.height;
arrowSegment;
});
group.add({
arrow.animation.addByPrefix("arrow", "arrow");
arrow.animation.play("arrow");
arrow;
});
}
}
private static function generateEventIconWarning(group:EventIconGroup) {
for (spr in group) {
spr.colorTransform.redMultiplier = spr.colorTransform.greenMultiplier = spr.colorTransform.blueMultiplier = 0.5;
spr.colorTransform.redOffset = 100;
}
group.add(getEventComponent("warning", 16, -8));
group.copyColorTransformToChildren = false;
}
public override function onHovered() {
@@ -123,18 +338,150 @@ class CharterEvent extends UISliceSprite implements ICharterSelectable {
}
for(event in events) {
var spr = generateEventIcon(event);
var spr = generateEventIcon(event, false);
icons.push(spr);
members.push(spr);
}
draggable = true;
for (event in events)
if (event.name == "BPM Change") {
draggable = false;
break;
}
x = (snappedToGrid && eventsBackdrop != null ? eventsBackdrop.x : 0) - (bWidth = 37 + (icons.length * 22));
}
}
class EventIconGroup extends FlxSpriteGroup {
public var forceWidth:Float = 16;
public var forceHeight:Float = 16;
public var dontTransformChildren:Bool = true;
public var copyColorTransformToChildren:Bool = true;
public function new() {
super();
scrollFactor.set(1, 1);
}
override function preAdd(sprite:FlxSprite):Void
{
super.preAdd(sprite);
sprite.scrollFactor.set(1, 1);
}
public override function transformChildren<V>(Function:FlxSprite->V->Void, Value:V):Void
{
if (dontTransformChildren)
return;
super.transformChildren(Function, Value);
}
override function set_x(Value:Float):Float
{
if (exists && x != Value)
transformChildren(xTransform, Value - x); // offset
return x = Value;
}
override function set_y(Value:Float):Float
{
if (exists && y != Value)
transformChildren(yTransform, Value - y); // offset
return y = Value;
}
override function get_width() {
return forceWidth;
}
override function get_height() {
return forceHeight;
}
override public function draw() {
@:privateAccess
if (copyColorTransformToChildren && colorTransform != null) for (child in members) child.colorTransform.__copyFrom(colorTransform);
super.draw();
}
override function update(elapsed:Float) {
super.update(elapsed);
}
}
class EventNumber extends FlxSprite {
public static inline final ALIGN_NORMAL:Int = 0;
public static inline final ALIGN_CENTER:Int = 1;
public var digits:Array<Int> = [];
public static inline final FRAME_POINT:Int = 10;
public static inline final FRAME_NEGATIVE:Int = 11;
public var align:Int = ALIGN_NORMAL;
public var spacing:Float = 6;
public function new(x:Float, y:Float, number:Float, ?align:Int, spacing:Float = 6, precision:Int = 3) {
super(x, y);
this.digits = [];
this.align = align == null ? ALIGN_NORMAL : align;
this.spacing = spacing;
if (number == 0) {
this.digits.insert(0, 0);
}
else {
var decimals:Float = FlxMath.roundDecimal(Math.abs(number % 1), precision);
if (decimals > 0) this.digits.insert(0, FRAME_POINT);
while(decimals > 0) {
this.digits.push(Math.floor(decimals * 10));
decimals = FlxMath.roundDecimal((decimals * 10) % 1, precision);
}
var ints = Std.int(Math.abs(number));
if (ints == 0) this.digits.insert(0, 0);
while (ints > 0) {
this.digits.insert(0, ints % 10);
ints = Std.int(ints / 10);
}
if (number < 0) {
this.digits.insert(0, FRAME_NEGATIVE);
}
}
loadGraphic(Paths.image('editors/charter/event-icons/components/eventNums'), true, 6, 7);
}
override function update(elapsed:Float) {
super.update(elapsed);
}
override function draw() {
var baseX = x;
var offsetX = 0.0;
if(align == ALIGN_CENTER) offsetX = -(digits.length - 1) * spacing * Math.abs(scale.x) / 2;
x = baseX + offsetX;
for (i in 0...digits.length) {
frame = frames.frames[digits[i]];
super.draw();
x += spacing * Math.abs(scale.x);
}
x = baseX;
}
public var numWidth(get, never):Float;
private function get_numWidth():Float {
return Math.abs(scale.x) * spacing * digits.length;
}
public var numHeight(get, never):Float;
private function get_numHeight():Float {
return Math.abs(scale.y) * frameHeight;
}
public override function updateHitbox():Void {
var numWidth = this.numWidth;
var numHeight = this.numHeight;
width = numWidth;
height = numHeight;
offset.set(-0.5 * (numWidth - spacing * digits.length), -0.5 * (numHeight - frameHeight));
centerOrigin();
}
}
@@ -57,7 +57,7 @@ class CharterEventScreen extends UISubstateWindow {
});
eventsList.add(new EventButton(events[events.length-1], CharterEvent.generateEventIcon(events[events.length-1]), events.length-1, this, eventsList));
changeTab(events.length-1);
}));
}, chartEvent.step));
for (k=>i in events)
eventsList.add(new EventButton(i, CharterEvent.generateEventIcon(i), k, this, eventsList));
add(eventsList);
@@ -82,12 +82,12 @@ class CharterEventScreen extends UISubstateWindow {
else {
chartEvent.events = [for (i in eventsList.buttons.members) i.event];
chartEvent.refreshEventIcons();
Charter.instance.updateBPMEvents();
Charter.undos.addToUndo(CEditEvent(chartEvent, oldEvents, [for (event in events) Reflect.copy(event)]));
}
}
Charter.instance.updateBPMEvents();
close();
});
saveButton.x -= saveButton.bWidth;
@@ -112,23 +112,17 @@ class CharterEventScreen extends UISubstateWindow {
// destroy old elements
paramsFields = [];
for(e in paramsPanel) {
e.destroy();
paramsPanel.remove(e);
while(paramsPanel.members.length > 0) {
var e = paramsPanel.members.pop();
if(e != null) {
e.destroy();
}
}
if (id >= 0 && id < events.length) {
curEvent = id;
var curEvent = events[curEvent];
eventName.text = curEvent.name;
// add new elements
var y:Float = eventName.y + eventName.height + 10;
for(k=>param in EventsData.getEventParams(curEvent.name)) {
function addLabel() {
var label:UIText = new UIText(eventName.x, y, 0, param.name);
y += label.height + 4;
paramsPanel.add(label);
};
var value:Dynamic = CoolUtil.getDefault(curEvent.params[k], param.defValue);
var lastAdded = switch(param.type) {
@@ -199,6 +193,88 @@ class CharterEventScreen extends UISubstateWindow {
}
}
function generateEventUI(event:ChartEvent):Void {
var script = CharterEvent.getUIScript(event, "event-ui");
if(script != null && !(script is DummyScript)) {
script.set("paramsPanel", paramsPanel);
script.set("paramsFields", paramsFields);
if(script.get("generateUI") != null) {
if(script.call("generateUI") == false)
return;
}
}
// add new elements
var _y:Float = eventName.y + eventName.height + 10;
var params = EventsData.getEventParams(event.name);
for(k=>param in params) {
var x = eventName.x + (param.x == null ? 0 : param.x);
var y = (param.y == null ? _y : param.y);
function addLabel() {
var label:UIText = new UIText(x, y, 0, param.name);
_y += label.height + 4;
y += label.height + 4;
paramsPanel.add(label);
};
var value:Dynamic = CoolUtil.getDefault(event.params[k], param.defValue);
var lastAdded = switch(param.type) {
case TString:
addLabel();
var textBox:UITextBox = new UITextBox(x, y, cast value);
paramsPanel.add(textBox); paramsFields.push(textBox);
textBox;
case TBool:
var checkbox = new UICheckbox(x, y, param.name, cast value);
paramsPanel.add(checkbox); paramsFields.push(checkbox);
checkbox;
case TInt(min, max, step):
addLabel();
var numericStepper = new UINumericStepper(x, y, cast value, CoolUtil.getDefault(step, 1), 0, min, max);
paramsPanel.add(numericStepper); paramsFields.push(numericStepper);
numericStepper;
case TFloat(min, max, step, precision):
addLabel();
var numericStepper = new UINumericStepper(x, y, cast value, CoolUtil.getDefault(step, 1), precision, min, max);
paramsPanel.add(numericStepper); paramsFields.push(numericStepper);
numericStepper;
case TStrumLine:
addLabel();
var dropdown = new UIDropDown(x, y, 320, 32, [
for(k=>s in cast(FlxG.state, Charter).strumLines.members)
'Strumline #${k+1} (${s.strumLine.characters[0]})'
], cast value);
paramsPanel.add(dropdown); paramsFields.push(dropdown);
dropdown;
case TColorWheel:
addLabel();
var colorWheel = new UIColorwheel(x, y, CoolUtil.getColorFromDynamic(value));
paramsPanel.add(colorWheel); paramsFields.push(colorWheel);
colorWheel;
case TDropDown(options):
addLabel();
var dropdown = new UIDropDown(x, y, 320, 32, options, Std.int(Math.abs(options.indexOf(cast value))));
paramsPanel.add(dropdown); paramsFields.push(dropdown);
dropdown;
default:
paramsFields.push(null);
null;
}
if (lastAdded is UISliceSprite)
_y += cast(lastAdded, UISliceSprite).bHeight + 4;
else if (lastAdded is FlxSprite)
_y += cast(lastAdded, FlxSprite).height + 6;
}
if(script != null && !(script is DummyScript)) {
if(script.get("postGenerateUI") != null) {
script.set("params", params);
script.call("postGenerateUI");
}
}
}
public function saveCurTab() {
if (curEvent < 0) return;
@@ -1,9 +1,11 @@
package funkin.editors.charter;
import funkin.backend.system.Conductor;
import funkin.backend.chart.EventsData;
class CharterEventTypeSelection extends UISubstateWindow {
var callback:String->Void;
var eventStepTime:Float;
var buttons:Array<UIButton> = [];
@@ -13,9 +15,10 @@ class CharterEventTypeSelection extends UISubstateWindow {
var upIndicator:UIText;
var downIndicator:UIText;
public function new(callback:String->Void) {
public function new(callback:String->Void, eventStepTime:Float) {
super();
this.callback = callback;
this.eventStepTime = eventStepTime;
}
public override function create() {
@@ -32,6 +35,15 @@ class CharterEventTypeSelection extends UISubstateWindow {
buttonsBG.frames = Paths.getFrames('editors/ui/inputbox');
add(buttonsBG);
var disableConductorEvents:Bool = false;
var disableOnlyContinuousChanges:Bool = false;
for (change in Conductor.bpmChangeMap) {
if (change.continuous && MathUtil.greaterThanEqual(eventStepTime, change.stepTime) && MathUtil.lessThan(eventStepTime, change.endStepTime)) {
disableOnlyContinuousChanges = MathUtil.equal(eventStepTime, change.stepTime); //allow time sig and instant bpm changes on the same event
disableConductorEvents = !disableOnlyContinuousChanges;
}
}
for(k=>eventName in EventsData.eventsList) {
var button = new UIButton(0, (32 * k), eventName, function() {
close();
@@ -52,6 +64,11 @@ class CharterEventTypeSelection extends UISubstateWindow {
icon.x = button.x + 8;
icon.y = button.y + Math.abs(button.bHeight - icon.height) / 2;
add(icon);
if (disableConductorEvents && (eventName == "Time Signature Change" || eventName == "Continuous BPM Change" || eventName == "BPM Change") || (disableOnlyContinuousChanges && eventName == "Continuous BPM Change")) {
button.selectable = button.shouldPress = false;
button.autoAlpha = true;
}
}
windowSpr.bHeight = 61 + (32 * (17));
@@ -79,7 +96,7 @@ class CharterEventTypeSelection extends UISubstateWindow {
sinner += elapsed;
for (button in buttons)
button.selectable = buttonsBG.hovered;
if (button.shouldPress) button.selectable = buttonsBG.hovered;
buttonCameras.zoom = subCam.zoom;
@@ -62,8 +62,8 @@ class CharterMetaDataScreen extends UISubstateWindow {
add(new UIText(beatsPerMeasureStepper.x + 30, beatsPerMeasureStepper.y + 3, 0, "/", 22));
stepsPerBeatStepper = new UINumericStepper(beatsPerMeasureStepper.x + 30 + 24, beatsPerMeasureStepper.y, metadata.stepsPerBeat, 1, 0, 1, null, 54);
add(stepsPerBeatStepper);
denominatorStepper = new UINumericStepper(beatsPerMeasureStepper.x + 30 + 24, beatsPerMeasureStepper.y, Math.floor(16 / metadata.stepsPerBeat), 1, 0, 1, null, 54);
add(denominatorStepper);
add(title = new UIText(songNameTextBox.x, songNameTextBox.y + 10 + 46, 0, "Menus Data (Freeplay/Story)", 28));
@@ -136,7 +136,7 @@ class CharterMetaDataScreen extends UISubstateWindow {
}
public function saveMeta() {
for (stepper in [bpmStepper, beatsPerMeasureStepper, stepsPerBeatStepper])
for (stepper in [bpmStepper, beatsPerMeasureStepper, denominatorStepper])
@:privateAccess stepper.__onChange(stepper.label.text);
var customVals = {};
@@ -107,8 +107,8 @@ class SongCreationScreen extends UISubstateWindow {
songDataGroup.add(new UIText(beatsPerMeasureStepper.x + 30, beatsPerMeasureStepper.y + 3, 0, "/", 22));
stepsPerBeatStepper = new UINumericStepper(beatsPerMeasureStepper.x + 30 + 24, beatsPerMeasureStepper.y, 4, 1, 0, 1, null, 54);
songDataGroup.add(stepsPerBeatStepper);
denominatorStepper = new UINumericStepper(beatsPerMeasureStepper.x + 30 + 24, beatsPerMeasureStepper.y, 4, 1, 0, 1, null, 54);
songDataGroup.add(denominatorStepper);
instExplorer = new UIFileExplorer(songNameTextBox.x, songNameTextBox.y + 32 + 36, null, null, Flags.SOUND_EXT, function (res) {
var audioPlayer:UIAudioPlayer = new UIAudioPlayer(instExplorer.x + 8, instExplorer.y + 8, res);