#target photoshop

/******************************************************************************

FILE:     psd2agk-1.06.jsx
VERSION: 1.06
WRITTEN:  2017

psd2agk-1.06.jsx is a Photoshop script used to save toplevel layersets as sprites for App Game Kit.
The original x/y position and depth of the layers are maintained in AGK2, and spritesheet 
animations are automatically generated.

PREREQUIREMENTS:

You need to have Photoshop installed, of course. I have tested the script on CS6. The AGK file
generated by the script are Tier 1 (BASIC) using Virtual Resolution.

HOW TO USE:

To test it this script, first open game-graphic.psd in Photoshop. Then double-click psd2agk.jsx to
execute it. You’ll get a warning. Click OK. Now Photoshop will be super buzy as it saves each
layerset as a sprite.

When Photoshop is done, the script has saved the sprites as .png files in a subfolder called
Output. This folder must always be located at the same directory as the .psd file you run time 
script on.
It has also made a file called generatedLoader.agc which you will use to load your sprites.

HOW TO ADD THE GENERATED FILES TO AGK:

1) Open your AGK project. (If it is a new project, save it now.) Then select File > New File give it
the name generatedLoader.agc and click Save. (You only need this the first time you add the 
graphic to this project)

2) In AGK, add the following lines to main.agc:
	#include "generatedLoader.agc"
	Gosub autoLoad

2) from your ’Output’ directory, copy generatedLoader.agc to your AGK project directory, 
overwriting the old file.

3) Copy the .png files from your ’Output’ folder to your projects ’/media’ folder.

ADVANCED FEATURES:

To adjust settings, you can open psd2agk-1.06.jsx in a text editor.

The toplevel layersets are simply merged into sprites, so you can have all kind of layers or 
layersets inside them. For instance, take a look at all the crap in my ’backdrop’ layerset in 
game-graphics.psd

If the layersets filename contains the phrase Ani, it will treat all layersets inside it as frames in a 
animation and generate a spritesheet. Place the earliest frames at the top of the layer stack.

If the layersets name contain two slashes //, it is considered a comment layer and is ignored.

Layersets that share same name are considered to be sprites that share the same image. This 
means that you can duplicate the layerset, and the script will cleverly recycle the same image 
file. Lets say you duplicate a layerset called buildingBlock so you have 4 layersets:
	buildingBlock copy 3
	buildingBlock copy 2
	buildingBlock copy 
	buildingBlock
They will all be using a single image file. (that copy stuff is cleverly ignored)

FINAL NOTES:

The generated .png and .agc files overwrites existing files in the Output directory. Just so
you know.

For each generated sprite, all layersets must be turned off, one at a time. This means that the
script execution time will grow exponentially with each extra layerset: If you have 31 layersets, 
Photoshop will need to toggle layerset visibility 31*31 = 961 times. (The script is thoughtful 
enough to give a tiny beep when it’s done)

******************************************************************************/




/******************************************************************************
   SETTINGS
******************************************************************************/

// How many rows the spritesheets should contain.
// Set it to a number. Default is 8
imgPrRow = 8;

// Name of folder to save in, relative to the .psd file location
outputFolderName = "Output";

// Beep when the script is finished
// Set it to true or false
beepWhenFinished = true;

// Use this to scale down the output sprites. 
// 1.0 means no rescaling.
// If you like to work on double scale originals, set the finalGraphicScale to 0.5
finalGraphicScale = 1.0; 

// Type of resampling used if you size down the graphic.
// Set it to 1, 2 or 3:
//    1: NONE  
//    2: BICUBIC
//    3: BILINEAR
scaleResampling = 2;

// Save using the tinyPNG plugin?
// Set it to true or false
// NOTE: If set to true, tinyPNG needs to be installed
saveUsingTinyPNG = false;

// Do you want to execute an action on each sprite?
// Set it to true or false
// NOTE: Avoid actions which change the sprite size. Stick to basic)
// NOTE: Write "noAction" in the layerset name to not execute action on the sprite
doPsAction = false;

