Files
CodenameEngine/source/funkin/editors/charter/CharterEvent.hx
T
Ralty e4be3bd427 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
2025-07-16 19:27:05 +07:00

487 lines
14 KiB
Haxe

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>;
public var step:Float;
public var icons:Array<FlxSprite> = [];
public var selected:Bool = false;
public var draggable:Bool = true;
public var eventsBackdrop:EventBackdrop;
public var snappedToGrid:Bool = true;
public function new(step:Float, ?events:Array<ChartEvent>) {
super(-100, (step * 40) - 17, 100, 34, 'editors/charter/event-spr');
this.step = step;
this.events = events.getDefault([]);
cursor = BUTTON;
}
public override function update(elapsed:Float) {
super.update(elapsed);
if (snappedToGrid && eventsBackdrop != null)
x = eventsBackdrop.x + eventsBackdrop.width - (bWidth = 37 + (icons.length * 22));
for(k=>i in icons) {
i.follow(this, (k * 22) + 30 - (i.width / 2), (bHeight - i.height) / 2);
}
colorTransform.redMultiplier = colorTransform.greenMultiplier = colorTransform.blueMultiplier = selected ? 0.75 : 1;
colorTransform.redOffset = colorTransform.greenOffset = selected ? 96 : 0;
colorTransform.blueOffset = selected ? 168 : 0;
for (sprite in icons)
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;
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();
var graphic:FlxGraphicAsset = try {
isBase64 ? openfl.display.BitmapData.fromBase64(path, 'UTF8') : path;
} catch(e:Dynamic) {
Logs.trace('Failed to load event icon: ${e.toString()}', ERROR);
isBase64 = false;
defaultPath;
}
if(!isBase64) {
if (!Assets.exists(graphic))
graphic = defaultPath;
}
return new FlxSprite(graphic);
}
/**
* 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() {
super.onHovered();
/*
if (FlxG.mouse.justReleased)
FlxG.state.openSubState(new CharterEventScreen(this));
*/
}
public function handleSelection(selectionBox:UISliceSprite):Bool {
return (selectionBox.x + selectionBox.bWidth > x) && (selectionBox.x < x + bWidth) && (selectionBox.y + selectionBox.bHeight > y) && (selectionBox.y < y + bHeight);
}
public function handleDrag(change:FlxPoint) {
var newStep:Float = step = FlxMath.bound(step + change.x, 0, Charter.instance.__endStep-1);
y = ((newStep) * 40) - 17;
}
public function refreshEventIcons() {
while(icons.length > 0) {
var i = icons.shift();
members.remove(i);
i.destroy();
}
for(event in events) {
var spr = generateEventIcon(event, false);
icons.push(spr);
members.push(spr);
}
draggable = true;
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();
}
}