mirror of
https://github.com/ninjamuffin99/Funkin.git
synced 2025-02-04 10:47:57 +00:00
Work on re-enabling cutscenes
This commit is contained in:
parent
b126ac28f4
commit
57caeef802
|
@ -129,7 +129,7 @@
|
||||||
<haxelib name="haxeui-flixel"/> <!-- Integrate HaxeUI with Flixel -->
|
<haxelib name="haxeui-flixel"/> <!-- Integrate HaxeUI with Flixel -->
|
||||||
<haxelib name="polymod" /> <!-- Modding framework -->
|
<haxelib name="polymod" /> <!-- Modding framework -->
|
||||||
<haxelib name="flxanimate" /> <!-- Texture atlas rendering -->
|
<haxelib name="flxanimate" /> <!-- Texture atlas rendering -->
|
||||||
<!-- <haxelib name="hxcodec" /> Video playback -->
|
<haxelib name="hxCodec" /> <!-- Video playback -->
|
||||||
<haxelib name="json2object" /> <!-- JSON parsing -->
|
<haxelib name="json2object" /> <!-- JSON parsing -->
|
||||||
|
|
||||||
<haxelib name="thx.semver" />
|
<haxelib name="thx.semver" />
|
||||||
|
|
2
hmm.json
2
hmm.json
|
@ -68,7 +68,7 @@
|
||||||
"name": "hxcodec",
|
"name": "hxcodec",
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"dir": null,
|
"dir": null,
|
||||||
"ref": "91adeec",
|
"ref": "14ef69f",
|
||||||
"url": "https://github.com/polybiusproxy/hxCodec"
|
"url": "https://github.com/polybiusproxy/hxCodec"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,80 +0,0 @@
|
||||||
package funkin;
|
|
||||||
|
|
||||||
import flixel.FlxSprite;
|
|
||||||
import flixel.FlxState;
|
|
||||||
import flixel.addons.display.FlxGridOverlay;
|
|
||||||
import flixel.group.FlxGroup.FlxTypedGroup;
|
|
||||||
import flixel.math.FlxPoint;
|
|
||||||
import flixel.text.FlxText;
|
|
||||||
import flixel.util.FlxColor;
|
|
||||||
import openfl.Assets;
|
|
||||||
import openfl.display.BitmapData;
|
|
||||||
import openfl.display.MovieClip;
|
|
||||||
import openfl.display.Timeline;
|
|
||||||
import openfl.geom.Matrix;
|
|
||||||
import openfl.geom.Rectangle;
|
|
||||||
|
|
||||||
class CutsceneAnimTestState extends FlxState
|
|
||||||
{
|
|
||||||
var cutsceneGroup:CutsceneCharacter;
|
|
||||||
|
|
||||||
var curSelected:Int = 0;
|
|
||||||
var debugTxt:FlxText;
|
|
||||||
|
|
||||||
var funnySprite:FlxSprite = new FlxSprite();
|
|
||||||
var clip:MovieClip;
|
|
||||||
|
|
||||||
public function new()
|
|
||||||
{
|
|
||||||
super();
|
|
||||||
|
|
||||||
var gridBG:FlxSprite = FlxGridOverlay.create(10, 10);
|
|
||||||
gridBG.scrollFactor.set(0.5, 0.5);
|
|
||||||
add(gridBG);
|
|
||||||
|
|
||||||
debugTxt = new FlxText(900, 20, 0, "", 20);
|
|
||||||
debugTxt.color = FlxColor.BLUE;
|
|
||||||
add(debugTxt);
|
|
||||||
|
|
||||||
clip = Assets.getMovieClip("tanky:");
|
|
||||||
// clip.x = FlxG.width/2;
|
|
||||||
// clip.y = FlxG.height/2;
|
|
||||||
FlxG.stage.addChild(clip);
|
|
||||||
|
|
||||||
var swagShit:MovieClip = Assets.getMovieClip('tankBG:');
|
|
||||||
// swagShit.scaleX = 5;
|
|
||||||
|
|
||||||
FlxG.stage.addChild(swagShit);
|
|
||||||
swagShit.gotoAndStop(13);
|
|
||||||
|
|
||||||
var swfMountain = new BitmapData(FlxG.width, FlxG.height, true, 0x00000000);
|
|
||||||
swfMountain.draw(swagShit, swagShit.transform.matrix);
|
|
||||||
|
|
||||||
var mountains:FlxSprite = new FlxSprite().loadGraphic(swfMountain);
|
|
||||||
// add(mountains);
|
|
||||||
|
|
||||||
FlxG.stage.removeChild(swagShit);
|
|
||||||
|
|
||||||
funnySprite.x = FlxG.width / 2;
|
|
||||||
funnySprite.y = FlxG.height / 2;
|
|
||||||
add(funnySprite);
|
|
||||||
}
|
|
||||||
|
|
||||||
override function update(elapsed:Float)
|
|
||||||
{
|
|
||||||
super.update(elapsed);
|
|
||||||
|
|
||||||
// jam sprite into top left corner
|
|
||||||
var drawMatrix:Matrix = clip.transform.matrix;
|
|
||||||
var bounds:Rectangle = clip.getBounds(null);
|
|
||||||
drawMatrix.tx = -bounds.x;
|
|
||||||
drawMatrix.ty = -bounds.y;
|
|
||||||
// make bitmapdata only as big as it needs to be
|
|
||||||
var funnyBmp:BitmapData = new BitmapData(Math.ceil(bounds.width), Math.ceil(bounds.height), true, 0x00000000);
|
|
||||||
funnyBmp.draw(clip, drawMatrix, true);
|
|
||||||
funnySprite.loadGraphic(funnyBmp);
|
|
||||||
// jam sprite back into place lol
|
|
||||||
funnySprite.offset.x = -bounds.x;
|
|
||||||
funnySprite.offset.y = -bounds.y;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,75 +0,0 @@
|
||||||
package funkin;
|
|
||||||
|
|
||||||
import flixel.FlxSprite;
|
|
||||||
import flixel.group.FlxGroup.FlxTypedGroup;
|
|
||||||
import flixel.math.FlxPoint;
|
|
||||||
|
|
||||||
class CutsceneCharacter extends FlxTypedGroup<FlxSprite>
|
|
||||||
{
|
|
||||||
public var coolPos:FlxPoint = FlxPoint.get();
|
|
||||||
public var animShit:Map<String, FlxPoint> = new Map();
|
|
||||||
|
|
||||||
var imageShit:String;
|
|
||||||
|
|
||||||
public function new(x:Float, y:Float, imageShit:String)
|
|
||||||
{
|
|
||||||
super();
|
|
||||||
|
|
||||||
coolPos.set(x, y);
|
|
||||||
|
|
||||||
this.imageShit = imageShit;
|
|
||||||
parseOffsets();
|
|
||||||
createCutscene(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// shitshow, oh well
|
|
||||||
var arrayLMFAOOOO:Array<String> = [];
|
|
||||||
|
|
||||||
function parseOffsets()
|
|
||||||
{
|
|
||||||
var splitShit:Array<String> = CoolUtil.coolTextFile(Paths.file('images/cutsceneStuff/' + imageShit + "CutsceneOffsets.txt"));
|
|
||||||
|
|
||||||
for (i in splitShit)
|
|
||||||
{
|
|
||||||
var xAndY:FlxPoint = FlxPoint.get();
|
|
||||||
var dumbSplit:Array<String> = i.split('---')[1].trim().split(' ');
|
|
||||||
trace('cool split: ' + i.split('---')[1]);
|
|
||||||
trace(dumbSplit);
|
|
||||||
xAndY.set(Std.parseFloat(dumbSplit[0]), Std.parseFloat(dumbSplit[1]));
|
|
||||||
|
|
||||||
animShit.set(i.split('---')[0].trim(), xAndY);
|
|
||||||
arrayLMFAOOOO.push(i.split('---')[0].trim());
|
|
||||||
}
|
|
||||||
|
|
||||||
trace(animShit);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function createCutscene(daNum:Int = 0)
|
|
||||||
{
|
|
||||||
var cutScene:FlxSprite = new FlxSprite(coolPos.x + animShit.get(arrayLMFAOOOO[daNum]).x, coolPos.y + animShit.get(arrayLMFAOOOO[daNum]).y);
|
|
||||||
cutScene.frames = Paths.getSparrowAtlas('cutsceneStuff/' + imageShit + "-" + daNum);
|
|
||||||
cutScene.animation.addByPrefix('weed', arrayLMFAOOOO[daNum], 24, false);
|
|
||||||
cutScene.animation.play('weed');
|
|
||||||
cutScene.antialiasing = true;
|
|
||||||
|
|
||||||
cutScene.animation.finishCallback = function(anim:String)
|
|
||||||
{
|
|
||||||
cutScene.kill();
|
|
||||||
cutScene.destroy();
|
|
||||||
cutScene = null;
|
|
||||||
|
|
||||||
if (daNum + 1 < arrayLMFAOOOO.length) createCutscene(daNum + 1);
|
|
||||||
else
|
|
||||||
ended();
|
|
||||||
};
|
|
||||||
|
|
||||||
add(cutScene);
|
|
||||||
}
|
|
||||||
|
|
||||||
public var onFinish:Void->Void;
|
|
||||||
|
|
||||||
public function ended():Void
|
|
||||||
{
|
|
||||||
if (onFinish != null) onFinish();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,13 +1,10 @@
|
||||||
package funkin.play.cutscene;
|
package funkin.play.cutscene;
|
||||||
|
|
||||||
// import hxcodec.flixel.FlxVideoSprite;
|
|
||||||
// import hxcodec.flixel.FlxCutsceneState;
|
|
||||||
import flixel.FlxSprite;
|
import flixel.FlxSprite;
|
||||||
import flixel.tweens.FlxEase;
|
import flixel.tweens.FlxEase;
|
||||||
import flixel.tweens.FlxTween;
|
import flixel.tweens.FlxTween;
|
||||||
import flixel.util.FlxColor;
|
import flixel.util.FlxColor;
|
||||||
import flixel.util.FlxTimer;
|
import flixel.util.FlxTimer;
|
||||||
import funkin.graphics.video.FlxVideo;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Static methods for playing cutscenes in the PlayState.
|
* Static methods for playing cutscenes in the PlayState.
|
||||||
|
@ -15,112 +12,46 @@ import funkin.graphics.video.FlxVideo;
|
||||||
*/
|
*/
|
||||||
class VanillaCutscenes
|
class VanillaCutscenes
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* Well, well, well, what have we got here?
|
|
||||||
*/
|
|
||||||
public static function playUghCutscene():Void
|
|
||||||
{
|
|
||||||
playVideoCutscene('music/ughCutscene.mp4');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Nice bars for an ugly, boring teenager!
|
|
||||||
*/
|
|
||||||
public static function playGunsCutscene():Void
|
|
||||||
{
|
|
||||||
playVideoCutscene('music/gunsCutscene.mp4');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Don't you have a school to shoot up?
|
|
||||||
*/
|
|
||||||
public static function playStressCutscene():Void
|
|
||||||
{
|
|
||||||
playVideoCutscene('music/stressCutscene.mp4');
|
|
||||||
}
|
|
||||||
|
|
||||||
static var blackScreen:FlxSprite;
|
static var blackScreen:FlxSprite;
|
||||||
|
|
||||||
/**
|
static final TWEEN_DURATION:Float = 2.0;
|
||||||
* Plays a cutscene from a video file, then starts the countdown once the video is done.
|
|
||||||
* TODO: Cutscene is currently skipped on native platforms.
|
|
||||||
*/
|
|
||||||
static function playVideoCutscene(path:String):Void
|
|
||||||
{
|
|
||||||
// Tell PlayState to stop the song until the video is done.
|
|
||||||
PlayState.isInCutscene = true;
|
|
||||||
PlayState.instance.camHUD.visible = false;
|
|
||||||
|
|
||||||
// Display a black screen to hide the game while the video is playing.
|
|
||||||
blackScreen = new FlxSprite(-200, -200).makeGraphic(FlxG.width * 2, FlxG.height * 2, FlxColor.BLACK);
|
|
||||||
blackScreen.scrollFactor.set(0, 0);
|
|
||||||
blackScreen.cameras = [PlayState.instance.camCutscene];
|
|
||||||
PlayState.instance.add(blackScreen);
|
|
||||||
|
|
||||||
#if html5
|
|
||||||
// Video displays OVER the FlxState.
|
|
||||||
vid = new FlxVideo(path);
|
|
||||||
vid.finishCallback = finishCutscene.bind(0.5);
|
|
||||||
#else
|
|
||||||
// Video displays OVER the FlxState.
|
|
||||||
// vid = new FlxVideoSprite(0, 0);
|
|
||||||
|
|
||||||
vid.cameras = [PlayState.instance.camCutscene];
|
|
||||||
|
|
||||||
PlayState.instance.add(vid);
|
|
||||||
|
|
||||||
vid.playVideo(Paths.file(path), false);
|
|
||||||
vid.onEndReached.add(finishCutscene.bind(0.5));
|
|
||||||
#end
|
|
||||||
}
|
|
||||||
|
|
||||||
static var vid:#if html5 FlxVideo #else Dynamic /**FlxVideoSprite **/ #end;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Does the cleanup to start the countdown after the video is done.
|
* Plays the cutscene that appears at the start of Winter Horrorland.
|
||||||
* Gets called immediately if the video can't be played.
|
* TODO: Move this to `winter-horrorland.hxc`
|
||||||
*/
|
|
||||||
public static function finishCutscene(?transitionTime:Float = 2.5):Void
|
|
||||||
{
|
|
||||||
trace('ALERT: Finish cutscene called!');
|
|
||||||
|
|
||||||
#if html5
|
|
||||||
#else
|
|
||||||
vid.stop();
|
|
||||||
PlayState.instance.remove(vid);
|
|
||||||
#end
|
|
||||||
|
|
||||||
PlayState.instance.camHUD.visible = true;
|
|
||||||
|
|
||||||
FlxTween.tween(blackScreen, {alpha: 0}, transitionTime,
|
|
||||||
{
|
|
||||||
ease: FlxEase.quadInOut,
|
|
||||||
onComplete: function(twn:FlxTween) {
|
|
||||||
PlayState.instance.remove(blackScreen);
|
|
||||||
blackScreen = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
FlxTween.tween(FlxG.camera, {zoom: PlayState.defaultCameraZoom}, transitionTime,
|
|
||||||
{
|
|
||||||
ease: FlxEase.quadInOut,
|
|
||||||
onComplete: function(twn:FlxTween) {
|
|
||||||
PlayState.instance.startCountdown();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* FNF corruption mod???
|
|
||||||
*/
|
*/
|
||||||
public static function playHorrorStartCutscene():Void
|
public static function playHorrorStartCutscene():Void
|
||||||
{
|
{
|
||||||
PlayState.isInCutscene = true;
|
PlayState.instance.isInCutscene = true;
|
||||||
PlayState.instance.camHUD.visible = false;
|
PlayState.instance.camHUD.visible = false;
|
||||||
|
|
||||||
blackScreen = new FlxSprite(-200, -200).makeGraphic(FlxG.width * 2, FlxG.height * 2, FlxColor.BLACK);
|
blackScreen = new FlxSprite(-200, -200).makeGraphic(FlxG.width * 2, FlxG.height * 2, FlxColor.BLACK);
|
||||||
blackScreen.scrollFactor.set(0, 0);
|
blackScreen.scrollFactor.set(0, 0);
|
||||||
|
blackScreen.zIndex = 1000000;
|
||||||
PlayState.instance.add(blackScreen);
|
PlayState.instance.add(blackScreen);
|
||||||
|
|
||||||
new FlxTimer().start(0.1, _ -> finishCutscene(2.5));
|
new FlxTimer().start(0.1, function(_) {
|
||||||
|
trace('Playing horrorland cutscene...');
|
||||||
|
PlayState.instance.remove(blackScreen);
|
||||||
|
|
||||||
|
// Force set the camera position and zoom.
|
||||||
|
PlayState.instance.cameraFollowPoint.setPosition(400, -2050);
|
||||||
|
PlayState.instance.resetCamera();
|
||||||
|
FlxG.camera.zoom = 2.5;
|
||||||
|
|
||||||
|
// Play the Sound effect.
|
||||||
|
FlxG.sound.play(Paths.sound('Lights_Turn_On'), function() {
|
||||||
|
// Fade in the HUD.
|
||||||
|
trace('SFX done...');
|
||||||
|
PlayState.instance.camHUD.visible = true;
|
||||||
|
PlayState.instance.camHUD.alpha = 0.0; // Use alpha rather than visible to let us fade it in.
|
||||||
|
FlxTween.tween(PlayState.instance.camHUD, {alpha: 1.0}, TWEEN_DURATION, {ease: FlxEase.quadInOut});
|
||||||
|
|
||||||
|
// Start the countdown.
|
||||||
|
trace('Zoom out done...');
|
||||||
|
trace('Begin countdown (ends cutscene)');
|
||||||
|
PlayState.instance.startCountdown();
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
155
source/funkin/play/cutscene/VideoCutscene.hx
Normal file
155
source/funkin/play/cutscene/VideoCutscene.hx
Normal file
|
@ -0,0 +1,155 @@
|
||||||
|
package funkin.play.cutscene;
|
||||||
|
|
||||||
|
import funkin.play.PlayState;
|
||||||
|
import flixel.FlxSprite;
|
||||||
|
import flixel.tweens.FlxEase;
|
||||||
|
import flixel.tweens.FlxTween;
|
||||||
|
import flixel.util.FlxColor;
|
||||||
|
import flixel.util.FlxTimer;
|
||||||
|
#if html5
|
||||||
|
import funkin.graphics.video.FlxVideo;
|
||||||
|
#else
|
||||||
|
import hxcodec.flixel.FlxVideoSprite;
|
||||||
|
#end
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assumes you are in the PlayState.
|
||||||
|
*/
|
||||||
|
class VideoCutscene
|
||||||
|
{
|
||||||
|
static var blackScreen:FlxSprite;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Play a video cutscene.
|
||||||
|
* TODO: Currently this is hardcoded to start the countdown after the video is done.
|
||||||
|
* @param path The path to the video file. Use Paths.file(path) to get the correct path.
|
||||||
|
*/
|
||||||
|
public static function play(filePath:String):Void
|
||||||
|
{
|
||||||
|
if (PlayState.instance == null) return;
|
||||||
|
|
||||||
|
if (!openfl.Assets.exists(filePath))
|
||||||
|
{
|
||||||
|
trace('ERROR: Video file does not exist: ${filePath}');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger the cutscene. Don't play the song in the background.
|
||||||
|
PlayState.instance.isInCutscene = true;
|
||||||
|
PlayState.instance.camHUD.visible = false;
|
||||||
|
PlayState.instance.camCutscene.visible = true;
|
||||||
|
|
||||||
|
// Display a black screen to hide the game while the video is playing.
|
||||||
|
blackScreen = new FlxSprite(-200, -200).makeGraphic(FlxG.width * 2, FlxG.height * 2, FlxColor.BLACK);
|
||||||
|
blackScreen.scrollFactor.set(0, 0);
|
||||||
|
blackScreen.cameras = [PlayState.instance.camCutscene];
|
||||||
|
PlayState.instance.add(blackScreen);
|
||||||
|
|
||||||
|
#if html5
|
||||||
|
playVideoHTML5(filePath);
|
||||||
|
#else
|
||||||
|
playVideoNative(filePath);
|
||||||
|
#end
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function isPlaying():Bool
|
||||||
|
{
|
||||||
|
return vid != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if html5
|
||||||
|
static var vid:FlxVideo;
|
||||||
|
|
||||||
|
static function playVideoHTML5(filePath:String):Void
|
||||||
|
{
|
||||||
|
// Video displays OVER the FlxState.
|
||||||
|
vid = new FlxVideo(filePath);
|
||||||
|
if (vid != null)
|
||||||
|
{
|
||||||
|
vid.zIndex = 0;
|
||||||
|
|
||||||
|
vid.finishCallback = finishVideo.bind(0.5);
|
||||||
|
|
||||||
|
vid.cameras = [PlayState.instance.camCutscene];
|
||||||
|
|
||||||
|
PlayState.instance.add(vid);
|
||||||
|
|
||||||
|
PlayState.instance.refresh();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
trace('ALERT: Video is null! Could not play cutscene!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
static var vid:FlxVideoSprite;
|
||||||
|
|
||||||
|
static function playVideoNative(filePath:String):Void
|
||||||
|
{
|
||||||
|
// Video displays OVER the FlxState.
|
||||||
|
vid = new FlxVideoSprite(0, 0);
|
||||||
|
|
||||||
|
if (vid != null)
|
||||||
|
{
|
||||||
|
vid.zIndex = 0;
|
||||||
|
vid.onEndReached.add(finishVideo.bind(0.5));
|
||||||
|
|
||||||
|
vid.cameras = [PlayState.instance.camCutscene];
|
||||||
|
|
||||||
|
PlayState.instance.add(vid);
|
||||||
|
|
||||||
|
PlayState.instance.refresh();
|
||||||
|
vid.playVideo(filePath, false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
trace('ALERT: Video is null! Could not play cutscene!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#end
|
||||||
|
|
||||||
|
public static function finishVideo(?transitionTime:Float = 0.5):Void
|
||||||
|
{
|
||||||
|
trace('ALERT: Finish video cutscene called!');
|
||||||
|
|
||||||
|
#if html5
|
||||||
|
if (vid != null)
|
||||||
|
{
|
||||||
|
PlayState.instance.remove(vid);
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
if (vid != null)
|
||||||
|
{
|
||||||
|
vid.stop();
|
||||||
|
PlayState.instance.remove(vid);
|
||||||
|
}
|
||||||
|
#end
|
||||||
|
vid.destroy();
|
||||||
|
vid = null;
|
||||||
|
|
||||||
|
PlayState.instance.camCutscene.visible = true;
|
||||||
|
PlayState.instance.camHUD.visible = true;
|
||||||
|
|
||||||
|
FlxTween.tween(blackScreen, {alpha: 0}, transitionTime,
|
||||||
|
{
|
||||||
|
ease: FlxEase.quadInOut,
|
||||||
|
onComplete: function(twn:FlxTween) {
|
||||||
|
PlayState.instance.remove(blackScreen);
|
||||||
|
blackScreen = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
FlxTween.tween(FlxG.camera, {zoom: PlayState.instance.defaultCameraZoom}, transitionTime,
|
||||||
|
{
|
||||||
|
ease: FlxEase.quadInOut,
|
||||||
|
onComplete: function(twn:FlxTween) {
|
||||||
|
PlayState.instance.startCountdown();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
trace('Video playback failed (${e})');
|
||||||
|
vid = null;
|
||||||
|
finishCutscene(0.5);
|
||||||
|
*/
|
Loading…
Reference in a new issue