diff --git a/appData/templates/gbs2/project.gbsproj b/appData/templates/gbs2/project.gbsproj index 5cec40d20..0b0207346 100644 --- a/appData/templates/gbs2/project.gbsproj +++ b/appData/templates/gbs2/project.gbsproj @@ -4542,12 +4542,12 @@ "type": "number", "value": 0 }, - "toTileIndex": { - "type": "number", - "value": 3 - }, "variable": "L0", - "tilesetId": "4991cc19-85b6-45f9-bef6-39496dc2c62a" + "tilesetId": "4991cc19-85b6-45f9-bef6-39496dc2c62a", + "frames": { + "type": "number", + "value": 4 + } }, "id": "fb3e9a6c-a491-457e-8c93-d0971516643a" }, @@ -4560,12 +4560,12 @@ "type": "number", "value": 4 }, - "toTileIndex": { - "type": "number", - "value": 7 - }, "variable": "L1", - "tilesetId": "4991cc19-85b6-45f9-bef6-39496dc2c62a" + "tilesetId": "4991cc19-85b6-45f9-bef6-39496dc2c62a", + "frames": { + "type": "number", + "value": 4 + } }, "id": "418dc58d-8057-41e3-ada5-96e620d889f5" }, @@ -4578,12 +4578,12 @@ "type": "number", "value": 0 }, - "toTileIndex": { - "type": "number", - "value": 3 - }, "variable": "L2", - "tilesetId": "d7042527-d20e-486d-93e6-66b9397df510" + "tilesetId": "d7042527-d20e-486d-93e6-66b9397df510", + "frames": { + "type": "number", + "value": 4 + } }, "id": "77f7c1aa-6c94-4b45-a05a-3914ec07e588" } diff --git a/src/components/forms/TilesetSelect.tsx b/src/components/forms/TilesetSelect.tsx index ed26ea113..72f68138c 100644 --- a/src/components/forms/TilesetSelect.tsx +++ b/src/components/forms/TilesetSelect.tsx @@ -1,7 +1,11 @@ import React, { FC, useEffect, useState } from "react"; import { useAppSelector } from "store/hooks"; import { tilesetSelectors } from "store/features/entities/entitiesState"; -import { Tileset } from "shared/lib/entities/entitiesTypes"; +import { + UnitType, + Tileset, + GridUnitType, +} from "shared/lib/entities/entitiesTypes"; import { Option, Select, @@ -10,12 +14,17 @@ import { SelectCommonProps, } from "ui/form/Select"; import { TileCanvas } from "components/world/TileCanvas"; +import { UnitsSelectButtonInputOverlay } from "components/forms/UnitsSelectButtonInputOverlay"; +import styled from "styled-components"; interface TilesetSelectProps extends SelectCommonProps { name: string; value?: string; tileIndex?: number; onChange?: (newId: string) => void; + units?: UnitType; + unitsAllowed?: UnitType[]; + onChangeUnits?: (newUnits: UnitType) => void; optional?: boolean; optionalLabel?: string; optionalDefaultTilesetId?: string; @@ -25,10 +34,17 @@ interface TilesetOption extends Option { tileset: Tileset; } +const Wrapper = styled.div` + position: relative; +`; + export const TilesetSelect: FC = ({ value, tileIndex, onChange, + units, + unitsAllowed, + onChangeUnits, optional, optionalLabel, optionalDefaultTilesetId, @@ -105,33 +121,52 @@ export const TilesetSelect: FC = ({ }; return ( - { + return ( + + } + > + {option.label} + + ); + }} + components={{ + SingleValue: () => ( + + } + > + {currentValue?.label} + + ), + }} + {...selectProps} + /> + {units && ( + + )} + ); }; diff --git a/src/components/forms/UnitsSelectButtonInputOverlay.tsx b/src/components/forms/UnitsSelectButtonInputOverlay.tsx index 15b5e1c2d..892240378 100644 --- a/src/components/forms/UnitsSelectButtonInputOverlay.tsx +++ b/src/components/forms/UnitsSelectButtonInputOverlay.tsx @@ -69,6 +69,8 @@ export const UnitsSelectButtonInputOverlay = ({ pixels: l10n("FIELD_PIXELS"), time: l10n("FIELD_SECONDS"), frames: l10n("FIELD_FRAMES"), + "8px": "8px", + "16px": "16px", }), [] ); @@ -79,6 +81,8 @@ export const UnitsSelectButtonInputOverlay = ({ pixels: l10n("FIELD_PIXELS_SHORT").toLocaleLowerCase(), time: l10n("FIELD_SECONDS").toLocaleLowerCase(), frames: l10n("FIELD_FRAMES").toLocaleLowerCase(), + "8px": "8px", + "16px": "16px", }), [] ); diff --git a/src/components/script/ScriptEventFormInput.tsx b/src/components/script/ScriptEventFormInput.tsx index 0c5ffa6ec..190d4419e 100644 --- a/src/components/script/ScriptEventFormInput.tsx +++ b/src/components/script/ScriptEventFormInput.tsx @@ -631,6 +631,11 @@ const ScriptEventFormInput = ({ value={String(value)} onChange={onChangeField} tileIndex={argValue(args.tileIndex) as number | undefined} + units={ + (args[field.unitsField || ""] || field.unitsDefault) as UnitType + } + unitsAllowed={field.unitsAllowed} + onChangeUnits={onChangeUnits} /> ); diff --git a/src/components/world/SceneEventHelper.tsx b/src/components/world/SceneEventHelper.tsx index e5b883e1f..2b9912736 100644 --- a/src/components/world/SceneEventHelper.tsx +++ b/src/components/world/SceneEventHelper.tsx @@ -134,8 +134,22 @@ export const SceneEventHelper: FC = ({ scene }) => { argValue(event.args?.[scriptEventDef.helper.y]), 0 ); - const tileWidth = scriptEventDef.helper.tileWidth ?? 1; - const tileHeight = scriptEventDef.helper.tileHeight ?? 1; + const tileSize = ensureMaybeString( + argValue(event.args?.[scriptEventDef.helper.tileSize ?? ""]), + "" + ); + let tileWidth = 1; + let tileHeight = 1; + if (tileSize === "16px") { + tileWidth = 2; + tileHeight = 2; + } + if (scriptEventDef.helper.tileWidth) { + tileWidth = scriptEventDef.helper.tileWidth; + } + if (scriptEventDef.helper.tileHeight) { + tileHeight = scriptEventDef.helper.tileHeight; + } if (x === undefined && y === undefined) { return
; } diff --git a/src/components/world/TileCanvas.tsx b/src/components/world/TileCanvas.tsx index 2dc00b196..c7e7dc821 100644 --- a/src/components/world/TileCanvas.tsx +++ b/src/components/world/TileCanvas.tsx @@ -1,20 +1,27 @@ import React, { useCallback, useEffect, useState } from "react"; import { useAppSelector } from "store/hooks"; -import { DMG_PALETTE } from "consts"; +import { DMG_PALETTE, TILE_SIZE } from "consts"; import { tilesetSelectors } from "store/features/entities/entitiesState"; import TilePreviewWorker, { TilePreviewResult } from "./TilePreview.worker"; import { assetURL } from "shared/lib/helpers/assets"; +import { GridUnitType } from "shared/lib/entities/entitiesTypes"; interface TileCanvasProps { tilesetId: string; tileIndex?: number; + tileSize?: GridUnitType; } const worker = new TilePreviewWorker(); -export const TileCanvas = ({ tilesetId, tileIndex }: TileCanvasProps) => { - const width = 8; - const height = 8; +export const TileCanvas = ({ + tilesetId, + tileIndex, + tileSize, +}: TileCanvasProps) => { + const size = tileSize === "16px" ? 2 : 1; + const width = TILE_SIZE * size; + const height = TILE_SIZE * size; const [workerId] = useState(Math.random()); const canvasRef = React.useRef(null); const tileset = useAppSelector( @@ -40,7 +47,7 @@ export const TileCanvas = ({ tilesetId, tileIndex }: TileCanvasProps) => { ctx.drawImage(offscreenCanvas, 0, 0); } }, - [tileset, workerId] + [height, tileset, width, workerId] ); useEffect(() => { @@ -65,8 +72,9 @@ export const TileCanvas = ({ tilesetId, tileIndex }: TileCanvasProps) => { src: tilesetURL, palette: DMG_PALETTE.colors, tileIndex, + tileSize, }); - }, [canvasRef, tileIndex, tileset, workerId]); + }, [canvasRef, tileIndex, tileSize, tileset, workerId]); return ( = {}; -const TILE_SIZE = 8; workerCtx.onmessage = async (evt) => { const id = evt.data.id; const src = evt.data.src; const tileIndex = evt.data.tileIndex ?? 0; + const tileSize = evt.data.tileSize === "16px" ? TILE_SIZE * 2 : TILE_SIZE; const palette = evt.data.palette; const paletteRGB = palette.map(hex2GBCrgb); @@ -53,7 +52,7 @@ workerCtx.onmessage = async (evt) => { const imgblob = await fetch(src).then((r) => r.blob()); img = await createImageBitmap(imgblob); - canvas = new OffscreenCanvas(TILE_SIZE, TILE_SIZE); + canvas = new OffscreenCanvas(tileSize, tileSize); const tmpCtx = canvas.getContext("2d"); if (!tmpCtx) { @@ -68,38 +67,36 @@ workerCtx.onmessage = async (evt) => { }; } - const width = img.width; - const height = img.height; - const tileWidth = Math.floor(width / TILE_SIZE); + const tileWidth = Math.floor(img.width / tileSize); - const offsetX = TILE_SIZE * (tileIndex % tileWidth); - const offsetY = TILE_SIZE * Math.floor(tileIndex / tileWidth); + const offsetX = tileSize * (tileIndex % tileWidth); + const offsetY = tileSize * Math.floor(tileIndex / tileWidth); - canvas.width = TILE_SIZE; - canvas.height = TILE_SIZE; + canvas.width = tileSize; + canvas.height = tileSize; ctx.drawImage( img, offsetX, offsetY, - TILE_SIZE, - TILE_SIZE, + tileSize, + tileSize, 0, 0, - TILE_SIZE, - TILE_SIZE + tileSize, + tileSize ); - const imageData = ctx.getImageData(0, 0, TILE_SIZE, TILE_SIZE); + const imageData = ctx.getImageData(0, 0, tileSize, tileSize); const data = imageData.data; const p1X = 0; - const p2X = 8; + const p2X = tileSize; const p1Y = 0; - const p2Y = 8; + const p2Y = tileSize; for (let pX = p1X; pX < p2X; pX++) { for (let pY = p1Y; pY < p2Y; pY++) { - const index = (pX + pY * width) * 4; + const index = (pX + pY * tileSize) * 4; const colorIndex = indexColour(data[index + 1]); const color = paletteRGB[colorIndex]; data[index] = color.r; @@ -112,7 +109,7 @@ workerCtx.onmessage = async (evt) => { ctx.putImageData(imageData, 0, 0); const canvasImage = canvas.transferToImageBitmap(); - workerCtx.postMessage({ id, width, height, canvasImage }, [canvasImage]); + workerCtx.postMessage({ id, canvasImage }, [canvasImage]); }; // ----------------------------------------------------------------- diff --git a/src/lang/en.json b/src/lang/en.json index f02865d47..02c58c396 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -757,6 +757,8 @@ "FIELD_TO_TILE_DESC": "The ending tile offset inside tileset", "FIELD_STATE_VARIABLE": "State Variable", "FIELD_STATE_VARIABLE_DESC": "A variable to store the current state of this event", + "FIELD_ANIMATION_FRAMES": "Animation Frames", + "FIELD_ANIMATION_FRAMES_DESC": "The number of animation frames to cycle through.", "// 7": "Asset Viewer ---------------------------------------------", "ASSET_SEARCH": "Search...", diff --git a/src/lib/compiler/generateGBVMData.ts b/src/lib/compiler/generateGBVMData.ts index c934091d8..b01bb4f10 100644 --- a/src/lib/compiler/generateGBVMData.ts +++ b/src/lib/compiler/generateGBVMData.ts @@ -55,6 +55,8 @@ export interface PrecompiledTilesetData { id: string; symbol: string; data: number[] | Uint8Array; + width: number; + height: number; } export interface PrecompiledTileData { diff --git a/src/lib/compiler/scriptBuilder.ts b/src/lib/compiler/scriptBuilder.ts index d28681de1..65ee92ed6 100644 --- a/src/lib/compiler/scriptBuilder.ts +++ b/src/lib/compiler/scriptBuilder.ts @@ -1841,6 +1841,22 @@ extern void __mute_mask_${symbol}; this._addCmd(".CGB_PAL", r1, g1, b1, r2, g2, b2, r3, g3, b3, r4, g4, b4); }; + _replaceTile = ( + addr: ScriptBuilderStackVariable, + symbol: string, + tileIndex: ScriptBuilderStackVariable, + numTiles: number + ) => { + this._addCmd( + "VM_REPLACE_TILE", + addr, + `___bank_${symbol}`, + `_${symbol}`, + tileIndex, + numTiles + ); + }; + _replaceTileXY = ( x: number, y: number, @@ -1857,6 +1873,10 @@ extern void __mute_mask_${symbol}; ); }; + _getTileXY = (addr: ScriptBuilderStackVariable, x: number, y: number) => { + this._addCmd("VM_GET_TILE_XY", addr, x, y); + }; + _callFar = (symbol: string, argsLen: number) => { this._addCmd("VM_CALL_FAR", `___bank_${symbol}`, `_${symbol}`); if (argsLen > 0) { @@ -3810,6 +3830,17 @@ extern void __mute_mask_${symbol}; this._addNL(); }; + variableAdd = (variable: ScriptBuilderVariable, value: number) => { + this._addComment("Variable Increment By " + value); + this._rpn() // + .refVariable(variable) + .int8(value) + .operator(".ADD") + .refSetVariable(variable) + .stop(); + this._addNL(); + }; + variableSetToTrue = (variable: string) => { this._addComment("Variable Set To True"); this._setVariableConst(variable, 1); @@ -4029,7 +4060,13 @@ extern void __mute_mask_${symbol}; return defaultVariable; }; - localVariableFromUnion = (unionValue: ScriptBuilderUnionValue) => { + localVariableFromUnion = ( + unionValue: ScriptBuilderUnionValue + ): string | ScriptBuilderFunctionArg => { + if (!unionValue) { + // Guard undefined values + return this.localVariableFromUnion({ type: "number", value: 0 }); + } if (unionValue.type === "variable") { return unionValue.value; } @@ -4162,7 +4199,8 @@ extern void __mute_mask_${symbol}; x: number, y: number, tilesetId: string, - tileIndex: number + tileIndex: number, + tileSize: "8px" | "16px" ) => { const { tilesets } = this.options; const tileset = tilesets.find((t) => t.id === tilesetId) ?? tilesets[0]; @@ -4172,7 +4210,36 @@ extern void __mute_mask_${symbol}; this._addComment(`Replace Tile XY`); this._stackPushConst(tileIndex); - this._replaceTileXY(x, y, tileset.symbol, ".ARG0"); + if (tileSize === "16px") { + // Top left tile + this._replaceTileXY(x, y, tileset.symbol, ".ARG0"); + // Top right tile + this._rpn() // + .ref(".ARG0") + .int8(1) + .operator(".ADD") + .refSet(".ARG0") + .stop(); + this._replaceTileXY(x + 1, y, tileset.symbol, ".ARG0"); + // Bottom right tile + this._rpn() // + .ref(".ARG0") + .int8(tileset.width) + .operator(".ADD") + .refSet(".ARG0") + .stop(); + this._replaceTileXY(x + 1, y + 1, tileset.symbol, ".ARG0"); + // Bottom left tile + this._rpn() // + .ref(".ARG0") + .int8(1) + .operator(".SUB") + .refSet(".ARG0") + .stop(); + this._replaceTileXY(x, y + 1, tileset.symbol, ".ARG0"); + } else { + this._replaceTileXY(x, y, tileset.symbol, ".ARG0"); + } this._stackPop(1); }; @@ -4180,7 +4247,8 @@ extern void __mute_mask_${symbol}; x: number, y: number, tilesetId: string, - tileIndexVariable: string + tileIndexVariable: string, + tileSize: "8px" | "16px" ) => { const { tilesets } = this.options; const tileset = tilesets.find((t) => t.id === tilesetId) ?? tilesets[0]; @@ -4193,11 +4261,40 @@ extern void __mute_mask_${symbol}; this._addComment(`Replace Tile XY`); if (this._isIndirectVariable(tileIndexVariable)) { this._stackPushInd(variableAlias); + } else { + this._stackPush(variableAlias); + } + if (tileSize === "16px") { + // Top left tile this._replaceTileXY(x, y, tileset.symbol, ".ARG0"); - this._stackPop(1); + // Top right tile + this._rpn() // + .ref(".ARG0") + .int8(1) + .operator(".ADD") + .refSet(".ARG0") + .stop(); + this._replaceTileXY(x + 1, y, tileset.symbol, ".ARG0"); + // Bottom right tile + this._rpn() // + .ref(".ARG0") + .int8(tileset.width) + .operator(".ADD") + .refSet(".ARG0") + .stop(); + this._replaceTileXY(x + 1, y + 1, tileset.symbol, ".ARG0"); + // Bottom left tile + this._rpn() // + .ref(".ARG0") + .int8(1) + .operator(".SUB") + .refSet(".ARG0") + .stop(); + this._replaceTileXY(x, y + 1, tileset.symbol, ".ARG0"); } else { - this._replaceTileXY(x, y, tileset.symbol, variableAlias); + this._replaceTileXY(x, y, tileset.symbol, ".ARG0"); } + this._stackPop(1); }; // -------------------------------------------------------------------------- diff --git a/src/lib/events/eventReplaceTileXY.js b/src/lib/events/eventReplaceTileXY.js index 9c80998f8..5dd330d3f 100644 --- a/src/lib/events/eventReplaceTileXY.js +++ b/src/lib/events/eventReplaceTileXY.js @@ -37,26 +37,33 @@ const fields = [ ], }, { - key: "tilesetId", - type: "tileset", - label: l10n("FIELD_TILESET"), - description: l10n("FIELD_TILESET_DESC"), - defaultValue: "LAST_TILESET", - }, - { - key: "tileIndex", - label: l10n("FIELD_TILE"), - description: l10n("FIELD_TILE_DESC"), - type: "union", - types: ["number", "variable", "property"], - defaultType: "number", - min: 0, - width: "50%", - defaultValue: { - number: 0, - variable: "LAST_VARIABLE", - property: "$self$:xpos", - }, + type: "group", + fields: [ + { + key: "tilesetId", + type: "tileset", + label: l10n("FIELD_TILESET"), + description: l10n("FIELD_TILESET_DESC"), + defaultValue: "LAST_TILESET", + unitsField: "tileSize", + unitsDefault: "8px", + unitsAllowed: ["8px", "16px"], + }, + { + key: "tileIndex", + label: l10n("FIELD_TILE"), + description: l10n("FIELD_TILE_DESC"), + type: "union", + types: ["number", "variable", "property"], + defaultType: "number", + min: 0, + defaultValue: { + number: 0, + variable: "LAST_VARIABLE", + property: "$self$:xpos", + }, + }, + ], }, ]; @@ -68,10 +75,22 @@ const compile = (input, helpers) => { markLocalsUsed, } = helpers; if (input.tileIndex.type === "number") { - replaceTileXY(input.x, input.y, input.tilesetId, input.tileIndex.value); + replaceTileXY( + input.x, + input.y, + input.tilesetId, + input.tileIndex.value, + input.tileSize + ); } else { const indexVar = localVariableFromUnion(input.tileIndex); - replaceTileXYVariable(input.x, input.y, input.tilesetId, indexVar); + replaceTileXYVariable( + input.x, + input.y, + input.tilesetId, + indexVar, + input.tileSize + ); markLocalsUsed(indexVar); } }; @@ -87,5 +106,6 @@ module.exports = { type: "position", x: "x", y: "y", + tileSize: "tileSize", }, }; diff --git a/src/lib/events/eventReplaceTileXYSequence.js b/src/lib/events/eventReplaceTileXYSequence.js index 84322da53..e7c93efbc 100644 --- a/src/lib/events/eventReplaceTileXYSequence.js +++ b/src/lib/events/eventReplaceTileXYSequence.js @@ -42,6 +42,9 @@ const fields = [ label: l10n("FIELD_TILESET"), description: l10n("FIELD_TILESET_DESC"), defaultValue: "LAST_TILESET", + unitsField: "tileSize", + unitsDefault: "8px", + unitsAllowed: ["8px", "16px"], }, { type: "group", @@ -62,16 +65,16 @@ const fields = [ }, }, { - key: "toTileIndex", - label: l10n("FIELD_TO_TILE"), - description: l10n("FIELD_TO_TILE_DESC"), + key: "frames", + label: l10n("FIELD_ANIMATION_FRAMES"), + description: l10n("FIELD_ANIMATION_FRAMES_DESC"), type: "union", types: ["number", "variable", "property"], defaultType: "number", - min: 0, + min: 1, width: "50%", defaultValue: { - number: 0, + number: 1, variable: "LAST_VARIABLE", property: "$self$:xpos", }, @@ -93,25 +96,67 @@ const compile = (input, helpers) => { localVariableFromUnion, ifVariableCompare, variableInc, + variableAdd, variableSetToUnionValue, markLocalsUsed, + _declareLocal, + _rpn, + _addComment, + _addNL, } = helpers; const fromVar = localVariableFromUnion(input.tileIndex); - const toVar = localVariableFromUnion(input.toTileIndex); + const framesVar = localVariableFromUnion(input.frames); + const toVar = _declareLocal("to_var", 1, true); + + // Calculate max frame + _addComment("Calculate max frame"); + if (input.tileSize === "16px") { + _rpn() // + .refVariable(fromVar) + .refVariable(framesVar) + .int8(1) + .operator(".SUB") + .int8(2) + .operator(".MUL") + .operator(".ADD") + .refSet(toVar) + .stop(); + } else { + _rpn() // + .refVariable(fromVar) + .refVariable(framesVar) + .int8(1) + .operator(".SUB") + .operator(".ADD") + .refSet(toVar) + .stop(); + } + _addNL(); ifVariableCompare(input.variable, ".LT", fromVar, () => { variableSetToUnionValue(input.variable, input.tileIndex); }); - replaceTileXYVariable(input.x, input.y, input.tilesetId, input.variable); - variableInc(input.variable); + replaceTileXYVariable( + input.x, + input.y, + input.tilesetId, + input.variable, + input.tileSize + ); + + if (input.tileSize === "16px") { + variableAdd(input.variable, 2); + } else { + variableInc(input.variable); + } ifVariableCompare(input.variable, ".GT", toVar, () => { variableSetToUnionValue(input.variable, input.tileIndex); }); - markLocalsUsed(fromVar, toVar); + markLocalsUsed(fromVar, framesVar, toVar); }; module.exports = { @@ -125,5 +170,6 @@ module.exports = { type: "position", x: "x", y: "y", + tileSize: "tileSize", }, }; diff --git a/src/lib/project/loadScriptEventHandlers.ts b/src/lib/project/loadScriptEventHandlers.ts index 707c9b6eb..aff487c43 100644 --- a/src/lib/project/loadScriptEventHandlers.ts +++ b/src/lib/project/loadScriptEventHandlers.ts @@ -21,6 +21,7 @@ export type ScriptEventHelperDef = x: string; y: string; units?: string; + tileSize?: string; tileWidth?: number; tileHeight?: number; } diff --git a/src/shared/lib/entities/entitiesTypes.ts b/src/shared/lib/entities/entitiesTypes.ts index dbf689b65..c75bad675 100644 --- a/src/shared/lib/entities/entitiesTypes.ts +++ b/src/shared/lib/entities/entitiesTypes.ts @@ -470,11 +470,17 @@ export interface ScriptEventFieldCondition { export const distanceUnitTypes = ["tiles", "pixels"] as const; export const timeUnitTypes = ["time", "frames"] as const; -export const unitTypes = [...distanceUnitTypes, ...timeUnitTypes] as const; +export const gridUnitTypes = ["8px", "16px"] as const; +export const unitTypes = [ + ...distanceUnitTypes, + ...timeUnitTypes, + ...gridUnitTypes, +] as const; export type UnitType = typeof unitTypes[number]; export type DistanceUnitType = typeof distanceUnitTypes[number]; export type TimeUnitType = typeof timeUnitTypes[number]; +export type GridUnitType = typeof gridUnitTypes[number]; export const movementTypes = ["horizontal", "vertical", "diagonal"] as const; export type MovementType = typeof movementTypes[number];