"use strict";

/*
Sky Dragon Story
The tale of a light-hearted mergirl and her search for
truth, justice, identity, and divinty.
Trigger warnings: depression, suicide.
*/
const SDS_VERSION = "b.2023.3.4"; // unless I forget to update this field

var DEFINE_MAIN_STATES = {
	LOADING : 0,
	TITLE : 1,
	PLAY : 2,
	ENDING : 3
}

var SDS_fullscreen=false;
function SDS_ToggleFullscreen(){
	if (SDS_popout != null) SDS_popout.close();

	SDS_fullscreen = !SDS_fullscreen;
	//var gamediv = document.getElementById("game");
	//var gamecanv = document.getElementById("game_canvas");
	var gamecanv = SDS.ctx.canvas;
	var gamediv = gamecanv.parentElement;
	if (SDS_fullscreen){
		var gameaspect = 1.0*gamecanv.width/gamecanv.height;
		var screenaspect = 1.0*screen.width/screen.height;
		var fullwidth = screen.width;
		var fullheight = screen.height;
		if (gameaspect>screenaspect){
			fullheight = fullwidth / gameaspect;
		}
		else{
			fullwidth = fullheight * gameaspect;
		}
		gamecanv.style.width=""+fullwidth+"px";
		gamecanv.style.height=""+fullheight+"px";
		gamediv.style.width=""+fullwidth+"px";
		gamediv.style.height=""+fullheight+"px";
	}
	else{
		gamecanv.style.width=""+gamecanv.width+"px";
		gamecanv.style.height=""+gamecanv.height+"px";
		gamediv.style.width=""+gamecanv.width+"px";
		gamediv.style.height=""+gamecanv.height+"px";
	}
	SDS_margins();
}
function SDS_margins(){
	if (SDS_popout!=null) return;
	if (!SDS_fullscreen/* && window.width%2*/){
		_HEAP.margin = (window.innerWidth-SDS.divGame.clientWidth)/2.0;
		SDS.divGame.style.marginLeft = Math.ceil(_HEAP.margin)+"px";
		SDS.divGame.style.marginRight = Math.floor(_HEAP.margin)+"px";
	}
	else{
		SDS.divGame.style.marginLeft = "";
		SDS.divGame.style.marginRight = "";
	}
};