// The title of your action folder:
actionFolderTitle = "x";

// The title of your action:
actionTitle = "Ghost";


// Decides if the agk code should specify that the reference is an integer
// needed if you go #option_explicit but otherwise it kinda just clutters
// your code. An AGK example is seen in the next two lines:
// scaryGhostImg as integer
// scaryGhostImg = LoadImage("scaryGhost.png")
specifyAsInteger = false



/******************************************************************************
   INTRO
******************************************************************************/

var linialBackup = preferences.rulerUnits; // Settings saved
preferences.rulerUnits = Units.PIXELS;     // Settings changed

layerNameArray = new Array();



// The script will call the original .psd document 'docRefSource'. Then it creates a duplicate name 'docRefCopy' which it resize and do stuff with. Finally, it creates a 'docRef' duplicate of each sprite.


if (app.documents.length == 0) alert("You need to have an image open in Photoshop for this script to work!");
else {	
	docRefSource = app.activeDocument;
	var mappe = Folder(docRefSource.path + "/" + outputFolderName + "/");
	if (mappe instanceof Folder && mappe.exists) {
		outFit = "";
		listOfGraphic = "";
		countingRepeatedGraphic = 1;

		theSpriteDepth = 100;
		
		docRefCopy = docRefSource.duplicate();
		app.activeDocument = docRefCopy;
		
		// Size the shit down
		if (finalGraphicScale != 1.0) {
			if (scaleResampling == 1) docRefCopy.resizeImage(docRefCopy.width*finalGraphicScale, docRefCopy.height*finalGraphicScale, null, ResampleMethod.NONE);
			else if (scaleResampling == 2) docRefCopy.resizeImage(docRefCopy.width*finalGraphicScale, docRefCopy.height*finalGraphicScale, null, ResampleMethod.BICUBIC);
			else if (scaleResampling == 3) docRefCopy.resizeImage(docRefCopy.width*finalGraphicScale, docRefCopy.height*finalGraphicScale, null, ResampleMethod.BILINEAR);
			else alert("Wrong value in scaleResamling.");
		}
		
		orgWidth = docRefCopy.width.value;          // Log the width and height
		orgHeight = docRefCopy.height.value;		

		/******************************************************************************
		   RUN ON EACH VISIBLE LAYER
		******************************************************************************/

		



		for (var firstLvlLayerRef = 0; firstLvlLayerRef < docRefCopy.layers.length; firstLvlLayerRef++){
			docRefCopy.activeLayer = docRefCopy.layers[firstLvlLayerRef];
		if (docRefCopy.activeLayer.name.indexOf("//") == -1) {	//Only run if the layer is not a comment
			
				alreadyUsed = false;							// Determine if the name is already in the array
				for (i = 0; i < layerNameArray.length; i++) {
					if (layerNameArray[i] == removeCrapText(docRefCopy.activeLayer.name)) alreadyUsed = true;
				}
				//alert ("" + docRefCopy.activeLayer.name + " is already used: " + alreadyUsed);
				
				layerNameArray.push(removeCrapText(docRefCopy.activeLayer.name));	// push a cleaned version of the layer name into the array
				
				if (docRefCopy.layers[firstLvlLayerRef].typename == "LayerSet" && docRefCopy.layers[firstLvlLayerRef].name.indexOf("Ani") != -1) {
					if (alreadyUsed) repeatSpriteSheet();
					else mkSpriteSheet ();
				}
				else {
					if (alreadyUsed) layerRepeat ();
					else layerToPng ();
				}
			}
		}


		docRefCopy.close(SaveOptions.DONOTSAVECHANGES);
		
		
		
		

		/******************************************************************************
		   GENERATE TEXT
		******************************************************************************/
		outFit = listOfGraphic + "\n\n\n\n" + outFit;
		outFit = "// Generated text here\n\nautoLoad:\n\n" + outFit + "\nReturn\n";

		var targ = new File(docRefSource.path + "/" + outputFolderName + "/generatedLoader.agc");
		targ.open("w");
		targ.write(outFit);
		targ.close();
		
		if (beepWhenFinished) beep();
		
	} else alert("The folder " + docRefSource.path + "/" + outputFolderName + "/" + " does not exist.");
} // else



