From f2b5978cae84cad5e2d2bc56aebff0bfb72dacf6 Mon Sep 17 00:00:00 2001
From: dylan <>
Date: Fri, 5 May 2023 14:59:52 -0700
Subject: [PATCH] Starting on sprite editor

---
 cart.ts            |   3 +-
 cart_unpacked.json |  37 +++++++-
 codetab.ts         | 222 +++++++++++++++++++++++++++++++++++++++++++++
 colors.ts          |   2 +
 editmode.ts        | 215 +++----------------------------------------
 sheet.ts           |  19 +++-
 spritetab.ts       |  64 +++++++++++++
 7 files changed, 352 insertions(+), 210 deletions(-)
 create mode 100644 codetab.ts
 create mode 100644 spritetab.ts

diff --git a/cart.ts b/cart.ts
index 29adc11..f8fab9b 100644
--- a/cart.ts
+++ b/cart.ts
@@ -1,6 +1,7 @@
 import fakeCart from "./cart_unpacked.json" assert { type: "json" };
+import { Sheet } from "./sheet.ts";
 
-const cart = fakeCart;
+const cart = fakeCart as Array<Sheet>;
 
 export const loadCart = (_name: string) => {
 	return;
diff --git a/cart_unpacked.json b/cart_unpacked.json
index 9a21964..7f9c82b 100644
--- a/cart_unpacked.json
+++ b/cart_unpacked.json
@@ -5,6 +5,41 @@
 	},
 	{
 		"sheet_type": "code",
-		"value": "speed = 2; return (8)"
+		"value": "speed = 2;\nreturn 8;"
+	},
+	{
+		"sheet_type": "spritesheet",
+		"value": [
+			[
+				2, 2, 2, 2, 2, 2, 2, 2,
+				2, 1, 1, 1, 1, 1, 1, 2,
+				2, 1, 1, 1, 1, 1, 1, 2,
+				2, 1, 1, 1, 1, 1, 1, 2,
+				2, 1, 1, 1, 1, 1, 1, 2,
+				2, 1, 1, 1, 1, 1, 1, 2,
+				2, 1, 1, 1, 1, 1, 1, 2,
+				2, 2, 2, 2, 2, 2, 2, 2
+			],
+			[
+				2, 2, 2, 2, 2, 2, 2, 2,
+				2, 3, 3, 1, 1, 3, 3, 2,
+				2, 3, 3, 1, 1, 3, 3, 2,
+				2, 1, 1, 1, 1, 1, 1, 2,
+				2, 1, 1, 1, 1, 1, 1, 2,
+				2, 3, 3, 1, 1, 3, 3, 2,
+				2, 3, 3, 1, 1, 3, 3, 2,
+				2, 2, 2, 2, 2, 2, 2, 2
+			],
+			[
+				2, 2, 2, 2, 2, 2, 2, 2,
+				2, 4, 4, 4, 4, 5, 5, 2,
+				2, 4, 4, 4, 5, 5, 5, 2,
+				2, 4, 4, 5, 5, 5, 6, 2,
+				2, 4, 5, 5, 5, 6, 6, 2,
+				2, 5, 5, 5, 6, 6, 6, 2,
+				2, 5, 5, 6, 6, 6, 6, 2,
+				2, 2, 2, 2, 2, 2, 2, 2
+			]
+		]
 	}
 ]