var SDS_popout = null;
function SDS_OpenPopOut(){
	if (SDS_popout!=null) return;
	if (!SDS.ctx || !SDS.divGame) return;
	if (SDS_fullscreen) SDS_ToggleFullscreen();

	// scrollbars and resizable seem to be nonstandard.
	// width and height seem to be imprecise.
	// use CSS and JS later to compensate.
	// URL bar can't be hidden due to security concerns
	SDS_popout = window.open(
		"",
		"Game Window",
		"popup=1,width="+SDS.ctx.canvas.width+",height="+SDS.ctx.canvas.height
	);
	if (!SDS_popout) return; // popup denied

	SDS.ctx.canvas.parentElement.removeChild(SDS.ctx.canvas);
	SDS.divGame.style.display = "none";

	var canvas = SDS_GenerateCanvas();

	var html = '<!DOCTYPE HTML><html><head><title>Sky Dragon Story</title><link rel="stylesheet" type="text/css" href="SDS.css"></link></head><body class="popout"></body></html>';

	SDS_popout.document.open();
	SDS_popout.document.write(html);
	SDS_popout.document.close();
	SDS_popout.document.body.style.margin="0px";
	SDS_popout.document.body.appendChild(canvas);
	
	//SDS_popout.resizeTo(canvas.width, canvas.height + (SDS_popout.outerHeight - SDS_popout.innerHeight));
	// and OF COURSE window parameters aren't fully defined
	// for an entire frame or so.
	SDS_popout.requestAnimationFrame(()=>{
		SDS_popout.resizeTo(canvas.width + (SDS_popout.outerWidth - SDS_popout.innerWidth), canvas.height + (SDS_popout.outerHeight - SDS_popout.innerHeight));
		//SDS_ResizePopOut(canvas.width,canvas.height,canvas.width + (SDS_popout.outerWidth - SDS_popout.innerWidth), canvas.height + (SDS_popout.outerHeight - SDS_popout.innerHeight));
		SDS_popout.addEventListener('resize',()=>{
			SDS_AspectPopOut(true);
		});
	});

	// oh yeah... better accept keystrokes here.
	SDS_popout.document.addEventListener('keydown', function(event){GLOBAL_CONTROLS.onKeyDown(event,GLOBAL_CONTROLS);});
	SDS_popout.document.addEventListener('keyup', function(event){GLOBAL_CONTROLS.onKeyUp(event,GLOBAL_CONTROLS);});
	
	SDS_popout.addEventListener('beforeunload',SDS_ClosePopOut);

	if (window.cancelAnimationFrame) window.cancelAnimationFrame(SDS_lastframe);
	if (SDS_popout.requestAnimationFrame) SDS_popout.requestAnimationFrame(SDS_AnimationFrame);

	var soundnote = document.getElementById("soundnote");
	var popoutnote = document.getElementById("popoutnote");
	if (soundnote) soundnote.style.display="none";
	if (popoutnote) popoutnote.style.display="none";
};
function SDS_ClosePopOut(){
	if (!SDS_popout) return;

	var canvas = SDS_GenerateCanvas();
	SDS.divGame.appendChild(canvas);
	SDS.divGame.style.display = "block";

	if (SDS_popout.cancelAnimationFrame) SDS_popout.cancelAnimationFrame(SDS_lastframe);
	if (window.requestAnimationFrame) window.requestAnimationFrame(SDS_AnimationFrame);
	SDS_popout = null;
	//SDS_last_popsize = null;

	var soundnote = document.getElementById("soundnote");
	var popoutnote = document.getElementById("popoutnote");
	if (soundnote) soundnote.style.display="block";
	if (popoutnote) popoutnote.style.display="block";

	SDS_margins();
};
//var SDS_last_popsize = null;
//function SDS_ResizePopOut(iw,ih,ow,oh){
//	// It appears possible for the outer-inner differential to be inconsistent
//	// leading to cycles of repeated finalization-resize requests
//	// for fundamentally the same intended inner size.
//	if (SDS_popout==null) return;
//	if (SDS_last_popsize!=null && SDS_last_popsize.iw==iw && SDS_last_popsize.ih==ih) return;
//	SDS_last_popsize = {
//		iw:iw,
//		ih:ih,
//		ow:ow,
//		oh:oh,
//	};
//	SDS_popout.resizeTo(ow, oh);
//};

var SDS_aspect_finalize = null;
function SDS_AspectPopOut(snap){
	if (SDS_popout == null) return;
	var gameAspect = SDS.gameRender.width / SDS.gameRender.height;
	var winAspect = SDS_popout.innerWidth / SDS_popout.innerHeight;
	var multiple = 1;

	if (winAspect >= gameAspect){
		// height constrained
		multiple = SDS_popout.innerHeight / SDS.gameRender.height;
	}
	else{
		// width constrained
		multiple = SDS_popout.innerWidth / SDS.gameRender.width;
	}
	if (snap){
		multiple = Math.round(multiple);
		multiple = Math.max(multiple,1);
	}

	var neww = Math.round(multiple * SDS.gameRender.width);
	var newh = Math.round(multiple * SDS.gameRender.height);

	SDS.ctx.canvas.style.width = ""+neww+"px";
	SDS.ctx.canvas.style.height = ""+newh+"px";

	//console.log(""+SDS_popout.innerWidth+" "+SDS_popout.innerHeight+" -> "+multiple+"x "+neww+" "+newh);

	//SDS_popout.resizeTo(neww + (SDS_popout.outerWidth - SDS_popout.innerWidth), newh + (SDS_popout.outerHeight - SDS_popout.innerHeight));
	if (SDS_aspect_finalize) SDS_popout.clearTimeout(SDS_aspect_finalize);
	SDS_aspect_finalize = SDS_popout.setTimeout(()=>{
		//console.log("Requesting (with outer-inner diff) "+neww+"+"+(SDS_popout.outerWidth - SDS_popout.innerWidth)+" "+newh+"+"+(SDS_popout.outerHeight - SDS_popout.innerHeight));
		SDS_popout.resizeTo(neww + (SDS_popout.outerWidth - SDS_popout.innerWidth), newh + (SDS_popout.outerHeight - SDS_popout.innerHeight));
		//SDS_ResizePopOut(neww,newh, neww + (SDS_popout.outerWidth - SDS_popout.innerWidth), newh + (SDS_popout.outerHeight - SDS_popout.innerHeight))
		SDS_aspect_finalize = null;
	},100);
};

