Files
CodenameEngine/source/funkin/editors/charter/Charter.hx
T

2038 lines
64 KiB
Haxe

package funkin.editors.charter;
// ! FUCK YOU CHUF (your biggest fan -lunar) <3
import flixel.input.FlxPointer;
import openfl.geom.Point;
import openfl.geom.Rectangle;
import flixel.graphics.FlxGraphic;
import flixel.addons.display.FlxTiledSprite;
import funkin.editors.ui.UIContextMenu.UIContextMenuOptionSpr;
import funkin.editors.ui.UITopMenu.UITopMenuButton;
import funkin.editors.charter.CharterStrumline;
import funkin.editors.charter.CharterBackdropGroup.EventBackdrop;
import funkin.backend.system.framerate.Framerate;
import haxe.Json;
import flixel.input.keyboard.FlxKey;
import flixel.sound.FlxSound;
import flixel.math.FlxPoint;
import funkin.editors.charter.CharterBackdropGroup.CharterBackdropDummy;
import funkin.backend.system.Conductor;
import funkin.backend.chart.*;
import funkin.backend.chart.ChartData;
import flixel.addons.display.FlxBackdrop;
import funkin.editors.ui.UIContextMenu.UIContextMenuOption;
import funkin.editors.ui.UIState;
import flixel.util.FlxColor;
import openfl.display.BitmapData;
import funkin.backend.shaders.CustomShader;
class Charter extends UIState {
public static var __song:String;
static var __diff:String;
static var __reload:Bool;
var chart(get, null):ChartData;
private function get_chart()
return PlayState.SONG;
public static var instance(get, null):Charter;
private static inline function get_instance()
return FlxG.state is Charter ? cast FlxG.state : null;
public var charterBG:FunkinSprite;
public var uiGroup:FlxTypedGroup<FlxSprite> = new FlxTypedGroup<FlxSprite>();
public var topMenu:Array<UIContextMenuOption>;
@:noCompletion private var playbackIndex:Int = 7;
@:noCompletion private var snapIndex:Int = 6;
@:noCompletion private var noteIndex:Int = 5;
public var scrollBar:UIScrollBar;
public var songPosInfo:UIText;
public var quantButtons:Array<CharterQuantButton> = [];
public var playBackSlider:UISlider;
public var topMenuSpr:UITopMenu;
public var gridBackdrops:CharterBackdropGroup;
public var eventsBackdrop:EventBackdrop;
public var addEventSpr:CharterEventAdd;
public var gridBackdropDummy:CharterBackdropDummy;
public var noteHoverer:CharterNoteHoverer;
public var noteDeleteAnims:CharterDeleteAnim;
public var strumlineInfoBG:FlxSprite;
public var strumlineAddButton:CharterStrumlineButton;
public var strumlineLockButton:CharterStrumlineButton;
public var hitsound:FlxSound;
public var metronome:FlxSound;
public var vocals:FlxSound;
public var quant:Int = 16;
public var quants:Array<Int> = [4, 8, 12, 16, 20, 24, 32, 48, 64, 192]; // different quants
public var noteType:Int = 0;
public var noteTypes:Array<String> = [];
public var noteTypeText:UIText;
public var strumLines:CharterStrumLineGroup = new CharterStrumLineGroup();
public var notesGroup:CharterNoteGroup = new CharterNoteGroup();
public var eventsGroup:CharterEventGroup = new CharterEventGroup();
public var charterCamera:FlxCamera;
public var uiCamera:FlxCamera;
public var selectionBox:UISliceSprite;
public var autoSaveNotif:CharterAutoSaveUI;
public static var autoSaveTimer:Float = 0;
public static var selection:Selection;
public static var playtestInfo:PlaytestInfo;
public static var undos:UndoList<CharterChange>;
public static var clipboard:Array<CharterCopyboardObject> = [];
public static var waveformHandler:CharterWaveformHandler;
public function new(song:String, diff:String, reload:Bool = true) {
super();
if (song != null) {
__song = song;
__diff = diff;
__reload = reload;
}
}
public override function create() {
super.create();
WindowUtils.suffix = " (Chart Editor)";
SaveWarning.selectionClass = CharterSelection;
SaveWarning.saveFunc = () -> {_file_save(null);};
topMenu = [
{
label: "File",
childs: [
{
label: "New"
},
null,
{
label: "Save",
keybind: [CONTROL, S],
onSelect: _file_save,
},
{
label: "Save As...",
keybind: [CONTROL, SHIFT, S],
onSelect: _file_saveas,
},
null,
{
label: "Save Without Events",
keybind: [CONTROL, ALT, TAB, S],
onSelect: _file_save_no_events,
},
{
label: "Save Without Events As...",
keybind: [CONTROL, SHIFT, ALT, TAB, S],
onSelect: _file_saveas_no_events,
},
{
label: "Save Events Separately",
keybind: [CONTROL, TAB, S],
onSelect: _file_events_save,
},
{
label: "Save Events Separately As...",
keybind: [CONTROL, SHIFT, TAB, S],
onSelect: _file_events_saveas,
},
null,
{
label: "Save Meta",
keybind: [CONTROL, ALT, S],
onSelect: _file_meta_save,
},
{
label: "Save Meta As...",
keybind: [CONTROL, ALT ,SHIFT, S],
onSelect: _file_meta_saveas,
},
null,
{
label: "Export For FNF Legacy...",
onSelect: _file_saveas_fnflegacy,
},
{
label: "Export For Psych Engine...",
onSelect: _file_saveas_psych,
},
null,
{
label: "Exit",
onSelect: _file_exit
}
]
},
{
label: "Edit",
childs: [
{
label: "Undo",
keybind: [CONTROL, Z],
onSelect: _edit_undo
},
{
label: "Redo",
keybinds: [[CONTROL, Y], [CONTROL, SHIFT, Z]],
onSelect: _edit_redo
},
null,
{
label: "Copy",
keybind: [CONTROL, C],
onSelect: _edit_copy
},
{
label: "Paste",
keybind: [CONTROL, V],
onSelect: _edit_paste
},
null,
{
label: "Cut",
keybind: [CONTROL, X],
onSelect: _edit_cut
},
{
label: "Delete",
keybind: [DELETE],
onSelect: _edit_delete
}
]
},
{
label: "Chart",
childs: [
{
label: "Playtest",
keybind: [ENTER],
onSelect: _chart_playtest
},
{
label: "Playtest here",
keybind: [SHIFT, ENTER],
onSelect: _chart_playtest_here
},
null,
{
label: "Playtest as opponent",
keybind: [CONTROL, ENTER],
onSelect: _chart_playtest_opponent
},
{
label: "Playtest as opponent here",
keybind: [CONTROL, SHIFT, ENTER],
onSelect: _chart_playtest_opponent_here
},
null,
{
label: 'Enable scripts during playtesting',
onSelect: _chart_enablescripts,
icon: Options.charterEnablePlaytestScripts ? 1 : 0
},
null,
{
label: "Edit chart data",
color: 0xFF959829, icon: 4,
onCreate: function (button:UIContextMenuOptionSpr) {button.label.offset.x = button.icon.offset.x = -2;},
onSelect: chart_edit_data
},
{
label: "Edit metadata information",
color: 0xFF959829, icon: 4,
onCreate: function (button:UIContextMenuOptionSpr) {button.label.offset.x = button.icon.offset.x = -2;},
onSelect: chart_edit_metadata
}
]
},
{
label: "View",
childs: [
{
label: "Zoom in",
keybind: [CONTROL, NUMPADPLUS],
onSelect: _view_zoomin
},
{
label: "Zoom out",
keybind: [CONTROL, NUMPADMINUS],
onSelect: _view_zoomout
},
{
label: "Reset zoom",
keybind: [CONTROL, NUMPADZERO],
onSelect: _view_zoomreset
},
null,
{
label: 'Show Sections Separator',
onSelect: _view_showeventSecSeparator,
icon: Options.charterShowSections ? 1 : 0
},
{
label: 'Show Beats Separator',
onSelect: _view_showeventBeatSeparator,
icon: Options.charterShowBeats ? 1 : 0
},
null,
{
label: 'Low Detail Waveforms',
onSelect: _view_switchWaveformDetail,
icon: Options.charterLowDetailWaveforms ? 1 : 0
}
]
},
{
label: "Song",
childs: [
{
label: "Go back to the start",
keybind: [HOME],
onSelect: _song_start
},
{
label: "Go to the end",
keybind: [END],
onSelect: _song_end
},
null,
{
label: "Mute instrumental",
onSelect: _song_muteinst
},
{
label: "Mute voices",
onSelect: _song_mutevoices
}
]
},
{
label: "Note >",
childs: buildNoteTypesUI()
},
{
label: "Snap >",
childs: buildSnapsUI()
},
{
label: "Playback >",
childs: [
{
label: "Play/Pause",
keybind: [SPACE],
onSelect: _playback_play
},
null,
{
label: "↑ Speed 25%",
keybind: [PERIOD],
onSelect: _playback_speed_raise
},
{
label: "Reset Speed",
onSelect: _playback_speed_reset
},
{
label: "↓ Speed 25%",
keybind: [COMMA],
onSelect: _playback_speed_lower
},
null,
{
label: "Go back a section",
keybind: [A],
onSelect: _playback_back
},
{
label: "Go forward a section",
keybind: [D],
onSelect: _playback_forward
},
{
label: "Go to start of section",
keybind: [SHIFT, S],
onSelect: _playback_section_start
},
null,
{
label: "Go back a step",
keybind: [W],
onSelect: _playback_back_step
},
{
label: "Go forward a step",
keybind: [S],
onSelect: _playback_forward_step
},
null,
{
label: "Metronome",
onSelect: _playback_metronome,
icon: Options.charterMetronomeEnabled ? 1 : 0
},
{
label: "Visual metronome"
},
]
}
];
hitsound = FlxG.sound.load(Paths.sound('editors/charter/hitsound'));
metronome = FlxG.sound.load(Paths.sound('editors/charter/metronome'));
charterCamera = FlxG.camera;
uiCamera = new FlxCamera();
uiCamera.bgColor = 0;
FlxG.cameras.add(uiCamera);
for (camera in FlxG.cameras.list) camera.antialiasing = false;
charterBG = new FunkinSprite(0, 0, Paths.image('menus/menuDesat'));
charterBG.color = 0xFF181818;
charterBG.cameras = [charterCamera];
charterBG.screenCenter();
charterBG.scrollFactor.set();
add(charterBG);
gridBackdrops = new CharterBackdropGroup(strumLines);
gridBackdrops.notesGroup = this.notesGroup;
eventsBackdrop = new EventBackdrop();
eventsBackdrop.x = -eventsBackdrop.width;
eventsBackdrop.cameras = [charterCamera];
eventsGroup.eventsBackdrop = eventsBackdrop;
add(gridBackdropDummy = new CharterBackdropDummy(gridBackdrops));
selectionBox = new UISliceSprite(0, 0, 2, 2, 'editors/ui/selection');
selectionBox.visible = false;
selectionBox.scrollFactor.set(1, 1);
selectionBox.incorporeal = true;
noteHoverer = new CharterNoteHoverer();
noteDeleteAnims = new CharterDeleteAnim();
selectionBox.cameras = notesGroup.cameras = gridBackdrops.cameras =
noteHoverer.cameras = noteDeleteAnims.cameras = [charterCamera];
topMenuSpr = new UITopMenu(topMenu);
topMenuSpr.cameras = uiGroup.cameras = [uiCamera];
noteTypeText = new UIText(0, 0, 0, "(0) Default Note");
noteTypeText.cameras = [uiCamera];
scrollBar = new UIScrollBar(FlxG.width - 20, topMenuSpr.bHeight, 1000, 0, 100);
scrollBar.cameras = [uiCamera];
scrollBar.onChange = function(v) {
if (!FlxG.sound.music.playing)
Conductor.songPosition = Conductor.getTimeForStep(v) + Conductor.songOffset;
};
uiGroup.add(scrollBar);
songPosInfo = new UIText(FlxG.width - 30 - 400, scrollBar.y + 10, 400, "00:00\nBeat: 0\nStep: 0\nMeasure: 0\nBPM: 0\nTime Signature: 4/4");
songPosInfo.alignment = RIGHT;
uiGroup.add(songPosInfo);
playBackSlider = new UISlider(FlxG.width - 160 - 26 - 20, (23/2) - (12/2), 160, 1, [{start: 0.25, end: 1, size: 0.5}, {start: 1, end: 2, size: 0.5}], true);
playBackSlider.onChange = function (v) {
FlxG.sound.music.pitch = vocals.pitch = v;
for (strumLine in strumLines.members) strumLine.vocals.pitch = v;
};
uiGroup.add(playBackSlider);
quants.reverse();
for (quant in quants) {
var button:CharterQuantButton = new CharterQuantButton(0, 0, quant);
button.onClick = () -> {setquant(button.quant);};
quantButtons.push(cast uiGroup.add(button));
}
quants.reverse();
strumlineInfoBG = new UISprite();
strumlineInfoBG.loadGraphic(Paths.image('editors/charter/strumline-info-bg'));
strumlineInfoBG.y = 23;
strumlineInfoBG.scrollFactor.set();
autoSaveNotif = new CharterAutoSaveUI(20, strumlineInfoBG.y + strumlineInfoBG.height + 20);
uiGroup.add(autoSaveNotif);
strumlineAddButton = new CharterStrumlineButton("editors/new", "Create New");
strumlineAddButton.onClick = createStrumWithUI;
strumlineAddButton.animationOnClick = false;
strumlineAddButton.textColorLerp = 0.5;
strumlineLockButton = new CharterStrumlineButton("editors/charter/lock-strumline", "Lock/Unlock");
strumlineLockButton.onClick = function () {
if (strumLines != null) {
strumLines.draggable = !strumLines.draggable;
strumlineLockButton.textTweenColor.color = strumLines.draggable ? 0xFF5C95CA : 0xFFE16565;
}
};
strumlineAddButton.cameras = strumlineLockButton.cameras = [charterCamera];
strumlineInfoBG.cameras = [charterCamera];
strumLines.cameras = [charterCamera];
addEventSpr = new CharterEventAdd();
addEventSpr.x -= addEventSpr.bWidth;
addEventSpr.cameras = [charterCamera];
addEventSpr.alpha = 0;
// adds grid and notes so that they're ALWAYS behind the UI
add(gridBackdrops);
add(eventsBackdrop);
add(addEventSpr);
add(eventsGroup);
add(noteHoverer);
add(noteDeleteAnims);
add(notesGroup);
add(selectionBox);
add(strumlineInfoBG);
add(strumlineLockButton);
add(strumlineAddButton);
add(strumLines);
// add the top menu last OUT of the ui group so that it stays on top
add(topMenuSpr);
add(noteTypeText);
// add the ui group
add(uiGroup);
loadSong();
if(Framerate.isLoaded) {
Framerate.fpsCounter.alpha = 0.4;
Framerate.memoryCounter.alpha = 0.4;
Framerate.codenameBuildField.alpha = 0.4;
}
updateDisplaySprites();
// ! IF YOU EVER WANNA VIEW IN THE FUTURE, JUST USE A FLXSPRITE :D -lunar
/*var dataDisplay:FlxSprite = new FlxSprite().loadGraphic(waveformHandler.waveDatas.get("Voices.ogg"));
dataDisplay.scrollFactor.set(1, 0);
dataDisplay.scale.set(2, 2);
dataDisplay.updateHitbox();
dataDisplay.screenCenter(Y);
dataDisplay.cameras = [charterCamera]; dataDisplay.x = -dataDisplay.width; add(dataDisplay);*/
DiscordUtil.call("onEditorLoaded", ["Chart Editor", __song + " (" + __diff + ")"]);
}
override function destroy() {
__updatePlaytestInfo();
if(Framerate.isLoaded) {
Framerate.fpsCounter.alpha = 1;
Framerate.memoryCounter.alpha = 1;
Framerate.codenameBuildField.alpha = 1;
}
super.destroy();
}
public function loadSong() {
if (__reload) {
EventsData.reloadEvents();
PlayState.loadSong(__song, __diff, false, false);
__resetStatics();
}
Conductor.setupSong(PlayState.SONG);
noteTypes = PlayState.SONG.noteTypes;
FlxG.sound.setMusic(FlxG.sound.load(Paths.inst(__song, __diff)));
if (PlayState.SONG.meta.needsVoices != false) // null or true
vocals = FlxG.sound.load(Paths.voices(__song, __diff));
else
vocals = new FlxSound();
vocals.group = FlxG.sound.defaultMusicGroup;
gridBackdrops.createGrids(PlayState.SONG.strumLines.length);
for(strL in PlayState.SONG.strumLines)
createStrumline(strumLines.members.length, strL, false, false);
// create notes
notesGroup.autoSort = false;
var noteCount:Int = 0;
for (strL in PlayState.SONG.strumLines)
noteCount += strL.notes.length;
notesGroup.preallocate(noteCount);
var notesCreated:Int = 0;
for (i => strL in PlayState.SONG.strumLines)
for (note in strL.notes) {
var n = new CharterNote();
var t = Conductor.getStepForTime(note.time);
n.updatePos(t, note.id, Conductor.getStepForTime(note.time + note.sLen) - t, note.type, strumLines.members[i]);
notesGroup.members[notesCreated++] = n;
}
notesGroup.sortNotes();
notesGroup.autoSort = true;
// create events
eventsGroup.autoSort = false;
var __last:CharterEvent = null;
var __lastTime:Float = Math.NaN;
for(e in PlayState.SONG.events) {
if (e == null) continue;
if (__last != null && __lastTime == e.time) {
__last.events.push(e);
} else {
__last = new CharterEvent(Conductor.getStepForTime(e.time), [e]);
__lastTime = e.time;
eventsGroup.add(__last);
}
}
eventsGroup.sortEvents();
eventsGroup.autoSort = true;
for(e in eventsGroup.members)
e.refreshEventIcons();
buildNoteTypesUI();
refreshBPMSensitive();
// Just for now until i add event stacking -lunar
try {__relinkUndos();}
catch (e) {Logs.trace('Failed to relink undos: ${Std.string(e)}', ERROR);}
__applyPlaytestInfo();
}
public var __endStep:Float = 0;
public function refreshBPMSensitive() {
// refreshes everything dependant on BPM, and BPM changes
var length = FlxG.sound.music.getDefault(vocals).length;
scrollBar.length = __endStep = Conductor.getStepForTime(length);
gridBackdrops.bottomLimitY = __endStep * 40;
eventsBackdrop.bottomSeparator.y = gridBackdrops.bottomLimitY-2;
updateWaveforms();
}
inline function isSoundLoaded(sound:FlxSound) {
@:privateAccess
return sound != null && sound._sound != null && sound._sound.length > 0;
}
public function updateWaveforms() {
var wavesToGenerate:Array<{name:String, sound:FlxSound}> = [];
if(isSoundLoaded(FlxG.sound.music))
wavesToGenerate.push({name: "Inst.ogg", sound: FlxG.sound.music});
if (PlayState.SONG.meta.needsVoices != false && isSoundLoaded(vocals))
wavesToGenerate.push({name: "Voices.ogg", sound: vocals});
for (strumLine in strumLines)
if (strumLine.vocals != null && strumLine.strumLine.vocalsSuffix != null && strumLine.strumLine.vocalsSuffix != "" && isSoundLoaded(strumLine.vocals))
wavesToGenerate.push({
name: 'Voices${strumLine.strumLine.vocalsSuffix}.ogg',
sound: strumLine.vocals
});
var oldWaveformList:Array<String> = waveformHandler.waveformList;
var newWaveformList:Array<String> = [for (data in wavesToGenerate) data.name];
if (waveformHandler == null ? true : waveformHandler.ampsNeeded != __endStep*40) {
waveformHandler.clearWaveforms();
waveformHandler.ampsNeeded = __endStep * 40;
for (data in wavesToGenerate) {
waveformHandler.generateShader(data.name, data.sound);
waveformHandler.waveformList.push(data.name);
}
} else if (waveformHandler != null && waveformHandler.waveformList.length != newWaveformList.length) {
for (name in oldWaveformList)
if (!newWaveformList.contains(name))
waveformHandler.clearWaveform(name);
for (data in wavesToGenerate)
if (!oldWaveformList.contains(data.name))
waveformHandler.generateShader(data.name, data.sound);
waveformHandler.waveformList = newWaveformList;
}
for (strumLine in strumLines) {
if (strumLine.selectedWaveform == -1) continue;
var oldName:String = oldWaveformList[strumLine.selectedWaveform];
strumLine.selectedWaveform = waveformHandler.waveformList.indexOf(oldName);
}
}
public override function beatHit(curBeat:Int) {
super.beatHit(curBeat);
if (FlxG.sound.music.playing) {
if (Options.charterMetronomeEnabled)
metronome.replay();
}
}
/**
* NOTE AND CHARTER GRID LOGIC HERE
*/
#if REGION
public var gridActionType:CharterGridActionType = NONE;
public var dragStartPos:FlxPoint = new FlxPoint();
public var mousePos:FlxPoint = new FlxPoint();
public var selectionDragging:Bool = false;
public function updateSelectionLogic() {
function select(s:ICharterSelectable) {
if (FlxG.keys.pressed.CONTROL) selection.push(s);
else if (FlxG.keys.pressed.SHIFT) selection.remove(s);
else selection = [s];
}
for (group in [notesGroup, eventsGroup]) {
cast(group, FlxTypedGroup<Dynamic>).forEach(function(s) {
s.selected = false;
if (gridActionType == NONE) {
if (s is CharterNote) {
var n:CharterNote = cast s;
if ((n.hovered || n.sustainDraggable) && FlxG.mouse.justReleased) select(cast s);
} else if (FlxG.mouse.justReleased && s.hovered) select(cast s);
}
});
}
selection = __fixSelection(selection);
for(s in selection) s.selected = true;
}
var __autoSaveLocation:String = null;
public function updateAutoSaving(elapsed:Float) {
if (!Options.charterAutoSaves) return;
autoSaveTimer -= elapsed;
if (autoSaveTimer < Options.charterAutoSaveWarningTime && !autoSaveNotif.cancelled && !autoSaveNotif.showedAnimation) {
if (Options.charterAutoSavesSeperateFolder)
__autoSaveLocation = __diff.toLowerCase() + DateTools.format(Date.now(), "%m-%d_%H-%M");
autoSaveNotif.startAutoSave(autoSaveTimer,
!Options.charterAutoSavesSeperateFolder ? 'Saved chart at ${__diff.toLowerCase()}.json!' :
'Saved chart at $__autoSaveLocation.json!'
);
}
if (autoSaveTimer <= 0) {
autoSaveTimer = Options.charterAutoSaveTime;
if (!autoSaveNotif.cancelled) {
buildChart();
var songPath:String = '${Paths.getAssetsRoot()}/songs/${__song.toLowerCase()}';
if (Options.charterAutoSavesSeperateFolder)
Chart.save(songPath, PlayState.SONG, __autoSaveLocation, {saveMetaInChart: false, folder: "autosaves", prettyPrint: Options.editorPrettyPrint});
else
Chart.save(songPath, PlayState.SONG, __diff.toLowerCase(), {saveMetaInChart: false, prettyPrint: Options.editorPrettyPrint});
undos.save();
}
autoSaveNotif.cancelled = false;
}
}
var deletedNotes:Selection = new Selection();
public function updateNoteLogic(elapsed:Float) {
updateSelectionLogic();
/**
* NOTE DRAG HANDLING
*/
mousePos = FlxG.mouse.getWorldPosition(charterCamera);
if (!gridBackdropDummy.hoveredByChild && !FlxG.mouse.pressed)
gridActionType = NONE;
selectionBox.visible = false;
switch(gridActionType) {
case BOX_SELECTION:
if (gridBackdropDummy.hoveredByChild) {
selectionBox.visible = true;
if (FlxG.mouse.pressed) {
selectionBox.x = Math.min(mousePos.x, dragStartPos.x);
selectionBox.y = Math.min(mousePos.y, dragStartPos.y);
selectionBox.bWidth = Std.int(Math.abs(mousePos.x - dragStartPos.x));
selectionBox.bHeight = Std.int(Math.abs(mousePos.y - dragStartPos.y));
} else {
if (FlxG.keys.pressed.SHIFT) {
for (group in [notesGroup, eventsGroup])
for(n in cast(group, FlxTypedGroup<Dynamic>))
if (n.handleSelection(selectionBox) && selection.contains(n))
selection.remove(n);
} else if (FlxG.keys.pressed.CONTROL) {
for (group in [notesGroup, eventsGroup])
for(n in cast(group, FlxTypedGroup<Dynamic>))
if (n.handleSelection(selectionBox) && !selection.contains(n))
selection.push(n);
} else {
selection = [];
for (group in [notesGroup, eventsGroup])
for(n in cast(group, FlxTypedGroup<Dynamic>))
if (n.handleSelection(selectionBox))
selection.push(n);
}
selection = __fixSelection(selection);
gridActionType = NONE;
}
}
case INVALID_DRAG:
// do nothing, locked
if (!FlxG.mouse.pressed)
gridActionType = NONE;
case NOTE_DRAG:
selectionDragging = FlxG.mouse.pressed;
if (selectionDragging) {
gridBackdrops.draggingObj = null;
selection.loop(function (n:CharterNote) {
n.snappedToStrumline = false;
n.setPosition(n.fullID * 40 + (mousePos.x - dragStartPos.x), n.step * 40 + (mousePos.y - dragStartPos.y));
n.y = FlxMath.bound(n.y, 0, (__endStep*40) - n.height);
n.x = FlxMath.bound(n.x, 0, ((strumLines.members.length * 4)-1) * 40);
n.cursor = HAND;
}, function (e:CharterEvent) {
e.y = e.step * 40 + (mousePos.y - dragStartPos.y) - 17;
e.y = FlxMath.bound(e.y, -17, (__endStep*40)-17);
e.cursor = HAND;
});
currentCursor = HAND;
} else {
dragStartPos.set(Std.int(dragStartPos.x / 40) * 40, dragStartPos.y);
var verticalChange:Float = (mousePos.y - dragStartPos.y) / 40;
var horizontalChange:Int = CoolUtil.floorInt((mousePos.x - dragStartPos.x) / 40);
var undoDrags:Array<SelectionDragChange> = [];
for (s in selection) {
if (s.draggable) {
var changePoint:FlxPoint = FlxPoint.get(verticalChange, horizontalChange);
if (!FlxG.keys.pressed.SHIFT)
changePoint.x -= ((s.step + verticalChange) - quantStepRounded(s.step+verticalChange, verticalChange > 0 ? 0.35 : 0.65));
var boundedChange:FlxPoint = changePoint.clone();
// Some maths, so cool bro -lunar (i dont know why i quopte my self here)
if (s.step + changePoint.x < 0) boundedChange.x += Math.abs(s.step + changePoint.x);
if (s.step + changePoint.x > __endStep-1) boundedChange.x -= (s.step + changePoint.x) - (__endStep-1);
if (s is CharterNote) {
var note:CharterNote = cast s;
if (note.fullID + changePoint.y < 0) boundedChange.y += Math.abs(note.fullID + changePoint.y);
if (note.fullID + changePoint.y > (strumLines.members.length*4)-1) boundedChange.y -= (note.fullID + changePoint.y) - ((strumLines.members.length*4)-1);
}
s.handleDrag(boundedChange);
undoDrags.push({selectable:s, change: boundedChange});
changePoint.put();
}
if (s is CharterNote) cast(s, CharterNote).snappedToStrumline = true;
if (s is UISprite) cast(s, UISprite).cursor = BUTTON;
}
if (!(verticalChange == 0 && horizontalChange == 0)) {
notesGroup.sortNotes(); eventsGroup.sortEvents();
undos.addToUndo(CSelectionDrag(undoDrags));
}
gridActionType = NONE;
currentCursor = ARROW;
}
case NONE:
if (FlxG.mouse.justPressed)
FlxG.mouse.getWorldPosition(charterCamera, dragStartPos);
else if (FlxG.mouse.justPressedRight) {
closeCurrentContextMenu();
gridActionType = DELETE_SELECTION;
}
if (gridBackdropDummy.hovered) {
// AUTO DETECT
if (FlxG.mouse.pressed && (Math.abs(mousePos.x - dragStartPos.x) > 20 || Math.abs(mousePos.y - dragStartPos.y) > 20))
gridActionType = BOX_SELECTION;
var id = Math.floor(mousePos.x / 40);
var mouseOnGrid = id >= 0 && id < 4 * gridBackdrops.strumlinesAmount && mousePos.y >= 0;
if (FlxG.mouse.justReleased) {
for (n in selection) n.selected = false;
selection = [];
if (mouseOnGrid && mousePos.y > 0 && mousePos.y < (__endStep)*40) {
var note = new CharterNote();
note.updatePos(
FlxMath.bound(FlxG.keys.pressed.SHIFT ? ((mousePos.y-20) / 40) : quantStep(mousePos.y/40), 0, __endStep-1),
id % 4, 0, noteType, strumLines.members[Std.int(id/4)]
);
notesGroup.add(note);
selection = [note];
undos.addToUndo(CCreateSelection([note]));
}
}
} else if (gridBackdropDummy.hoveredByChild) {
if (FlxG.mouse.pressed) {
var noteHovered:Bool = false;
for(n in selection) if (n.hovered) {noteHovered = true; break;}
var noteSusDrag:Bool = false;
for(s in selection) {
if (!(s is CharterNote)) continue;
var n:CharterNote = cast s;
if (n.sustainDraggable) {noteSusDrag = true; break;}
}
if ((Math.abs(mousePos.x - dragStartPos.x) > (noteSusDrag ? 1 : 5) || Math.abs(mousePos.y - dragStartPos.y) > (noteSusDrag ? 1 : 5))) {
if (noteHovered) gridActionType = noteHovered ? NOTE_DRAG : INVALID_DRAG;
if (noteSusDrag) gridActionType = SUSTAIN_DRAG;
}
}
}
case SUSTAIN_DRAG:
selectionDragging = FlxG.mouse.pressed;
if (selectionDragging) {
currentCursor = BUTTON;
selection.loop(function (n:CharterNote) {
var change:Float = Math.max((mousePos.y-(FlxG.keys.pressed.SHIFT ? dragStartPos.y : quantStep(dragStartPos.y))) / 40, -n.susLength);
n.tempSusLength = change;
if (!FlxG.keys.pressed.SHIFT)
n.tempSusLength -= (n.susLength + change) - quantStepRounded(n.susLength + change, change > 0 ? 0.35 : 0.65);
@:privateAccess n.__susInstaLerp = FlxG.keys.pressed.SHIFT;
});
} else {
var undoChanges:Array<NoteSustainChange> = [];
selection.loop(function (n:CharterNote) {
var oldSusLen:Float = n.susLength;
n.susLength += n.tempSusLength;
n.tempSusLength = 0;
@:privateAccess n.__susInstaLerp = false;
n.updatePos(n.step, n.id, n.susLength, n.type);
undoChanges.push({before: oldSusLen, after: n.susLength, note: n});
});
undos.addToUndo(CEditSustains(undoChanges));
gridActionType = NONE;
currentCursor = ARROW;
}
case DELETE_SELECTION:
notesGroup.forEach(function(n) {
if (n.hovered || n.sustainDraggable) {
deletedNotes.push(n);
deleteSingleSelection(n, false);
if (selection.contains(n)) selection.remove(n);
noteDeleteAnims.deleteNotes.push({
note: n, time: noteDeleteAnims.deleteTime
});
}
});
if (FlxG.mouse.justReleasedRight) {
if (deletedNotes.length > 0) undos.addToUndo(CDeleteSelection(deletedNotes.copy()));
else if (noteDeleteAnims.garbageIcon.alpha <= .5) {
var mousePos = FlxG.mouse.getScreenPosition(uiCamera);
closeCurrentContextMenu();
openContextMenu(topMenu[1].childs, null, mousePos.x, mousePos.y);
mousePos.put();
}
gridActionType = NONE; deletedNotes = [];
}
}
addEventSpr.selectable = !selectionBox.visible;
var inBoundsY:Bool = (mousePos.y > 0 && mousePos.y < (__endStep)*40);
// Event Spr
if (mousePos.x < 0 && mousePos.x > -addEventSpr.bWidth && gridActionType == NONE && inBoundsY) {
addEventSpr.incorporeal = false;
addEventSpr.sprAlpha = lerp(addEventSpr.sprAlpha, 0.75, 0.25);
var event = getHoveredEvent(mousePos.y);
if (event != null) addEventSpr.updateEdit(event);
else addEventSpr.updatePos(FlxG.keys.pressed.SHIFT ? ((mousePos.y) / 40) : quantStepRounded(mousePos.y/40));
} else addEventSpr.sprAlpha = lerp(addEventSpr.sprAlpha, 0, 0.25);
noteHoverer.showHoverer = Charter.instance.gridBackdropDummy.hovered;
}
public function quantStep(step:Float):Float {
var stepMulti:Float = 1/(quant/16);
return Math.floor(step/stepMulti) * stepMulti;
}
public function quantStepRounded(step:Float, ?roundRatio:Float = 0.5):Float {
var stepMulti:Float = 1/(quant/16);
return ratioRound(step/stepMulti, roundRatio) * stepMulti;
}
public function ratioRound(val:Float, ratio:Float):Int
return Math.floor(val) + ((Math.abs(val % 1) > ratio ? 1 : 0) * (val > 0 ? 1 : -1));
public function getHoveredEvent(y:Float) {
var eventHovered:CharterEvent = null;
eventsGroup.forEach(function(e) {
if (eventHovered != null)
return;
if (e.hovered || (y >= e.y && y < (e.y + e.bHeight)))
eventHovered = e;
});
return eventHovered;
}
public function deleteSingleSelection(selected:ICharterSelectable, addToUndo:Bool = true):Null<ICharterSelectable> {
if (selected == null) return selected;
if (selected is CharterNote) {
var note:CharterNote = cast selected;
note.strumLineID = strumLines.members.indexOf(note.strumLine);
note.strumLine = null; // For static undos :D
notesGroup.remove(note);
note.kill();
} else if (selected is CharterEvent) {
var event:CharterEvent = cast selected;
eventsGroup.remove(event);
event.kill();
}
if (addToUndo)
undos.addToUndo(CDeleteSelection([selected]));
return null;
}
public function createSelection(selection:Selection, addToUndo:Bool = true) {
if (selection.length <= 0) return [];
notesGroup.autoSort = false;
selection.loop(function (n:CharterNote) {
n.strumLine = strumLines.members[n.strumLineID];
n.revive();
notesGroup.add(n);
}, function (e:CharterEvent) {
e.revive();
eventsGroup.add(e);
e.refreshEventIcons();
}, false);
notesGroup.sortNotes();
notesGroup.autoSort = true;
for (s in selection)
if (s is CharterEvent) {
Charter.instance.updateBPMEvents();
break;
}
if (addToUndo)
undos.addToUndo(CCreateSelection(selection));
return [];
}
public function deleteSelection(selection:Selection, addToUndo:Bool = true) {
if (selection.length <= 0) return [];
notesGroup.autoSort = false;
for (objects in [notesGroup, eventsGroup]) {
var group = cast(objects, FlxTypedGroup<Dynamic>);
var member = 0;
while(member < group.members.length) {
var s = group.members[member];
if (selection.contains(s))
deleteSingleSelection(s, false);
else member++;
}
}
notesGroup.sortNotes();
notesGroup.autoSort = true;
for (s in selection)
if (s is CharterEvent) {
Charter.instance.updateBPMEvents();
break;
}
if (addToUndo)
undos.addToUndo(CDeleteSelection(selection));
return [];
}
// STRUMLINE DELETION/CREATION
public function createStrumline(strumLineID:Int, strL:ChartStrumLine, addToUndo:Bool = true, ?__createNotes:Bool = true) {
var cStr = new CharterStrumline(strL);
strumLines.insert(strumLineID, cStr);
strumLines.snapStrums();
if (__createNotes) {
var toBeCreated:Selection = [];
for(note in strL.notes) {
var n = new CharterNote();
var t = Conductor.getStepForTime(note.time);
n.updatePos(t, note.id, Conductor.getStepForTime(note.time + note.sLen) - t, note.type, cStr);
notesGroup.add(n);
}
createSelection(toBeCreated, false);
}
if (addToUndo)
undos.addToUndo(CCreateStrumLine(strumLineID, strL));
}
public function deleteStrumline(strumLineID:Int, addToUndo:Bool = true) {
var undoNotes:Array<ChartNote> = [];
removeStrumlineFromSelection(strumLineID);
var i = 0;
var toBeDeleted:Selection = [];
for (note in notesGroup.members)
if (note.strumLineID == strumLineID) {
undoNotes.push(buildNote(note));
toBeDeleted.push(note);
}
deleteSelection(toBeDeleted, false);
var strL = strumLines.members[strumLineID].strumLine;
strumLines.members[strumLineID].destroy();
strumLines.members.remove(strumLines.members[strumLineID]);
strumLines.snapStrums();
if (addToUndo) {
var newStrL = Reflect.copy(strL);
newStrL.notes = undoNotes;
undos.addToUndo(CDeleteStrumLine(strumLineID, newStrL));
}
}
public function getStrumlineID(strL:ChartStrumLine):Int {
for (index=>strumLine in strumLines.members) {
if (strumLine.strumLine == strL)
return index;
}
return -1;
}
public function createStrumWithUI() {
FlxG.state.openSubState(new CharterStrumlineScreen(strumLines.members.length, null, (_) -> {
if (_ != null) {
createStrumline(strumLines.members.length, _);
strumlineAddButton.textTweenColor.color = 0xFF00FF00;
strumlineAddButton.pressAnimation(true);
}
}));
}
public inline function deleteStrumlineFromData(strL:ChartStrumLine)
deleteStrumline(getStrumlineID(strL));
public inline function editStrumline(strL:ChartStrumLine) {
var strID = getStrumlineID(strL);
var oldData:ChartStrumLine = Reflect.copy(strL);
FlxG.state.openSubState(new CharterStrumlineScreen(strID, strL, (_) -> {
strumLines.members[strID].strumLine = _;
strumLines.members[strID].updateInfo();
undos.addToUndo(CEditStrumLine(strID, oldData, _));
if (oldData.vocalsSuffix != _.vocalsSuffix) updateWaveforms();
}));
}
public inline function removeStrumlineFromSelection(strumLineID:Int) {
var i = 0;
while(i < selection.length) {
if (selection[i] is CharterNote) {
var note:CharterNote = cast selection[i];
if (note.strumLineID == strumLineID)
selection.remove(note);
else i++;
}
}
}
#end
var __crochet:Float;
var __firstFrame:Bool = true;
public override function update(elapsed:Float) {
updateNoteLogic(elapsed);
updateAutoSaving(elapsed);
if (FlxG.sound.music.playing || __firstFrame) {
gridBackdrops.conductorSprY = curStepFloat * 40;
} else {
gridBackdrops.conductorSprY = lerp(gridBackdrops.conductorSprY, curStepFloat * 40, __firstFrame ? 1 : 1/3);
}
charterCamera.scroll.set(
((((40*4) * gridBackdrops.strumlinesAmount) - FlxG.width) / 2),
gridBackdrops.conductorSprY - (FlxG.height * 0.5)
);
if (topMenuSpr.members[playbackIndex] != null) {
var playBackButton:UITopMenuButton = cast topMenuSpr.members[playbackIndex];
playBackButton.x = playBackSlider.x-playBackSlider.startText.width-10-playBackSlider.valueStepper.bWidth-playBackButton.bWidth-10;
playBackButton.label.offset.x = -1;
if (topMenuSpr.members[snapIndex] != null) {
var snapButton:UITopMenuButton = cast topMenuSpr.members[snapIndex];
var lastButtonX = playBackButton.x-10;
var buttonI:Int = 0;
for (button in quantButtons) {
button.visible = ((button.quant == quant) ||
(button.quant == quants[FlxMath.wrap(quants.indexOf(quant)-1, 0, quants.length-1)]) ||
(button.quant == quants[FlxMath.wrap(quants.indexOf(quant)+1, 0, quants.length-1)]));
button.selectable = button.visible;
if (!button.visible) continue;
button.x = lastButtonX -= button.bWidth;
button.framesOffset = button.quant == quant ? 9 : 0;
button.alpha = button.quant == quant ? 1 : (button.hovered ? 0.4 : 0);
}
snapButton.x = (lastButtonX -= snapButton.bWidth)-10;
}
}
if (topMenuSpr.members[noteIndex] != null) {
var noteTopButton:UITopMenuButton = cast topMenuSpr.members[noteIndex];
noteTypeText.x = noteTopButton.x + noteTopButton.bWidth + 6;
noteTypeText.y = Std.int((noteTopButton.bHeight - noteTypeText.height) / 2);
}
noteTypeText.text = '($noteType) ${noteTypes[noteType-1] == null ? "Default Note" : noteTypes[noteType-1]}';
super.update(elapsed);
scrollBar.size = (FlxG.height / 40 / charterCamera.zoom);
scrollBar.start = Conductor.curStepFloat - (scrollBar.size / 2);
if (gridBackdrops.strumlinesAmount != strumLines.members.length)
updateDisplaySprites();
// TODO: canTypeText in case an ui input element is focused
if (true) {
__crochet = ((60 / Conductor.bpm) * 1000);
if(FlxG.keys.justPressed.ANY && !strumLines.isDragging && this.currentFocus == null)
UIUtil.processShortcuts(topMenu);
if (FlxG.keys.pressed.CONTROL) {
if (FlxG.mouse.wheel != 0) {
zoom += 0.25 * FlxG.mouse.wheel;
__camZoom = Math.pow(2, zoom);
}
} else {
if (!FlxG.sound.music.playing) {
Conductor.songPosition -= (__crochet * FlxG.mouse.wheel) - Conductor.songOffset;
}
}
}
var songLength = FlxG.sound.music.getDefault(vocals).length;
Conductor.songPosition = FlxMath.bound(Conductor.songPosition + Conductor.songOffset, 0, songLength);
if (Conductor.songPosition >= songLength - Conductor.songOffset) {
FlxG.sound.music.pause();
vocals.pause();
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}';
if (charterCamera.zoom != (charterCamera.zoom = lerp(charterCamera.zoom, __camZoom, __firstFrame ? 1 : 0.125)))
updateDisplaySprites();
if (strumLines != null)
strumlineLockButton.button.animation.play(strumLines.draggable ? "1" : "0", true);
WindowUtils.prefix = undos.unsaved ? "* " : "";
SaveWarning.showWarning = undos.unsaved;
__firstFrame = false;
}
public static var startTime:Float = 0;
public static var startHere:Bool = false;
function updateDisplaySprites() {
gridBackdrops.strumlinesAmount = strumLines.members.length;
charterBG.scale.set(1 / charterCamera.zoom, 1 / charterCamera.zoom);
strumlineInfoBG.scale.set(FlxG.width / charterCamera.zoom, 1);
strumlineInfoBG.updateHitbox();
strumlineInfoBG.screenCenter(X);
strumlineInfoBG.y = -(((FlxG.height - (2 * topMenuSpr.bHeight)) / charterCamera.zoom) - FlxG.height) / 2;
for(id=>str in strumLines.members)
if (str != null) str.y = strumlineInfoBG.y;
strumlineAddButton.y = strumlineInfoBG.y;
strumlineLockButton.y = strumlineInfoBG.y;
}
var zoom(default, set):Float = 0;
var __camZoom(default, set):Float = 1;
function set_zoom(val:Float) {
return zoom = FlxMath.bound(val, -3.5, 1.75); // makes zooming not lag behind when continuing scrolling
}
function set___camZoom(val:Float) {
return __camZoom = FlxMath.bound(val, 0.1, 3);
}
// TOP MENU OPTIONS
#if REGION
function _file_exit(_) {
if (undos.unsaved) SaveWarning.triggerWarning();
else {undos = null; FlxG.switchState(new CharterSelection()); PlayState.resetSongInfos(); Charter.instance.__clearStatics();}
}
function _file_save(_) {
#if sys
saveTo('${Paths.getAssetsRoot()}/songs/${__song.toLowerCase()}');
undos.save();
return;
#end
_file_saveas(_);
}
function _file_saveas(_) {
openSubState(new SaveSubstate(Json.stringify(Chart.filterChartForSaving(PlayState.SONG, false), null, Options.editorPrettyPrint ? "\t" : null), {
defaultSaveFile: '${__diff.toLowerCase()}.json'
}));
undos.save();
}
function _file_save_no_events(_) {
#if sys
saveTo('${Paths.getAssetsRoot()}/songs/${__song.toLowerCase()}', true);
undos.save();
return;
#end
_file_saveas(_);
}
function _file_saveas_no_events(_) {
openSubState(new SaveSubstate(Json.stringify(Chart.filterChartForSaving(PlayState.SONG, false, false), null, Options.editorPrettyPrint ? "\t" : null), {
defaultSaveFile: '${__diff.toLowerCase()}.json'
}));
undos.save();
}
function _file_meta_save(_) {
#if sys
CoolUtil.safeSaveFile(
'${Paths.getAssetsRoot()}/songs/${__song.toLowerCase()}/meta.json',
Json.stringify(PlayState.SONG.meta == null ? {} : PlayState.SONG.meta, null, "\t")
);
#else
_file_meta_saveas(_);
#end
}
function _file_meta_saveas(_) {
openSubState(new SaveSubstate(Json.stringify(PlayState.SONG.meta == null ? {} : PlayState.SONG.meta, null, "\t"), { // always pretty print meta
defaultSaveFile: 'meta.json'
}));
}
function _file_saveas_fnflegacy(_) {
openSubState(new SaveSubstate(Json.stringify(FNFLegacyParser.encode(PlayState.SONG), null, Options.editorPrettyPrint ? "\t" : null), {
defaultSaveFile: '${__song.toLowerCase().replace(" ", "-")}${__diff.toLowerCase() == "normal" ? "" : '-${__diff.toLowerCase()}'}.json',
}));
}
function _file_saveas_psych(_) {
openSubState(new SaveSubstate(Json.stringify(PsychParser.encode(PlayState.SONG), null, Options.editorPrettyPrint ? "\t" : null), {
defaultSaveFile: '${__song.toLowerCase().replace(" ", "-")}${__diff.toLowerCase() == "normal" ? "" : '-${__diff.toLowerCase()}'}.json',
}));
}
function _file_events_save(_) {
#if sys
CoolUtil.safeSaveFile(
'${Paths.getAssetsRoot()}/songs/${__song.toLowerCase()}/events.json',
Json.stringify({events: PlayState.SONG.events == null ? [] : PlayState.SONG.events}, null, Options.editorPrettyPrint ? "\t" : null)
);
#else
_file_events_saveas(_);
#end
}
function _file_events_saveas(_) {
#if sys
openSubState(new SaveSubstate(Json.stringify({events: PlayState.SONG.events == null ? [] : PlayState.SONG.events}, null, Options.editorPrettyPrint ? "\t" : null), {
defaultSaveFile: 'events.json'
}));
#end
}
#if sys
function saveTo(path:String, separateEvents:Bool = false) {
buildChart();
Chart.save(path, PlayState.SONG, __diff.toLowerCase(), {saveMetaInChart: false, saveEventsInChart: !separateEvents, prettyPrint: Options.editorPrettyPrint});
}
#end
function _edit_copy(_) {
if(selection.length == 0) return;
var minStep:Float = selection[0].step;
for(s in selection)
if (s.step < minStep) minStep = s.step;
clipboard = [
for (s in selection)
if (s is CharterNote) {
var note:CharterNote = cast s;
CNote(note.step - minStep, note.id, note.strumLineID, note.susLength, note.type);
} else if (s is CharterEvent) {
var event = cast(s,CharterEvent);
CEvent(event.step - minStep, [for (event in event.events) Reflect.copy(event)]);
}
];
}
function _edit_paste(_) {
if (clipboard.length <= 0) return;
var minStep = curStep;
var sObjects:Array<ICharterSelectable> = [];
for(c in clipboard) {
switch(c) {
case CNote(step, id, strumLineID, susLength, type):
var note = new CharterNote();
note.updatePos(minStep + step, id, susLength, type, strumLines.members[Std.int(FlxMath.bound(strumLineID, 0, strumLines.length-1))]);
notesGroup.add(note);
sObjects.push(note);
case CEvent(step, events):
var event = new CharterEvent(minStep + step, events);
event.refreshEventIcons();
eventsGroup.add(event);
sObjects.push(event);
}
}
selection = sObjects;
_edit_copy(_); // to fix stupid bugs
undos.addToUndo(CCreateSelection(sObjects.copy()));
}
function _edit_cut(_) {
if (selection == null || selection.length == 0) return;
_edit_copy(_);
deleteSelection(selection, false);
}
function _edit_delete(_) {
if (selection == null || selection.length == 0) return;
selection.loop((n:CharterNote) -> {
noteDeleteAnims.deleteNotes.push({note: n, time: noteDeleteAnims.deleteTime});
});
selection = deleteSelection(selection, true);
}
function _edit_undo(_) {
if (strumLines.isDragging || selectionDragging || (subState != null && !(subState is UIContextMenu))) return;
selection = [];
var undo = undos.undo();
switch(undo) {
case null: // do nothing
case CDeleteStrumLine(strumLineID, strumLine):
createStrumline(strumLineID, strumLine, false);
case CCreateStrumLine(strumLineID, strumLine):
deleteStrumline(strumLineID, false);
case COrderStrumLine(strumLineID, oldID, newID):
var strumLine:CharterStrumline = strumLines.members[strumLineID];
strumLines.orderStrumline(strumLine, oldID);
undos.redoList[0] = COrderStrumLine(strumLines.members.indexOf(strumLine), oldID, newID);
case CEditStrumLine(strumLineID, oldStrumLine, newStrumLine):
strumLines.members[strumLineID].strumLine = oldStrumLine;
strumLines.members[strumLineID].updateInfo();
case CCreateSelection(selection):
deleteSelection(selection, false);
case CDeleteSelection(selection):
createSelection(selection, false);
case CSelectionDrag(selectionDrags):
for (s in selectionDrags)
if (s.selectable.draggable) s.selectable.handleDrag(s.change * -1);
selection = [for (s in selectionDrags) s.selectable];
case CEditSustains(changes):
for(n in changes)
n.note.updatePos(n.note.step, n.note.id, n.before, n.note.type);
case CEditEvent(event, oldEvents, newEvents):
event.events = oldEvents.copy();
event.refreshEventIcons();
Charter.instance.updateBPMEvents();
case CEditChartData(oldData, newData):
PlayState.SONG.stage = oldData.stage;
PlayState.SONG.scrollSpeed = oldData.speed;
case CEditNoteTypes(oldArray, newArray):
noteTypes = oldArray;
changeNoteType(null, false);
case CEditSpecNotesType(notes, oldTypes, newTypes):
for(i=>note in notes) note.updatePos(note.step, note.id, note.susLength, oldTypes[i]);
}
}
function _edit_redo(_) {
if (strumLines.isDragging || selectionDragging || (subState != null && !(subState is UIContextMenu))) return;
selection = [];
var redo = undos.redo();
switch(redo) {
case null: // do nothing
case CDeleteStrumLine(strumLineID, strumLine):
deleteStrumline(strumLineID, false);
case CCreateStrumLine(strumLineID, strumLine):
createStrumline(strumLineID, strumLine, false);
case COrderStrumLine(strumLineID, oldID, newID):
var strumLine:CharterStrumline = strumLines.members[strumLineID];
strumLines.orderStrumline(strumLine, newID);
undos.undoList[0] = COrderStrumLine(strumLines.members.indexOf(strumLine), oldID, newID);
case CEditStrumLine(strumLineID, oldStrumLine, newStrumLine):
strumLines.members[strumLineID].strumLine = newStrumLine;
strumLines.members[strumLineID].updateInfo();
case CCreateSelection(selection):
createSelection(selection, false);
case CDeleteSelection(selection):
deleteSelection(selection, false);
case CSelectionDrag(selectionDrags):
for (s in selectionDrags)
if (s.selectable.draggable) s.selectable.handleDrag(s.change);
//this.selection = selection;
case CEditSustains(changes):
for(n in changes)
n.note.updatePos(n.note.step, n.note.id, n.after, n.note.type);
case CEditEvent(event, oldEvents, newEvents):
event.events = newEvents.copy();
event.refreshEventIcons();
Charter.instance.updateBPMEvents();
case CEditChartData(oldData, newData):
PlayState.SONG.stage = newData.stage;
PlayState.SONG.scrollSpeed = newData.speed;
case CEditNoteTypes(oldArray, newArray):
noteTypes = newArray;
changeNoteType(null, false);
case CEditSpecNotesType(notes, oldTypes, newTypes):
for(i=>note in notes) note.updatePos(note.step, note.id, note.susLength, newTypes[i]);
}
}
inline function _chart_playtest(_)
playtestChart(0, false);
inline function _chart_playtest_here(_)
playtestChart(Conductor.songPosition, false, true);
inline function _chart_playtest_opponent(_)
playtestChart(0, true);
inline function _chart_playtest_opponent_here(_)
playtestChart(Conductor.songPosition, true, true);
function _chart_enablescripts(t) {
t.icon = (Options.charterEnablePlaytestScripts = !Options.charterEnablePlaytestScripts) ? 1 : 0;
}
function chart_edit_data(_)
FlxG.state.openSubState(new ChartDataScreen(PlayState.SONG));
function chart_edit_metadata(_)
FlxG.state.openSubState(new CharterMetaDataScreen(PlayState.SONG.meta));
function _playback_play(_) {
if (Conductor.songPosition >= FlxG.sound.music.getDefault(vocals).length - Conductor.songOffset) return;
if (FlxG.sound.music.playing) {
FlxG.sound.music.pause();
vocals.pause();
for (strumLine in strumLines.members) strumLine.vocals.pause();
} else {
FlxG.sound.music.play();
vocals.play();
vocals.time = FlxG.sound.music.time = Conductor.songPosition + Conductor.songOffset * 2;
for (strumLine in strumLines.members) {
strumLine.vocals.play();
strumLine.vocals.time = vocals.time;
}
}
}
function _playback_speed_raise(_) playBackSlider.value += .25;
function _playback_speed_reset(_) playBackSlider.value = 1;
function _playback_speed_lower(_) playBackSlider.value -= .25;
function _playback_metronome(t) {
t.icon = (Options.charterMetronomeEnabled = !Options.charterMetronomeEnabled) ? 1 : 0;
}
function _song_muteinst(t) {
FlxG.sound.music.volume = FlxG.sound.music.volume > 0 ? 0 : 1;
t.icon = 1 - Std.int(Math.ceil(FlxG.sound.music.volume));
}
function _song_mutevoices(t) {
vocals.volume = vocals.volume > 0 ? 0 : 1;
for (strumLine in strumLines.members) strumLine.vocals.volume = strumLine.vocals.volume > 0 ? 0 : 1;
t.icon = 1 - Std.int(Math.ceil(vocals.volume));
}
function _playback_back(_) {
if (FlxG.sound.music.playing) return;
Conductor.songPosition -= (Conductor.beatsPerMeasure * __crochet);
}
function _playback_forward(_) {
if (FlxG.sound.music.playing) return;
Conductor.songPosition += (Conductor.beatsPerMeasure * __crochet);
}
function _playback_section_start(_) {
if(FlxG.sound.music.playing) return;
Conductor.songPosition = (Conductor.beatsPerMeasure * (60000 / Conductor.bpm)) * curMeasure;
}
function _playback_back_step(_) {
if (FlxG.sound.music.playing) return;
Conductor.songPosition -= Conductor.stepCrochet;
}
function _playback_forward_step(_) {
if (FlxG.sound.music.playing) return;
Conductor.songPosition += Conductor.stepCrochet;
}
function _song_start(_) {
if (FlxG.sound.music.playing) return;
Conductor.songPosition = 0;
}
function _song_end(_) {
if (FlxG.sound.music.playing) return;
Conductor.songPosition = FlxG.sound.music.length;
}
function _view_zoomin(_) {
zoom += 0.25;
__camZoom = Math.pow(2, zoom);
}
function _view_zoomout(_) {
zoom -= 0.25;
__camZoom = Math.pow(2, zoom);
}
function _view_zoomreset(_) {
zoom = 0;
__camZoom = Math.pow(2, zoom);
}
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;
for (shader in waveformHandler.waveShaders) shader.data.lowDetail.value = [Options.charterLowDetailWaveforms];
}
inline function _snap_increasesnap(_) changequant(1);
inline function _snap_decreasesnap(_) changequant(-1);
inline function _snap_resetsnap(_) setquant(16);
inline function changequant(change:Int) {quant = quants[FlxMath.wrap(quants.indexOf(quant) + change, 0, quants.length-1)]; buildSnapsUI();};
inline function setquant(newquant:Int) {quant = newquant; buildSnapsUI();}
function buildSnapsUI():Array<UIContextMenuOption> {
var snapsTopButton:UITopMenuButton = topMenuSpr == null ? null : cast topMenuSpr.members[snapIndex];
var newChilds:Array<UIContextMenuOption> = [
{
label: "↑ Grid Snap",
keybind: [X],
onSelect: _snap_increasesnap
},
{
label: "Reset Grid Snap",
onSelect: _snap_resetsnap
},
{
label: "↓ Grid Snap",
keybind: [Z],
onSelect: _snap_decreasesnap
},
null
];
for (_quant in quants)
newChilds.push({
label: '${_quant}x Grid Snap',
onSelect: (_) -> {setquant(_quant); buildSnapsUI();},
icon: _quant == quant ? 1 : 0
});
if (snapsTopButton != null) snapsTopButton.contextMenu = newChilds;
return newChilds;
}
inline function _note_addsustain(t)
changeNoteSustain(1);
inline function _note_subtractsustain(t)
changeNoteSustain(-1);
function _note_selectall(_) {
selection = cast notesGroup.members.copy();
}
function _note_selectmeasure(_) {
selection = [for (note in notesGroup.members)
if (note.step > Conductor.curMeasure*Conductor.getMeasureLength() && note.step < (Conductor.curMeasure+1)*Conductor.getMeasureLength()) note
];
}
#end
function changeNoteSustain(change:Float) {
if (selection.length <= 0 || change == 0 || gridActionType != NONE) return;
var undoChanges:Array<NoteSustainChange> = [];
for(s in selection)
if (s is CharterNote) {
var n:CharterNote = cast s;
var old:Float = n.susLength;
n.updatePos(n.step, n.id, Math.max(n.susLength + change, 0), n.type);
undoChanges.push({before: old, after: n.susLength, note: n});
}
undos.addToUndo(CEditSustains(undoChanges));
}
inline public function changeNoteType(?newID:Int, checkSelection:Bool = true) {
if(newID != null) noteType = newID;
noteType = Std.int(FlxMath.bound(noteType, 0, noteTypes.length));
buildNoteTypesUI();
var changedNotes:{notes:Array<CharterNote>, oldTypes:Array<Int>, newTypes:Array<Int>} = {notes:[], oldTypes:[], newTypes:[]};
for(note in notesGroup) if(note.type < 0 || note.type > noteTypes.length) {
changedNotes.notes.push(note); changedNotes.oldTypes.push(note.type); changedNotes.newTypes.push(0);
note.updatePos(note.step, note.id, note.susLength, 0);
}
if(checkSelection) for(s in selection)
if (s is CharterNote) {
var n:CharterNote = cast s;
changedNotes.notes.push(n); changedNotes.oldTypes.push(n.type); changedNotes.newTypes.push(newID);
n.updatePos(n.step, n.id, n.susLength, newID);
}
if(changedNotes.notes.length > 0) undos.addToUndo(CEditSpecNotesType(changedNotes.notes, changedNotes.oldTypes, changedNotes.newTypes));
}
function editNoteTypesList(_)
FlxG.state.openSubState(new CharterNoteTypesList());
function buildNoteTypesUI():Array<UIContextMenuOption> {
var noteTopButton:UITopMenuButton = topMenuSpr == null ? null : cast topMenuSpr.members[noteIndex];
var newChilds:Array<UIContextMenuOption> = [
{
label: "Add sustain length",
keybind: [E],
onSelect: _note_addsustain
},
{
label: "Subtract sustain length",
keybind: [Q],
onSelect: _note_subtractsustain
},
null,
{
label: "Select all",
keybind: [CONTROL, A],
onSelect: _note_selectall
},
{
label: "Select measure",
keybind: [CONTROL, SHIFT, A],
onSelect: _note_selectmeasure
},
null,
{
label: "(0) Default Note",
keybind: [ZERO],
onSelect: (_) -> {changeNoteType(0);},
icon: this.noteType == 0 ? 1 : 0
}
];
var noteKeys:Array<FlxKey> = [ZERO, ONE, TWO, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT, NINE];
for (i=>type in noteTypes) {
var realNoteID:Int = i+1; // Default Note not stored
var newChild:UIContextMenuOption = {
label: '(${realNoteID}) ${type}',
onSelect: (_) -> {changeNoteType(realNoteID);},
icon: this.noteType == realNoteID ? 1 : 0
};
if (realNoteID <= 9) newChild.keybind = [noteKeys[realNoteID]];
newChilds.push(newChild);
}
newChilds.push({
label: "Edit Note Types List",
color: 0xFF959829, icon: 4,
onCreate: function (button:UIContextMenuOptionSpr) {button.label.offset.x = button.icon.offset.x = -2;},
onSelect: editNoteTypesList
});
if (noteTopButton != null) noteTopButton.contextMenu = newChilds;
return newChilds;
}
public function playtestChart(time:Float = 0, opponentMode = false, here = false) {
buildChart();
startHere = here;
startTime = Conductor.songPosition;
PlayState.opponentMode = opponentMode;
PlayState.chartingMode = true;
FlxG.switchState(new PlayState());
}
public inline function buildNote(note:CharterNote):ChartNote {
var time = Conductor.getTimeForStep(note.step);
return {
type: note.type,
time: time,
sLen: Conductor.getTimeForStep(note.step + note.susLength) - time,
id: note.id
};
}
public function buildChart() {
PlayState.SONG.strumLines = [];
PlayState.SONG.noteTypes = this.noteTypes;
for(s in strumLines) {
s.strumLine.notes = [];
PlayState.SONG.strumLines.push(s.strumLine);
}
notesGroup.sortNotes();
for(n in notesGroup.members) {
if (PlayState.SONG.strumLines[n.strumLineID] != null)
PlayState.SONG.strumLines[n.strumLineID].notes.push(buildNote(n));
}
buildEvents();
}
public function buildEvents() {
PlayState.SONG.events = [];
eventsGroup.sortEvents();
for(e in eventsGroup.members) {
for(event in e.events) {
event.time = Conductor.getTimeForStep(e.step);
PlayState.SONG.events.push(event);
}
}
}
public function updateBPMEvents() {
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));
refreshBPMSensitive();
}
public inline function hitsoundsEnabled(id:Int)
return strumLines.members[id] != null && strumLines.members[id].hitsounds;
public inline function __fixSelection(selection:Selection):Selection {
var newSelection:Selection = new Selection();
for (s in selection) if (newSelection.indexOf(s) == -1) newSelection.push(s);
return newSelection.filter((s:ICharterSelectable) -> {return s != null;});
}
// UH OH!!! DANGER ZONE APPOARCHING !!!! LUNARS SHITTY CODE !!!! -lunar
@:noCompletion public function __relinkSingleSelection(selectable:ICharterSelectable):ICharterSelectable {
if (selectable is CharterNote)
return selectable.ID == -1 ? cast(selectable, CharterNote) : notesGroup.members[selectable.ID];
else if (selectable is CharterEvent)
return selectable.ID == -1 ? cast(selectable, CharterEvent) : eventsGroup.members[selectable.ID];
return null;
}
@:noCompletion public function __relinkSelection(selection:Selection) @:privateAccess {
var newSelection:Selection = new Selection();
for (i => selectable in selection)
newSelection[i] = __relinkSingleSelection(selectable);
return newSelection;
}
@:noCompletion public inline function __relinkUndos() {
selection = __relinkSelection(selection);
for (list => changeList in [undos.undoList, undos.redoList]) {
var newChanges:Array<CharterChange> = [];
for (i => change in changeList) {
switch (change) {
case CCreateSelection(selection):
newChanges[i] = CCreateSelection(__relinkSelection(selection));
case CDeleteSelection(selection):
newChanges[i] = CDeleteSelection(__relinkSelection(selection));
case CSelectionDrag(selectionDrags):
newChanges[i] = CSelectionDrag([
for (selectionDrag in selectionDrags)
{
selectable: __relinkSingleSelection(selectionDrag.selectable),
change: selectionDrag.change
}
]);
case CEditSustains(noteChanges):
newChanges[i] = CEditSustains([
for (noteChange in noteChanges)
{
note: cast(__relinkSingleSelection(noteChange.note), CharterNote),
before: noteChange.before,
after: noteChange.after
}
]);
case CEditEvent(event, oldEvents, newEvents):
newChanges[i] = CEditEvent(cast(__relinkSingleSelection(event), CharterEvent), oldEvents, newEvents);
case CEditSpecNotesType(notesChanged, oldNoteTypes, newNoteTypes):
newChanges[i] = CEditSpecNotesType([
for (noteChanged in notesChanged)
cast(__relinkSingleSelection(noteChanged), CharterNote)
], oldNoteTypes, newNoteTypes);
default: newChanges[i] = change;
}
}
if (list == 0) undos.undoList = newChanges;
else undos.redoList = newChanges;
}
}
@:noCompletion public function __resetStatics() {
selection = new Selection();
undos = new UndoList<CharterChange>();
clipboard = []; playtestInfo = null;
waveformHandler = new CharterWaveformHandler();
autoSaveTimer = Options.charterAutoSaveTime;
}
@:noCompletion public function __clearStatics() {
selection = null; undos = null; clipboard = null; playtestInfo = null;
waveformHandler.destroy(); Charter.waveformHandler = null; autoSaveTimer = 0;
}
@:noCompletion public function __updatePlaytestInfo() {
playtestInfo = {
songPosition: Conductor.songPosition,
playbackSpeed: playBackSlider.value,
quantSelected: quant,
noteTypeSelected: noteType,
strumlinesDraggable: strumLines.draggable,
hitSounds: [for (strumLine in strumLines.members) strumLine.hitsounds],
mutedVocals: [for (strumLine in strumLines.members) !(strumLine.vocals.volume > 0)],
waveforms: [for (strumLine in strumLines.members) strumLine.selectedWaveform]
}
}
@:noCompletion public function __applyPlaytestInfo() {
if (playtestInfo == null) return;
Conductor.songPosition = playtestInfo.songPosition;
playBackSlider.value = playtestInfo.playbackSpeed;
quant = playtestInfo.quantSelected;
noteType = playtestInfo.noteTypeSelected;
strumLines.draggable = playtestInfo.strumlinesDraggable;
for (i => strumLine in strumLines.members)
strumLine.hitsounds = playtestInfo.hitSounds[i];
for (i => strumLine in strumLines.members)
strumLine.vocals.volume = playtestInfo.mutedVocals[i] ? 0 : 1;
for (i => strumLine in strumLines.members)
strumLine.selectedWaveform = playtestInfo.waveforms[i];
}
}
enum CharterChange {
CCreateStrumLine(strumLineID:Int, strumLine:ChartStrumLine);
CEditStrumLine(strumLineID:Int, oldStrumLine:ChartStrumLine, newStrumLine:ChartStrumLine);
COrderStrumLine(strumLineID:Int, oldID:Int, newID:Int);
CDeleteStrumLine(strumLineID:Int, strumLine:ChartStrumLine);
CCreateSelection(selection:Selection);
CDeleteSelection(selection:Selection);
CSelectionDrag(selectionDrags:Array<SelectionDragChange>);
CEditSustains(notes:Array<NoteSustainChange>);
CEditEvent(event:CharterEvent, oldEvents:Array<ChartEvent>, newEvents:Array<ChartEvent>);
CEditChartData(oldData:{stage:String, speed:Float}, newData:{stage:String, speed:Float});
CEditNoteTypes(oldArray:Array<String>, newArray:Array<String>);
CEditSpecNotesType(notes:Array<CharterNote>, oldNoteTypes:Array<Int>, newNoteTypes:Array<Int>);
}
enum CharterCopyboardObject {
CNote(step:Float, id:Int, strumLineID:Int, susLength:Float, type:Int);
CEvent(step:Float, events:Array<ChartEvent>);
}
typedef NoteSustainChange = {
var note:CharterNote;
var before:Float;
var after:Float;
}
typedef SelectionDragChange = {
var selectable:ICharterSelectable;
var change:FlxPoint;
}
@:forward abstract Selection(Array<ICharterSelectable>) from Array<ICharterSelectable> to Array<ICharterSelectable> {
public inline function new(?array:Array<ICharterSelectable>)
this = array == null ? [] : array;
// too lazy to put this in every for loop so i made it a abstract
public inline function loop(onNote:CharterNote->Void, ?onEvent:CharterEvent->Void, ?draggableOnly:Bool = true) {
for (s in this) {
if (s is CharterNote && onNote != null && (draggableOnly ? s.draggable: true))
onNote(cast(s, CharterNote));
else if (s is CharterEvent && onEvent != null && (draggableOnly ? s.draggable: true))
onEvent(cast(s, CharterEvent));
}
}
}
interface ICharterSelectable {
public var x(default, set):Float;
public var y(default, set):Float;
public var ID:Int;
public var step:Float;
public var selected:Bool;
public var hovered:Bool;
public var draggable:Bool;
public function handleSelection(selectionBox:UISliceSprite):Bool;
public function handleDrag(change:FlxPoint):Void;
}
enum abstract CharterGridActionType(Int) {
var NONE = 0;
var BOX_SELECTION = 1;
var NOTE_DRAG = 2;
var INVALID_DRAG = 3;
var SUSTAIN_DRAG = 4;
var DELETE_SELECTION = 5;
}
typedef PlaytestInfo = {
var songPosition:Float;
var playbackSpeed:Float;
var quantSelected:Int;
var noteTypeSelected:Int;
var strumlinesDraggable:Bool;
var hitSounds:Array<Bool>;
var mutedVocals:Array<Bool>;
var waveforms:Array<Int>;
}