/******************************************************************************
   EXIT
******************************************************************************/



preferences.rulerUnits = linialBackup; // Settings restored










/******************************************************************************
   FUNCTIONS
******************************************************************************/




function removeCrapText (layerNameText) {

	if (layerNameText.indexOf("noAction") != -1) layerNameText = layerNameText.split("noAction").join("");

	
	if (layerNameText.indexOf(" copy") != -1) {
		for (b = 200; b > 1; b--) {
			layerNameText = layerNameText.split(" copy " + b).join("");

		}
		layerNameText = layerNameText.split(" copy").join("");
	}
	
	layerNameText = layerNameText.split(" ").join("");
	layerNameText = layerNameText.split(" ").join("");
	layerNameText = layerNameText.split(" ").join("");
	layerNameText = layerNameText.split(" ").join("");
	layerNameText = layerNameText.split(" ").join("");
	layerNameText = layerNameText.split(" ").join("");
	layerNameText = layerNameText.split(" ").join("");
	layerNameText = layerNameText.split(" ").join("");
	layerNameText = layerNameText.split(" ").join("");
	layerNameText = layerNameText.split(" ").join("");
	layerNameText = layerNameText.split(" ").join("");
	layerNameText = layerNameText.split(" ").join("");
	
	return layerNameText;
}



function layerToPng () { 
	// This function will save the layer as a .png and generate AGK code


	docRef = docRefCopy.duplicate();
	app.activeDocument = docRef;
	layerRef = docRef.activeLayer;

	for (var mm = 0; mm < docRef.layers.length; mm++){ // Turn all layers and layer sets invisible
		docRef.layers[mm].visible = false;
	}
	layerRef.visible = true;                                     // Turn our active layer visible again
	docRef.trim(TrimType.TRANSPARENT, true, true, false, false); // Crop top and left

	topOffset = orgHeight - docRef.height.value;
	leftOffset = orgWidth - docRef.width.value;
	layerName = removeCrapText(layerRef.name);

	// Generate AGK code
	
	if (specifyAsInteger) listOfGraphic = listOfGraphic + layerName + "Img as integer\n";
	listOfGraphic = listOfGraphic + layerName + "Img = LoadImage(\"" + layerName + ".png\")\n";
	
	outFitAdd = "";
	if (specifyAsInteger) outFitAdd = outFitAdd + layerName + "Spr as integer\n";
	outFitAdd = outFitAdd + layerName + "Spr = CreateSprite(" + layerName + "Img)\n";
	outFitAdd = outFitAdd + "SetSpritePosition ( " + layerName + "Spr, " + leftOffset + ", " + topOffset + " )\n";
	outFitAdd = outFitAdd + "SetSpriteDepth ( " + layerName + "Spr, " + theSpriteDepth + " )\n\n";		
	outFit = outFitAdd + outFit;
	
	countingRepeatedGraphic = countingRepeatedGraphic + 1;
	
	theSpriteDepth = theSpriteDepth + 10;
	
	// Trim
	docRef.trim(TrimType.TRANSPARENT, true, true, true, true); // Trim all

	// Save as PNG and close
	if (saveUsingTinyPNG) saveTinyPNG(docRef);
	else saveAsPNG (docRef);
	
} // end function layerToPng