function SDS_GenerateCanvas(){
	var canvas = document.createElement('canvas');
	canvas.width = 768;
	canvas.height = 480;
	canvas.id = "game_canvas";

	SDS.ctx = canvas.getContext("2d");
	SDS.ctx.imageSmoothingEnabled = false;
	SDS.ctx.mozImageSmoothingEnabled = false;
	SDS.ctx.oImageSmoothingEnabled = false;
	SDS.ctx.webkitImageSmoothingEnabled = false;

	return canvas;
};

function SDS_SetUpRoom(roomname, objMgr){
	var room = SDS_MAP_ROOMS[roomname];
	room.removeAllSprites(); // just in case, but hopefully redundant
	room.removeAllDeco();

	objMgr.destroyAll();
	for (var i = 0; i<room.object_initializers.length; i++){
		var obj = room.object_initializers[i](room);
		if (obj){
			objMgr.add(obj);
		}
	}

	room.bake(); // may not always be desired, but it is for now
	room.bakeLayers();
	room.bakeWater();
	room.initDeco();

	return room;
}

var _GET={};
function GLOBAL_ParseGets(){
	var params = window.location.href.split('?');
	if (params.length<=1) return;
	params=params[1];

	var pairs = params.split("&");

	for (var i=0; i<pairs.length; i++){
		var pair = pairs[i].split('=');

		pair[0]=decodeURIComponent(pair[0]);
		pair[1]=decodeURIComponent(pair[1]);
		_GET[pair[0]]=pair[1];
	}
}

var GLOBAL_TIME_FRAMES=0;
var GLOBAL_TIME_SECONDS=0;
var GLOBAL_TIME_MINUTES=0;
var GLOBAL_TIME_HOURS=0;
var GLOBAL_COMPLETION=0;
var GLOBAL_SPEEDRUN=false;
var GLOBAL_SPEEDRUN_EXTREME=false;
var GLOBAL_TIME_IN=false;
function SDS_incTime(){
	if (GLOBAL_TIME_IN) return;

	GLOBAL_TIME_FRAMES++;
	if (GLOBAL_TIME_FRAMES>29){
		GLOBAL_TIME_SECONDS++;
		GLOBAL_TIME_FRAMES-=30;
		if (GLOBAL_TIME_SECONDS>59){
			GLOBAL_TIME_MINUTES++;
			GLOBAL_TIME_SECONDS-=60;
			if (GLOBAL_TIME_MINUTES>59){
				GLOBAL_TIME_HOURS++;
				GLOBAL_TIME_MINUTES-=60;
			}
		}
	}
}

