iigs-game-engine/tools/mksprite.js

121 lines
4.3 KiB
JavaScript

/**
* Basic sprite compiler
*
* GTE has some specific needs that makes existing tools (like MrSprite) inappropriate. GTE
* sprites need to reference some internal data structures and have slightly different code
* in order to handle clipping to the playfield bounds.
*
* The core sprite drawing approach is the same (set up Bank 1 direct page and stack), but
* the setup and dispatch are a bit different.
*
* A Note on Clipping
*
* GTE supports two clipping buffers for sprites to use. The first one is a static buffer
* that is aligned with the playfield and is used to clip the sprite when crossing the
* left and right boundaries, but since it's a static image, mask data can be put anywhere
* that the sprites should not show through, so irregular borders and sprite punch-outs
* on the playfield are possible.
*
* The second buffer matches the current tiles in the playfield and can be used as a
* dynamic mask of the playfield. Since the sprite code itself must use this data,
* different variations of the same sprite can be created to stand in "front" and "behind"
* different screen elements.
*
* The sprite requires the X and Y registers for this. The most general code that
* should be used for each sprite word is this:
*
* Example: DATA = $5670, MASK = $000F, screen_mask = $FF00, field_mask = $F0FF
* lda DP ; A = $1234
* eor #DATA ; A = $4444
* and #~MASK ; A = $4440
* and screen_mask,y ; A = $4400
* and >field_mask,x ; A = $4000
* eor DP ; A = $5234 <-- Only the high nibble is set to the sprite data
* sta DP
*
* It is not *required* that sprites use this approach, any compiled sprites code can be used,
* so if a sprite does not need to be masked, than any of the fast sprite approaches can be
* used.
*
* For clipping vertically, we pass in the starting and finishing lines in a register and
* a sprite record is set up to allow the sprite to be entered in the middle and exited
* before the last line of the sprite.
*/
const { readPNG, pngToIIgsBuff } = require('./png2iigs');
const process = require('process');
main(process.argv.slice(2)).then(
() => process.exit(0),
(e) => {
console.error(e);
process.exit(1);
}
);
async function main(argv) {
const png = await readPNG(argv[0]);
const buff = pngToIIgsBuff(png);
const options = {
staticClip: true,
label: 'Sprite001'
};
startIndex = getArg(argv, '--start-index', x => parseInt(x, 10), 0);
asTileData = getArg(argv, '--as-tile-data', null, 0);
maxTiles = getArg(argv, '--max-tiles', x => parseInt(x, 10), 64);
}
function getArg(argv, arg, fn, defaultValue) {
for (let i = 0; i < argv.length; i += 1) {
if (argv[i] === arg) {
if (fn) {
return fn(argv[i+1]);
}
return true; // Return true if the argument was found
}
}
return defaultValue;
}
function buildMerlinCodeForSprite(sprite, options) {
const { label, staticClip } = options;
const rtnOpCode = options.longReturn ? 'rtl' : 'rts';
const sb = new StringBuilder();
sb.appendLine(`${label} ENT`);
sb.appendLine(` cpx #${sprite.height * 2}`);
sb.appendLine(` bcc *+3`);
sb.appendLine(` ${rtnOpCode}`);
sb.appendLine(` sei`);
sb.appendLine(` tcs`);
sb.appendLine(` jmp (${label}_jtbl,x)`);
sb.appendLine(`${label}_jtbl`);
for (let line = 0; line < sprite.rows.length; line += 1) {
lda DP ; A = $1234
* eor #DATA ; A = $4444
* and #~MASK ; A = $4440
* and screen_mask,y ; A = $4400
* and >field_mask,x ; A = $4000
* eor DP ; A = $5234 <-- Only the high nibble is set to the sprite data
* sta DP
sb.appendLine(` dw ${label}_${line}`);
}
// Implement each line to draw the sprite data
//
// label_XX tdc
// clc
// adc #160*line
// tcd
// main_XX lda 00
// and #
// ora #
//
for (let line = 0; line < sprite.rows.length; line += 1) {
}
return sb.toString();
}