function layerRepeat () { 
	// This function will save some AGK code but does not save as a new image file


	docRef = docRefCopy.duplicate();
	app.activeDocument = docRef;
	layerRef = docRef.activeLayer;

	for (var mm = 0; mm < docRef.layers.length; mm++){ // Turn all layers and layer sets invisible
		docRef.layers[mm].visible = false;
	}
	layerRef.visible = true;                                     // Turn our active layer visible again
	docRef.trim(TrimType.TRANSPARENT, true, true, false, false); // Crop top and left

	topOffset = orgHeight - docRef.height.value;
	leftOffset = orgWidth - docRef.width.value;
	layerName = removeCrapText(layerRef.name);

	// Generate AGK code
	outFitAdd = "";
	if (specifyAsInteger) outFitAdd = outFitAdd + layerName + "Spr" + countingRepeatedGraphic + " as integer\n";
	outFitAdd = outFitAdd + layerName + "Spr" + countingRepeatedGraphic + " = CreateSprite(" + layerName + "Img)\n";
	outFitAdd = outFitAdd + "SetSpritePosition ( " + layerName + "Spr" + countingRepeatedGraphic + ", " + leftOffset + ", " + topOffset + " )\n";
	outFitAdd = outFitAdd + "SetSpriteDepth ( " + layerName + "Spr" + countingRepeatedGraphic + ", " + theSpriteDepth + " )\n\n";	
	outFit = outFitAdd + outFit;
	countingRepeatedGraphic = countingRepeatedGraphic + 1;
	
	theSpriteDepth = theSpriteDepth + 10;

	docRef.close(SaveOptions.DONOTSAVECHANGES);
	
} // end function layerRepeat



function mkSpriteSheet () {
	// generates a spritesheet
	
	docRef = docRefCopy.duplicate();
	app.activeDocument = docRef;
	layerRef = docRef.activeLayer;

	for (var mm = 0; mm < docRef.layers.length; mm++){ // Turn all layers and layer sets invisible
		docRef.layers[mm].visible = false;
	}
	layerRef.visible = true;                                     // Turn our active layer visible again
	docRef.trim(TrimType.TRANSPARENT, true, true, false, false); // Crop top and left
	
	topOffset = orgHeight - docRef.height.value;
	leftOffset = orgWidth - docRef.width.value;
	layerName = removeCrapText(layerRef.name);


	NumberOfSprites = 0;
	for (var m = 0; m < layerRef.layers.length; m++){
		  NumberOfSprites = NumberOfSprites + 1;
	}
	
	// Trim
	docRef.trim(TrimType.TRANSPARENT, true, true, true, true); // Trim all
	
	spriteWidth = docRef.width.value;          // Log the width and height
	spriteHeight = docRef.height.value;
	
	// Generate AGK code
	if (specifyAsInteger) listOfGraphic = listOfGraphic + layerName + "Img as integer\n";	
	listOfGraphic = listOfGraphic + layerName + "Img = LoadImage(\"" + layerName + ".png\")\n";	
	outFitAdd = "";
	if (specifyAsInteger) outFitAdd = outFitAdd + layerName + "Spr as integer\n";
	outFitAdd = outFitAdd + layerName + "Spr = CreateSprite(" + layerName + "Img)\n";
	outFitAdd = outFitAdd + "SetSpritePosition ( " + layerName + "Spr, " + leftOffset + ", " + topOffset + " )\n";
	outFitAdd = outFitAdd + "SetSpriteAnimation ( " + layerName + "Spr, " + spriteWidth + ", " + spriteHeight + ", " + NumberOfSprites + " )\n";
	outFitAdd = outFitAdd + "SetSpriteDepth ( " + layerName + "Spr, " + theSpriteDepth + " )\n\n";
	outFit = outFitAdd + outFit;
	
	theSpriteDepth = theSpriteDepth + 10;

	var col = Math.ceil( NumberOfSprites / imgPrRow );

	spriteSheetWidth = 0;
	if (imgPrRow > NumberOfSprites) {
	  spriteSheetWidth = NumberOfSprites*spriteWidth;
	}
	else {
	  spriteSheetWidth = imgPrRow*spriteWidth;
	} 

	var SpriteSheetDocRef = documents.add(spriteSheetWidth, spriteHeight*col, 72.0, "Spritesheet generated by simpleSpriteSheet.jsx", NewDocumentMode.RGB, DocumentFill.TRANSPARENT);


	var yPos = 0; // Numbers used to place the frames
	var xPos = 0;

	for (var m = 0; m < layerRef.layers.length; m++){
	  //alert(firstLvlLayerRef.layers.length + " " + firstLvlLayerRef.layers[m].name);
	  app.activeDocument = docRef;
	  docRef.activeLayer = layerRef.layers[m];
	  layerRef.layers[m].duplicate( SpriteSheetDocRef );
	  app.activeDocument = SpriteSheetDocRef;
	  positionLayer( SpriteSheetDocRef.activeLayer, xPos*spriteWidth, yPos*spriteHeight );
	  xPos++;
	  if (xPos >= imgPrRow) {
	    xPos=0; 
	    yPos++;
	  }
	}
	
	// Save as PNG and close
	if (saveUsingTinyPNG) saveTinyPNG(SpriteSheetDocRef);
	else saveAsPNG (SpriteSheetDocRef);
	
	docRef.close(SaveOptions.DONOTSAVECHANGES);

} // function mkSpriteSheet