var SDS = {}; // game state wrapper var
function SDS_Main(){
	if (!navigator.userAgent.includes("Chrome")){
		alert("You appear to be using a browser other than Google Chrome.  This game was developed for desktop Chrome/Chromium/Electron because it has far and away the best standard support for various audio and graphics effects.  Firefox appears caught up enough to handle the core mechanics and drawing, but as of 10/2016, audio is still incorrect.  Consider yourself advised! ["+navigator.userAgent+"]");
	}

	SDS.objMgr = null; //new GameObjectManager();

	SDS.room = null; //SDS_SetUpRoom("village_caves", objMgr);

	SDS.meli = null; //new GO_Meli();
	/*meli.controls = GLOBAL_CONTROLS;
	// test room
	//meli.sprite.x = 32;
	//meli.sprite.y = 184;
	// village/caves
	//meli.sprite.x = 226;
	//meli.sprite.y = 92;
	//game start
	meli.sprite.x = 22<<4;
	meli.sprite.y = 15<<4;
	testRoom.addSprite(meli.sprite);
	testRoom.addSprite(meli.tail);
	testRoom.addSprite(meli.sparkle);
	objMgr.add(meli);*/


	SDS.gameRender = document.createElement('canvas');
	SDS.gameRender.width = 384;
	SDS.gameRender.height = 240;
	SDS.gctx = SDS.gameRender.getContext("2d");

	//testRoom.drawIntoCanvas(gameRender);
	//testRoom.drawSpritesIntoCanvas(gameRender);

	var canvas = SDS_GenerateCanvas()
	
	SDS.divGame = document.getElementById("game");
	SDS.divGame.appendChild(canvas);
	SDS.divGame.style.width = ""+canvas.width+"px";
	SDS.divGame.style.height = ""+canvas.height+"px";

	SDS.divGame.addEventListener("click",SDS_Audio);

	/*var devicePixelRatio = window.devicePixelRatio || 1;
	var backingStoreRatio = 	SDS.ctx.webkitBackingStorePixelRatio ||
							SDS.ctx.mozBackingStorePixelRatio ||
							SDS.ctx.msBackingStorePixelRatio ||
							SDS.ctx.oBackingStorePixelRatio ||
							SDS.ctx.backingStorePixelRatio || 1;

	var ratio = devicePixelRatio / backingStoreRatio;

	// working fix for blur high-density screens.
	// What it's doing has to do with how the browser is using the hardware rendering pipeline;
	// similar fixes exist to crisp up e.g. OGL vs DirectX.
	// Suffice to say this makes it the WRONG solution, but still the only one that works :P
	if (ratio>1){
		//SDS.ctx.translate(0.5,0.5);
		//SDS.ctx.translate(-.75,-.75);
	}*/

	// further exploration shows the blurriness occurs on high-density screens 
	// when the CSS canvas margins work out to fractional pixels
	window.addEventListener('resize',SDS_margins);
	SDS_margins();

	// and also this all also has also to do with the effing position of the window
	// within the user's screen.  If chrome is itself on an odd pixel, it'll blur.
	// fml.

	SDS.ctx.drawImage(SDS.gameRender,0,0,768,480);

	SDS.dlgMgr = new DialogueManager();
//	dlgMgr.open("narrator_testing",noop);
	
	SDS.meters = new Meters();

	SDS.meliControls = null;//meli.controls;

	SDS.evtMgr = new ScriptedEventManager(SDS.objMgr,SDS.dlgMgr);
	//evtMgr.run("Meli_goes_right_test");

	//var chip = new Soundchip("testsquare",Chiptune_ctx);
	//var bgm = new Chiptune("testsong",chip);

	//var chip = new Soundchip("oneofeach",Chiptune_ctx);
	//var bgm = new Chiptune("testsongDX",chip);

	//var chip = new Soundchip("noisetest",Chiptune_ctx);
	//var bgm = new Chiptune("noisesong",chip);

	SDS.bgmchip = new Soundchip("oneofeach",null);
	//SDS.bgmName = "Fly, Legendary SkyDragon";
	//SDS.bgmOverride = null; // global "hack" for when story events force a BGM different from the room
	//SDS.bgm = new Chiptune(SDS.bgmName,SDS.bgmchip);
	//
	//if (SDS.bgm){
	//	SDS.bgm.play(true);
	//}

	SDS.switchBgm = function(bgmName,loop){
		if (SDS.bgm) SDS.bgm.stop();
		SDS.bgmName = bgmName;
		
		// mitigate sound timing degradation by bouncing the contexts
		// whenever we need to to a full and proper reload
		SDS.bgmchip.resetContext();

		SDS.bgm = new Chiptune(SDS.bgmName,SDS.bgmchip);
		SDS.bgm.play(loop);
	}
	// note caveats on the Chiptune.queue function.
	// tl;dr, track queueing is naive and assumes
	// the current and incoming tunes use the same
	// chip and define content on the same channels.
	// Errors may arise otherwise, e.g attempting to
	// push new notes onto a previously undefined channel
	// or failing to end ongoing notes on a channel
	// with no data in the incoming track.
	SDS.queueBgm = function(bgmName,flush){
		if (!SDS.bgm || !SDS.bgm.is_playing){
			SDS.switchBgm(bgmName,true); // assuming anything that can queue can also loop
			return;
		}
		else if (bgmName==SDS.bgmName) return;

		SDS.bgm.queue(bgmName, flush, true);
		SDS.bgmName = bgmName;
	}
	SDS.switchBgm("Fly, Legendary SkyDragon",true);

	// we ~can~, kinda mostly generally, share one sound chip
	// but since each sound has a queueing delay, and they 
	// combine to cut out channels longer than needed,
	// try 2 sound chips.
	SDS.sfxchip = new Soundchip("oneofeach",null);
	SDS.systemSfx = null;

	SDS.gamestate = DEFINE_MAIN_STATES.TITLE;
	SDS.statecache = {};

	SDS.ending = null;

	SDS.loadDate = new Date();

	if (window.requestAnimationFrame) window.requestAnimationFrame(SDS_AnimationFrame);
	else setInterval(SDS_Gameloop,1000/30);
}