\ No newline at end of file
diff --git a/codetab.ts b/codetab.ts
new file mode 100644
index 0000000..0bb8df3
--- /dev/null
+++ b/codetab.ts
@@ -0,0 +1,222 @@
+import { clearScreen, fillRect } from "./window.ts";
+import { fontWidth, fontHeight } from "./font.ts";
+import { drawText } from "./builtins.ts";
+import { COLOR } from "./colors.ts";
+import {getSheet, setSheet} from "./sheet.ts";
+import { K, getKeyboardString, keyPressed, shiftKeyDown } from "./keyboard.ts";
+
+// TODO: Make scrolling work
+const state = {
+	scrollX: 0,
+	scrollY: 0,
+	anchor: 0,
+	focus: 0,
+	get focusX() {return indexToGrid(this.code, this.focus).x;},
+	get focusY() {return indexToGrid(this.code, this.focus).y;},
+	get anchorX() {return indexToGrid(this.code, this.anchor).x;},
+	get anchorY() {return indexToGrid(this.code, this.anchor).y;},
+	isCollapsed() {
+		return this.anchor === this.focus;
+	},
+	clampInRange(n: number) {
+		return Math.max(0, Math.min(n, this.code.length))
+	},
+	setSelection(anchor: number | {x: number, y: number}, focus?: number | {x: number, y: number}) {
+		if (typeof anchor !== "number") {
+			anchor = gridToIndex(this.code, anchor.x, anchor.y);
+		}
+		focus = focus ?? anchor;
+		if (typeof focus !== "number") {
+			focus = gridToIndex(this.code, focus.x, focus.y);
+		}
+		this.anchor = this.clampInRange(anchor),
+		this.focus = this.clampInRange(focus);
+	},
+	setFocus(focus: number | {x: number, y: number}) {
+		if (typeof focus !== "number") {
+			focus = gridToIndex(this.code, focus.x, focus.y);
+		}
+		this.focus = this.clampInRange(focus);
+	},
+	insertText(text: string) {
+		const {code, anchor, focus} = this;
+		this.code = code.slice(0, Math.min(anchor, focus)) + text + code.slice(Math.max(anchor, focus));
+		this.setSelection(Math.min(anchor, focus) + text.length);
+	},
+	indent(indentString: string) {
+		const lines = this.code.split("\n");
+		const {focusX, focusY, anchorX, anchorY} = this;
+		const newLines = lines.map((line, i) => {
+			if (i >= Math.min(focusY, anchorY) && i <= Math.max(focusY, anchorY)) {
+				return indentString+line;
+			} else {
+				return line;
+			}
+		});
+		this.code = newLines.join("\n");
+		this.setSelection({x: anchorX+1, y: anchorY}, {x: focusX+1, y: focusY});
+	},
+	outdent(outdentRegex: RegExp) {
+		const lines = this.code.split("\n");
+		const {focusX, focusY, anchorX, anchorY} = this;
+		const newLines = lines.map((line, i) => {
+			const match = line.match(outdentRegex);
+			if (i >= Math.min(focusY, anchorY) && i <= Math.max(focusY, anchorY) && match) {
+				return line.slice(match[0].length);
+			} else {
+				return line;
+			}
+		});
+		this.code = newLines.join("\n");
+		this.setSelection({x: Math.max(0,anchorX-1), y: anchorY}, {x: Math.max(0,focusX-1), y: focusY});
+	},
+	backspace() {
+		const {code, focus} = this;
+		if (this.isCollapsed()) {
+			if (focus > 0) {
+				this.code = code.slice(0, focus-1) + code.slice(focus);
+				this.setSelection(focus-1);
+			}
+		} else {
+			this.insertText("");
+		}
+	},
+	delete() {
+		const {code, focus} = this;
+		if (this.isCollapsed()) {
+			if (focus < code.length) {
+				this.code = code.slice(0, focus) + code.slice(1+focus);
+			}
+		} else {
+			this.insertText("");
+		}
+	},
+	get code() {
+		const {sheet_type, value} = getSheet(0);
+		if (sheet_type !== "code") {
+			throw "Trying to run a non-code sheet as code."
+		}
+		return value;
+	},
+	set code(val) {
+		setSheet(0, "code", val);
+	}
+}
+
+const indexToGrid = (str: string, index: number) => {
+	const linesUpTo = str.slice(0,index).split("\n");
+	return {
+		x: linesUpTo[linesUpTo.length-1].length,
+		y: linesUpTo.length - 1,
+	}
+}
+
+const gridToIndex = (str: string, x: number, y: number) => {
+	const lines = str.split("\n");
+	if (y < 0) {
+		return 0;
+	}
+	if (y >= lines.length) {
+		return str.length;
+	}
+	return lines.slice(0, y).join("\n").length+Math.min(x, lines[y].length)+1;
+}
+
+const drawCodeField = (code: string, x: number, y: number, w: number, h: number) => {
+	const {
+		scrollX,
+		scrollY,
+		anchor,
+		focus,
+	} = state;
+	const {
+		x: focusX,
+		y: focusY,
+	} = indexToGrid(code, focus);
+	const {
+		x: anchorX,
+		y: anchorY,
+	} = indexToGrid(code, anchor);
+	fillRect(x, y, w, h, COLOR.DARKBLUE);
+	if (anchor === focus) {
+		fillRect(x+focusX*fontWidth-scrollX, y+focusY*(fontHeight+1)-scrollY, fontWidth+1, fontHeight+1, COLOR.RED);
+	} else {
+		// TODO: Draw this selection better
+		fillRect(x+anchorX*fontWidth-scrollX, y+anchorY*(fontHeight+1)-scrollY, fontWidth+1, fontHeight+1, COLOR.GREEN);
+		fillRect(x+focusX*fontWidth-scrollX, y+focusY*(fontHeight+1)-scrollY, fontWidth+1, fontHeight+1, COLOR.YELLOW);
+	}
+	// TODO: Add syntax highlighting use "npm:js-tokens" maybe?
+	code.split("\n").forEach((line, i) => {
+		drawText(x-scrollX, 1+y+i*(fontHeight+1)-scrollY, line);
+	});
+}
+
+const update = () => {
+	const { focus, focusX, focusY} = state;
+	const keyboardString = getKeyboardString();
+	if (keyboardString) {
+		state.insertText(keyboardString);
+	}
+	// TODO: Handle ctrl-C, ctrl-V, ctrl-X, ctrl-Z
+	// TODO: Make ctrl-/ do commenting out (take inspiration from tab)
+
+	if (keyPressed(K.ENTER)) {
+		// TODO: Make this play nicely with indentation
+		state.insertText("\n");
+	}
+	if (keyPressed(K.TAB)) {
+		if (!shiftKeyDown()) {
+			if (state.isCollapsed()) {
+				state.insertText("\t");
+			} else {
+				state.indent("\t");
+			}
+		} else {
+			state.outdent(/^(\t| )/);
+		}
+	}
+	if (keyPressed(K.BACKSPACE)) {
+		state.backspace();
+	}
+	if (keyPressed(K.DELETE)) {
+		state.delete();
+	}
+	if (keyPressed(K.ARROW_RIGHT)) {
+		if (shiftKeyDown()) {
+			state.setFocus(focus+1);
+		} else {
+			state.setSelection(focus+1);
+		}
+	}
+	if (keyPressed(K.ARROW_LEFT)) {
+		if (shiftKeyDown()) {
+			state.setFocus(focus-1);
+		} else {
+			state.setSelection(focus-1);
+		}
+	}
+	if (keyPressed(K.ARROW_DOWN)) {
+		if (shiftKeyDown()) {
+			state.setFocus({x: focusX, y: focusY+1});
+		} else {
+			state.setSelection({x: focusX, y: focusY+1});
+		}
+	}
+	if (keyPressed(K.ARROW_UP)) {
+		if (shiftKeyDown()) {
+			state.setFocus({x: focusX, y: focusY-1});
+		} else {
+			state.setSelection({x: focusX, y: focusY-1});
+		}
+	}
+}
+
+const draw = () => {
+	clearScreen();
+	drawCodeField(state.code, 0, 8, 128, 112);
+}
+
+export const codetab = {
+	update,
+	draw,
+}
\ No newline at end of file
diff --git a/colors.ts b/colors.ts
index 0eb935f..9e781b1 100644
--- a/colors.ts
+++ b/colors.ts
@@ -1,4 +1,5 @@
 const colors = {
+	TRANSPARENT: [0, 0, 0],
 	BLACK: [0, 0, 0],
 	WHITE: [1, 1, 1],
 	RED: [1, 0, 0],
@@ -6,6 +7,7 @@ const colors = {
 	GREEN: [0, 1, 0],
 	BLUE: [0, 0, 1],
 	DARKBLUE: [0.1, 0.05, 0.4],
+	BROWN: [0.6, 0.5, 0.4],
 } as const;
 
 export const palette: Array<[number, number, number, number]> = Object.values(colors).map(val => [...val, 1]);
diff --git a/editmode.ts b/editmode.ts
index 0b41990..27ff2b9 100644
--- a/editmode.ts
+++ b/editmode.ts
@@ -1,221 +1,28 @@
 import { clearScreen, fillRect } from "./window.ts";
-import { fontWidth, fontHeight } from "./font.ts";
-import { drawText } from "./builtins.ts";
+import { codetab } from "./codetab.ts";
+import { spritetab } from "./spritetab.ts";
 import { COLOR } from "./colors.ts";
-import {getSheet, setSheet} from "./sheet.ts";
-import { K, getKeyboardString, keyPressed, shiftKeyDown } from "./keyboard.ts";
 
 // deno-lint-ignore prefer-const
-let tab: "code" | "sprite" | "map" | "sfx" | "music" = "code";
-
-// TODO: Make scrolling work
-const codeTabState = {
-	scrollX: 0,
-	scrollY: 0,
-	anchor: 0,
-	focus: 0,
-	get focusX() {return indexToGrid(this.code, this.focus).x;},
-	get focusY() {return indexToGrid(this.code, this.focus).y;},
-	get anchorX() {return indexToGrid(this.code, this.anchor).x;},
-	get anchorY() {return indexToGrid(this.code, this.anchor).y;},
-	isCollapsed() {
-		return this.anchor === this.focus;
-	},
-	clampInRange(n: number) {
-		return Math.max(0, Math.min(n, this.code.length))
-	},
-	setSelection(anchor: number | {x: number, y: number}, focus?: number | {x: number, y: number}) {
-		if (typeof anchor !== "number") {
-			anchor = gridToIndex(this.code, anchor.x, anchor.y);
-		}
-		focus = focus ?? anchor;
-		if (typeof focus !== "number") {
-			focus = gridToIndex(this.code, focus.x, focus.y);
-		}
-		this.anchor = this.clampInRange(anchor),
-		this.focus = this.clampInRange(focus);
-	},
-	setFocus(focus: number | {x: number, y: number}) {
-		if (typeof focus !== "number") {
-			focus = gridToIndex(this.code, focus.x, focus.y);
-		}
-		this.focus = this.clampInRange(focus);
-	},
-	insertText(text: string) {
-		const {code, anchor, focus} = this;
-		this.code = code.slice(0, Math.min(anchor, focus)) + text + code.slice(Math.max(anchor, focus));
-		this.setSelection(Math.min(anchor, focus) + text.length);
-	},
-	indent(indentString: string) {
-		const lines = this.code.split("\n");
-		const {focusX, focusY, anchorX, anchorY} = this;
-		const newLines = lines.map((line, i) => {
-			if (i >= Math.min(focusY, anchorY) && i <= Math.max(focusY, anchorY)) {
-				return indentString+line;
-			} else {
-				return line;
-			}
-		});
-		this.code = newLines.join("\n");
-		this.setSelection({x: anchorX+1, y: anchorY}, {x: focusX+1, y: focusY});
-	},
-	outdent(outdentRegex: RegExp) {
-		const lines = this.code.split("\n");
-		const {focusX, focusY, anchorX, anchorY} = this;
-		const newLines = lines.map((line, i) => {
-			const match = line.match(outdentRegex);
-			if (i >= Math.min(focusY, anchorY) && i <= Math.max(focusY, anchorY) && match) {
-				return line.slice(match[0].length);
-			} else {
-				return line;
-			}
-		});
-		this.code = newLines.join("\n");
-		this.setSelection({x: Math.max(0,anchorX-1), y: anchorY}, {x: Math.max(0,focusX-1), y: focusY});
-	},
-	backspace() {
-		const {code, focus} = this;
-		if (this.isCollapsed()) {
-			if (focus > 0) {
-				this.code = code.slice(0, focus-1) + code.slice(focus);
-				this.setSelection(focus-1);
-			}
-		} else {
-			this.insertText("");
-		}
-	},
-	delete() {
-		const {code, focus} = this;
-		if (this.isCollapsed()) {
-			if (focus < code.length) {
-				this.code = code.slice(0, focus) + code.slice(1+focus);
-			}
-		} else {
-			this.insertText("");
-		}
-	},
-	get code() {
-		return getSheet(0);
-	},
-	set code(val) {
-		setSheet(0, "code", val);
-	}
-}
-
-const indexToGrid = (str: string, index: number) => {
-	const linesUpTo = str.slice(0,index).split("\n");
-	return {
-		x: linesUpTo[linesUpTo.length-1].length,
-		y: linesUpTo.length - 1,
-	}
-}
-
-const gridToIndex = (str: string, x: number, y: number) => {
-	const lines = str.split("\n");
-	if (y < 0) {
-		return 0;
-	}
-	if (y >= lines.length) {
-		return str.length;
-	}
-	return lines.slice(0, y).join("\n").length+Math.min(x, lines[y].length)+1;
-}
-
-const drawCodeField = (code: string, x: number, y: number, w: number, h: number) => {
-	const {
-		scrollX,
-		scrollY,
-		anchor,
-		focus,
-	} = codeTabState;
-	const {
-		x: focusX,
-		y: focusY,
-	} = indexToGrid(code, focus);
-	const {
-		x: anchorX,
-		y: anchorY,
-	} = indexToGrid(code, anchor);
-	fillRect(x, y, w, h, COLOR.DARKBLUE);
-	if (anchor === focus) {
-		fillRect(x+focusX*fontWidth-scrollX, y+focusY*(fontHeight+1)-scrollY, fontWidth+1, fontHeight+1, COLOR.RED);
-	} else {
-		// TODO: Draw this selection better
-		fillRect(x+anchorX*fontWidth-scrollX, y+anchorY*(fontHeight+1)-scrollY, fontWidth+1, fontHeight+1, COLOR.GREEN);
-		fillRect(x+focusX*fontWidth-scrollX, y+focusY*(fontHeight+1)-scrollY, fontWidth+1, fontHeight+1, COLOR.YELLOW);
-	}
-	code.split("\n").forEach((line, i) => {
-		drawText(x-scrollX, 1+y+i*(fontHeight+1)-scrollY, line);
-	});
-}
+let tab: "code" | "sprite" | "map" | "sfx" | "music" = "sprite";
 
 const update = () => {
 	if (tab === "code") {
-		const { focus, focusX, focusY} = codeTabState;
-		const keyboardString = getKeyboardString();
-		if (keyboardString) {
-			codeTabState.insertText(keyboardString);
-		}
-		// TODO: Handle ctrl-C, ctrl-V, ctrl-X, ctrl-Z
-		// TODO: Make ctrl-/ do commenting out (take inspiration from tab)
-
-		if (keyPressed(K.ENTER)) {
-			// TODO: Make this play nicely with indentation
-			codeTabState.insertText("\n");
-		}
-		if (keyPressed(K.TAB)) {
-			if (!shiftKeyDown()) {
-				if (codeTabState.isCollapsed()) {
-					codeTabState.insertText("\t");
-				} else {
-					codeTabState.indent("\t");
-				}
-			} else {
-				codeTabState.outdent(/^(\t| )/);
-			}
-		}
-		if (keyPressed(K.BACKSPACE)) {
-			codeTabState.backspace();
-		}
-		if (keyPressed(K.DELETE)) {
-			codeTabState.delete();
-		}
-		if (keyPressed(K.ARROW_RIGHT)) {
-			if (shiftKeyDown()) {
-				codeTabState.setFocus(focus+1);
-			} else {
-				codeTabState.setSelection(focus+1);
-			}
-		}
-		if (keyPressed(K.ARROW_LEFT)) {
-			if (shiftKeyDown()) {
-				codeTabState.setFocus(focus-1);
-			} else {
-				codeTabState.setSelection(focus-1);
-			}
-		}
-		if (keyPressed(K.ARROW_DOWN)) {
-			if (shiftKeyDown()) {
-				codeTabState.setFocus({x: focusX, y: focusY+1});
-			} else {
-				codeTabState.setSelection({x: focusX, y: focusY+1});
-			}
-		}
-		if (keyPressed(K.ARROW_UP)) {
-			if (shiftKeyDown()) {
-				codeTabState.setFocus({x: focusX, y: focusY-1});
-			} else {
-				codeTabState.setSelection({x: focusX, y: focusY-1});
-			}
-		}
+		codetab.update();
+	} else if (tab === "sprite") {
+		spritetab.update();
 	}
 }
 
 const draw = () => {
 	clearScreen();
 	if (tab === "code") {
-		drawCodeField(getSheet(0), 0, 8, 128, 112);
+		codetab.draw();
+	} else if (tab === "sprite") {
+		spritetab.draw();
 	}
+	fillRect(0, 0, 128, 8, COLOR.RED);
+	fillRect(0, 120, 128, 8, COLOR.RED);
 }
 
 export const editmode = {
diff --git a/sheet.ts b/sheet.ts
index 95df41c..3995fd2 100644
--- a/sheet.ts
+++ b/sheet.ts
@@ -1,10 +1,18 @@
 import { getCart } from "./cart.ts";
 import { runCode, addToContext } from "./runcode.ts";
 
-export type SheetType = "code" | "spritesheet" | "map" | "sfx" | "patterns" | "fonts";
+// "code" | "spritesheet" | "map" | "sfx" | "patterns" | "fonts"
+export type Sheet = {
+	sheet_type: "code",
+	value: string,
+} | {
+	sheet_type: "spritesheet",
+	value: Array<Array<number>>,
+}
+export type SheetType = Sheet["sheet_type"];
 
 export const getSheet = (n: number) => {
-	return getCart()[n].value;
+	return getCart()[n];
 }
 
 // deno-lint-ignore no-explicit-any
@@ -13,8 +21,11 @@ export const setSheet = (n: number, type: SheetType, value: any) => {
 }
 
 export const codeSheet = (sheet: number) => {
-	const code = getSheet(sheet);
-	return runCode(code);
+	const {sheet_type, value} = getSheet(sheet);
+	if (sheet_type !== "code") {
+		throw "Trying to run a non-code sheet as code."
+	}
+	return runCode(value);
 }
 
 addToContext("code_sheet", codeSheet);
\ No newline at end of file
diff --git a/spritetab.ts b/spritetab.ts
new file mode 100644
index 0000000..77365e5
--- /dev/null
+++ b/spritetab.ts
@@ -0,0 +1,64 @@
+import { clearScreen, fillRect, setPixelColor } from "./window.ts";
+import { fontWidth, fontHeight } from "./font.ts";
+import { drawText, drawSprite } from "./builtins.ts";
+import { COLOR } from "./colors.ts";
+import {getSheet, setSheet} from "./sheet.ts";
+
+const state = {
+	selectedIndex: 0,
+	get sprites() {
+		const {sheet_type, value} = getSheet(2);
+		if (sheet_type !== "spritesheet") {
+			throw "Trying to use a non-sprite sheet as a spritesheet."
+		}
+		return value;
+	},
+	set sprites(val) {
+		setSheet(0, "spritesheet", val);
+	}
+}
+
+const update = () => {
+}
+
+const draw = () => {
+	const {sprites, selectedIndex} = state;
+	clearScreen();
+	fillRect(0, 8, 128, 112, COLOR.BROWN);
+	// Draw the palette
+	const paletteX = 88;
+	const paletteY = 12;
+	fillRect(paletteX-1, paletteY-1, 32+2, 32+2, COLOR.BLACK);
+	Object.keys(COLOR).forEach((name, i) => {
+		const swatchX = paletteX+8*(i%4);
+		const swatchY = paletteY+8*Math.floor(i/4);
+		fillRect(swatchX, swatchY, 8, 8, COLOR[name as keyof typeof COLOR]);
+		if (i === 0) {
+			// transparent
+			Array(64).fill(0).map((_z, j) => {
+				const jx = j%8;
+				const jy = Math.floor(j/8);
+				setPixelColor(swatchX+jx, swatchY+jy, (jx+jy)%2 ? COLOR.BLACK : COLOR.WHITE);
+			})
+		}
+	});
+	// Draw the current sprite
+	const spriteX = 8;
+	const spriteY = 12;
+	fillRect(spriteX-1, spriteY-1, 64+2, 64+2, COLOR.BLACK);
+	sprites[selectedIndex].forEach((pix, i) => {
+		fillRect(spriteX+8*(i%8), spriteY+8*Math.floor(i/8), 8, 8, pix);
+	});
+	// Draw the spritesheet
+	const sheetX = 0;
+	const sheetY = 88;
+	fillRect(sheetX, sheetY-1, 128, 64+1, COLOR.BLACK);
+	sprites.forEach((_sprite, i) => {
+		drawSprite(sheetX+8*(i%16), sheetY+8*Math.floor(i/16), i);
+	});
+}
+
+export const spritetab = {
+	update,
+	draw,
+}
\ No newline at end of file