function repeatSpriteSheet () {
	// repeats a spritesheet
	
	docRef = docRefCopy.duplicate();
	app.activeDocument = docRef;
	layerRef = docRef.activeLayer;

	for (var mm = 0; mm < docRef.layers.length; mm++){ // Turn all layers and layer sets invisible
		docRef.layers[mm].visible = false;
	}
	layerRef.visible = true;                                     // Turn our active layer visible again
	docRef.trim(TrimType.TRANSPARENT, true, true, false, false); // Crop top and left
	
	topOffset = orgHeight - docRef.height.value;
	leftOffset = orgWidth - docRef.width.value;
	layerName = removeCrapText(layerRef.name);

	
	
	NumberOfSprites = 0;
	for (var m = 0; m < layerRef.layers.length; m++){
		  NumberOfSprites = NumberOfSprites + 1;
	}
	
	// Trim
	docRef.trim(TrimType.TRANSPARENT, true, true, true, true); // Trim all
	
	spriteWidth = docRef.width.value;          // Log the width and height
	spriteHeight = docRef.height.value;
	
	// Generate AGK code
	outFitAdd = "";
	if (specifyAsInteger) outFitAdd = outFitAdd + layerName + "Spr" + countingRepeatedGraphic + " as integer\n";
	outFitAdd = outFitAdd + layerName + "Spr" + countingRepeatedGraphic + " = CreateSprite(" + layerName + "Img)\n";
	outFitAdd = outFitAdd + "SetSpritePosition ( " + layerName + "Spr" + countingRepeatedGraphic + ", " + leftOffset + ", " + topOffset + " )\n";
	outFitAdd = outFitAdd + "SetSpriteAnimation ( " + layerName + "Spr" + countingRepeatedGraphic + ", " + spriteWidth + ", " + spriteHeight + ", " + NumberOfSprites + " )\n\n";
	outFit = outFitAdd + outFit;

	
	countingRepeatedGraphic = countingRepeatedGraphic + 1;
	
	docRef.close(SaveOptions.DONOTSAVECHANGES);

} // function repeatSpriteSheet





function positionLayer( lyr, positionLayerX, positionLayerY ){ // layerObject, Number, Number
     // if can not move layer return
     if(lyr.iisBackgroundLayer||lyr.positionLocked)
	{
	     alert("Background!!!");
	     return
	}
     lyr.translate (positionLayerX, positionLayerY);
}