// Web Audio autoplay policy ca October 2018
function SDS_Audio(){
	SDS.bgmchip.resume();
	SDS.sfxchip.resume();
}

//ctx.globalCompositeOperation
SDS.screenFX = null;
SDS.screenFXColor = null;

var SDS_frameskip=1;
var SDS_framecount=1;
var SDS_lastframe = null;
function SDS_AnimationFrame(){
	--SDS_framecount;
	if (SDS_framecount<=0){
		SDS_framecount=SDS_frameskip+1;
		SDS_Gameloop();
	}
	SDS_lastframe = (SDS_popout || window).requestAnimationFrame(SDS_AnimationFrame);
}

function SDS_Gameloop(){
	SDS.dlgMgr.Update();

	switch (SDS.gamestate){
		case DEFINE_MAIN_STATES.LOADING:
			if (SDS_Load_imgComplete()){
				SDS.gamestate = SDS.statecache.nextstate ? SDS.statecache.nextstate : DEFINE_MAIN_STATES.TITLE;
				SDS.statecache = {};
			}
		break;
		case DEFINE_MAIN_STATES.TITLE:
			if (typeof(SDS.statecache.cloudscroll)==="undefined") SDS.statecache.cloudscroll=0;
			SDS.statecache.cloudscroll--;
			if (SDS.statecache.cloudscroll<=-SDS_STATIC_ART["Cloudbar"].width) SDS.statecache.cloudscroll+=SDS_STATIC_ART["Cloudbar"].width;

			if (GLOBAL_CONTROLS.pressed.A || GLOBAL_CONTROLS.pressed.B || GLOBAL_CONTROLS.pressed.start){
				if (!SDS.statecache.engaged){
					SDS.systemSfx = new Chiptune("FX_confirm",SDS.sfxchip);
					SDS.systemSfx.play(false);


					SDS.statecache.engaged=true;
					_HEAP.diaryElem = document.getElementById("diary");
					if (!_HEAP.diaryElem.value || _HEAP.diaryElem.value==""){
						SDS.dlgMgr.open("Diary_empty",function(index,retval){
							if (retval!=null){
								SDS.statecache.loadpoint=null;
								SDS.statecache.loadready=true;
							}
						});
					}
					else{
						SDS.dlgMgr.open("Diary_read",function(index,retval){
							if (retval!=null){
								SDS.statecache.loadpoint=GLOBAL_DIARY.read(_HEAP.diaryElem.value);
								if (SDS.statecache.loadpoint==null){
									SDS.dlgMgr.open("Diary_fail",function(index,retval){
										if (retval!=null){
											SDS.statecache.loadready=true;
										}
									});
								}
								else {
									SDS.dlgMgr.open("Diary_success",function(index,retval){
										if (retval!=null){
											SDS.statecache.loadready=true;
										}
									});
								}
							}
						});
					}
				}
			}

			if (SDS.statecache.loadready){
				SDS.objMgr = new GameObjectManager();
				SDS.evtMgr = new ScriptedEventManager(SDS.objMgr,SDS.dlgMgr);

				SDS.meli = new GO_Meli();
				SDS.meli.controls = GLOBAL_CONTROLS;

				if (!SDS.statecache.loadpoint){
					SDS.room = SDS_SetUpRoom("village_caves", SDS.objMgr);
					
					SDS.meli.sprite.x = 22<<4;
					SDS.meli.sprite.y = (15<<4)-6;
				}
				else{
					SDS.room = SDS_SetUpRoom(SDS.statecache.loadpoint.roomname, SDS.objMgr);
					_HEAP.diaryobj = SDS.objMgr.getNamedObject(SDS.statecache.loadpoint.diaryname);
					
					SDS.meli.sprite.x = _HEAP.diaryobj.sprite.x;
					SDS.meli.sprite.y = _HEAP.diaryobj.sprite.y;
				}

				if ((SDS.bgmOverride || SDS.room.getBGM()) != SDS.bgmName){
					SDS.switchBgm((SDS.bgmOverride || SDS.room.getBGM()),true);
					SDS.sfxchip.resetContext(); // because why not?
				}

				SDS.room.addSprite(SDS.meli.sprite);
				SDS.room.addSprite(SDS.meli.tail);
				SDS.room.addSprite(SDS.meli.sparkle);
				for (var i=0; i<SDS.meli.treasures.length; ++i){
					SDS.room.addSprite(SDS.meli.treasures[i]);
				}
				SDS.objMgr.add(SDS.meli);

				SDS.meters.forceRedraw();

				SDS.statecache = {};
				SDS.statecache.nextstate = DEFINE_MAIN_STATES.PLAY;
				SDS.gamestate = DEFINE_MAIN_STATES.LOADING;
			}

			SDS.gctx.clearRect(0,0,384,240);
			SDS.gctx.drawImage(SDS_STATIC_ART["Cloudbar"],SDS.statecache.cloudscroll,0);
			SDS.gctx.drawImage(SDS_STATIC_ART["Cloudbar"],SDS.statecache.cloudscroll+SDS_STATIC_ART["Cloudbar"].width,0);
			SDS.gctx.drawImage(SDS_STATIC_ART["Title"],0,0);
		break;

		case DEFINE_MAIN_STATES.PLAY:
			if (!SDS.dlgMgr.isPausing()){
				SDS.evtMgr.Update();

				if (SDS.dlgMgr.isBlocking() && SDS.meli.controls==GLOBAL_CONTROLS){
					SDS.meliControls = SDS.meli.controls;
					SDS.meli.controls = new Controls();
					SDS.objMgr.Update();
					SDS.room.Update();
					SDS.meli.controls=SDS.meliControls;
				}
				else{
					SDS.objMgr.Update();
					SDS.room.Update();
				}

				if (!SDS.dlgMgr.isBlocking() && SDS.meli.controls==GLOBAL_CONTROLS){
					SDS_incTime(); // only count frames where the player has control of the action
				}
			}

			if (SDS.meli.checking!=null){
				_HEAP.checkEvt = SDS.meli.checking.checkEvent(SDS.meli);
				if (_HEAP.checkEvt && _HEAP.checkEvt!=""){
					SDS.systemSfx = new Chiptune("FX_confirm",SDS.sfxchip);
					SDS.systemSfx.play(false);
					SDS.evtMgr.run(_HEAP.checkEvt);
				}
				SDS.meli.checking=null;
			}

			if (SDS.meli.triggering!=null){
				SDS.evtMgr.run(SDS.meli.triggering.triggerEvent(SDS.meli));
				SDS.meli.triggering=null;
			}

			if (SDS.meli.collecting!=null){
				SDS.evtMgr.run(SDS.meli.collecting.collect(SDS.meli));
				SDS.meli.collecting=null;
			}

			if (SDS.meli.in_door!=null){
				if (SDS.meli.in_door.getDestName()){
					SDS.room.flush();
					GLOBAL_SCRATCHPAD.reset();
					//SDS.objMgr.remove(SDS.meli);
					// force removal with ._remove (rather than .remove) now, since we'll be flushing before next update
					// also, pass the nocleanup flag true so sprites aren't unlinked
					SDS.objMgr._remove(SDS.meli,true); 
					
					//SDS.meli.in_door.relocate(SDS.meli.sprite); // needs to happen before we SetUpRoom, which will destroy all current objects
					SDS.room = SDS_SetUpRoom(SDS.meli.in_door.getDestName(),SDS.objMgr);

					if ((SDS.bgmOverride || SDS.room.getBGM()) != SDS.bgmName){
						SDS.switchBgm((SDS.bgmOverride || SDS.room.getBGM()),true);
					}

					SDS.room.addSprite(SDS.meli.sprite);
					SDS.room.addSprite(SDS.meli.tail);
					SDS.room.addSprite(SDS.meli.sparkle);
					for (var i=0; i<SDS.meli.treasures.length; ++i){
						SDS.room.addSprite(SDS.meli.treasures[i]);
					}

					SDS.objMgr.add(SDS.meli);
					SDS.meli.in_door.relocate(SDS.meli.sprite); // new architecture: relocate AFTER room add, so the relocator can know what we're going into; the old in_door door will persist just fine as long as it's one of our members
				}
				SDS.meli.in_door=null;

				// new room may need new assets
				//SDS.statecache = {};
				//SDS.statecache.nextstate = DEFINE_MAIN_STATES.PLAY;
				//SDS.gamestate = DEFINE_MAIN_STATES.LOADING;
				//return;
			}

			SDS.gctx.clearRect(0,0,384,240);
			SDS.room.scrollCenterOn(
				SDS.gameRender,
				SDS.meli.sprite.x+(SDS.meli.sprite.tile_sheet.tile_pixel_width>>1),
				SDS.meli.sprite.y+(SDS.meli.sprite.tile_sheet.tile_pixel_height>>1));
			SDS.room.drawIntoCanvas(SDS.gameRender);
			
			if (SDS.screenFX && SDS.screenFXColor){
				SDS.gctx.globalCompositeOperation = SDS.screenFX;
				SDS.gctx.fillStyle = SDS.screenFXColor;
				SDS.gctx.fillRect(0,0,384,240);
				SDS.gctx.globalCompositeOperation = "source-over";
			}

			SDS.meters.Update();
			SDS.meters.drawIntoCanvas(SDS.gameRender);

			// wait until all in-game stuff is clear before ending
			if (SDS.evtMgr.events.length===0 && SDS.dlgMgr.dialogues.length===0 && SDS.ending){
				SDS.gamestate = DEFINE_MAIN_STATES.ENDING;
			}
		break;

		case DEFINE_MAIN_STATES.ENDING:
			if (typeof(SDS.statecache.cloudscroll)==="undefined") SDS.statecache.cloudscroll=0;
			SDS.statecache.cloudscroll--;
			if (SDS.statecache.cloudscroll<=-SDS_STATIC_ART["Cloudbar"].width) SDS.statecache.cloudscroll+=SDS_STATIC_ART["Cloudbar"].width;

			if (typeof(SDS.statecache.endmarquee)==="undefined"){
				SDS.statecache.ending_ended = false;
				SDS.statecache.endmarquee = new Marquee(
					{ 
						x: 16,//0,
						y: 0,
						width: 352,//384, // text will be centered within box
						height: 240,
						
						dir: 1, // 0/false = horiz, 1/true = vert
						rate: 4,// in 1/8px per frame,
						loop: false, // if false, final content will be held
						content: SDS_BuildEnding(SDS.ending).join('\n'),//(DATA_SDS_ENDINGS[SDS.ending] || []).concat(DATA_SDS_ENDINGS["Credits"]).concat(DATA_SDS_ENDINGS["Fin"]).join('\n'), // newlines are honored on vert, treated as a full width of whitespace in horiz
						background: '#000',
						clear: true
					},
					GLOBAL_CONTROLS,
					function(){
						SDS.statecache.ending_ended = true;
					},
					SDS_STATIC_SHEETS["EndFont"]
				);
			}

			SDS.statecache.endmarquee.Update();


			SDS.gctx.clearRect(0,0,384,240);
			SDS.gctx.drawImage(SDS_STATIC_ART["Cloudbar"],SDS.statecache.cloudscroll,0);
			SDS.gctx.drawImage(SDS_STATIC_ART["Cloudbar"],SDS.statecache.cloudscroll+SDS_STATIC_ART["Cloudbar"].width,0);
			SDS.gctx.fillStyle = SDS.statecache.endmarquee.background;
			SDS.gctx.fillRect(0,0,16,240);
			SDS.gctx.fillRect(368,0,16,240);
			SDS.statecache.endmarquee.drawIntoCanvas(SDS.gameRender);

			if (SDS.statecache.ending_ended && GLOBAL_CONTROLS.pressed.A){
				SDS.statecache = {};
				SDS.ending = null;
				SDS.switchBgm("Fly, Legendary SkyDragon",true);
				SDS.gamestate = DEFINE_MAIN_STATES.TITLE;
			}

		break;
	}

	SDS.dlgMgr.drawIntoCanvas(SDS.gameRender);

	SDS.ctx.drawImage(SDS.gameRender,0,0,768,480);

	// meli's controls will be Updated (clear pressed&released)
	// in GameObjectManager's update loop as with all other GOs.
	// But when the globals aren't attached, they still need to update
	// and afaik no js event queue event that might set a button
	// will trigger between the GO update loop and here, so no
	// input edges should be dropped by redundancy.
	GLOBAL_CONTROLS.Update();

	if (SDS.bgm) SDS.bgm.Update();
	if (SDS.systemSfx) SDS.systemSfx.Update();
}


window.onload = SDS_Main_OnLoad;
window.addEventListener('beforeunload',()=>{if(SDS_popout!=null) SDS_popout.close();});

function SDS_Main_OnLoad(){
	GLOBAL_ParseGets();
	//GLOBAL_SPEEDRUN=(_GET["handle"]); // PHP-triggered timekeeping

	SDS_Data_Init();

	SDS_Load(SDS_Main);
}