function saveAsPNG (docRefSave) {
	
	
	if (docRefSave.activeLayer.name.indexOf ("noAction") != -1) doActionOnSprite = false;
	else doActionOnSprite = true;
	
	var dummyVisibleLayer = docRefSave.artLayers.add(); // Photoshop must have a visible layer selected
	docRefSave.activeLayer = dummyVisibleLayer
	docRefSave.mergeVisibleLayers() // Merge the layers.

	for (i = docRefSave.layers.length-1; i >=0; i--) { // Remove all empty layers.
	  if (!docRefSave.layers[i].visible) {
	    docRefSave.layers[i].remove()
	  }
	}
	
	if (doPsAction && doActionOnSprite) {
		doAction(actionTitle, actionFolderTitle);
		var dummyVisibleLayer = docRefSave.artLayers.add(); // Photoshop must have a visible layer selected
		docRefSave.activeLayer = dummyVisibleLayer
		docRefSave.mergeVisibleLayers() // Merge the layers.
	}
	
	pngOptions = new PNGSaveOptions() // Set up PNG save options.
	pngOptions.compression = 0
	pngOptions.interlaced = false

	savePath = File(docRefSource.path + "/" + outputFolderName + "/" + layerName + ".png"); // Set up destination path.
	docRefSave.saveAs(savePath, pngOptions, false, Extension.NONE); // Save!

	docRefSave.close(SaveOptions.DONOTSAVECHANGES);
	
}

function saveTinyPNG(tinyPNGdoc) {
	
	if (tinyPNGdoc.activeLayer.name.indexOf ("noAction") != -1) doActionOnSprite = false;
	else doActionOnSprite = true;
	
	if (tinyPNGdoc.mode == DocumentMode.INDEXEDCOLOR) {
		tinyPNGdoc.changeMode(ChangeMode.RGB);
	}

	if (tinyPNGdoc.bitsPerChannel == BitsPerChannelType.SIXTEEN) {
		convertBitDepth(8);
	}
	
	var dummyVisibleLayer = tinyPNGdoc.artLayers.add(); // Photoshop must have a visible layer selected
	tinyPNGdoc.activeLayer = dummyVisibleLayer
	tinyPNGdoc.mergeVisibleLayers() // Merge the layers.

	for (i = tinyPNGdoc.layers.length-1; i >=0; i--) { // Remove all empty layers.
	  if (!tinyPNGdoc.layers[i].visible) {
	    tinyPNGdoc.layers[i].remove()
	  }
	}
	
	if (doPsAction && doActionOnSprite) {
		doAction(actionTitle, actionFolderTitle);
		var dummyVisibleLayer = tinyPNGdoc.artLayers.add(); // Photoshop must have a visible layer selected
		tinyPNGdoc.activeLayer = dummyVisibleLayer
		tinyPNGdoc.mergeVisibleLayers() // Merge the layers.
	}

	savePath = File(docRefSource.path + "/" + outputFolderName + "/" + layerName + ".png"); // Set up destination path.
	
	var type = charIDToTypeID("tyPN"); /* tyJP for JPEG */
	var percentage = 100;

	var tinypng = new ActionDescriptor();
	tinypng.putPath(charIDToTypeID("In  "), savePath); /* Overwrite original! */
	tinypng.putEnumerated(charIDToTypeID("FlTy"), charIDToTypeID("tyFT"), type);
	tinypng.putUnitDouble(charIDToTypeID("Scl "), charIDToTypeID("#Prc"), percentage );

	var compress = new ActionDescriptor();
	compress.putObject(charIDToTypeID("Usng"), charIDToTypeID("tinY"), tinypng);
	executeAction(charIDToTypeID("Expr"), compress, DialogModes.NO);
	
	tinyPNGdoc.close(SaveOptions.DONOTSAVECHANGES);
}



function convertBitDepth(bitdepth) {
	var id1 = charIDToTypeID("CnvM");
	var convert = new ActionDescriptor();
	var id2 = charIDToTypeID("Dpth");
	convert.putInteger(id2, bitdepth);
	executeAction(id1, convert, DialogModes.NO);
}







