diff --git a/inspector/codec.js b/inspector/codec.js
index 34541541..9f994ed0 100644
--- a/inspector/codec.js
+++ b/inspector/codec.js
@@ -362,7 +362,7 @@ const encodeWalkto = ({ fromSide, offset }) => {
return encodeSide(fromSide) | (offset & 0xfc)
}
-const decodeAnimations = (data, startEndTableOff, firstCelOff, stateCount) => {
+const decodeAnimations = (data, startEndTableOff, nextBlockOff, stateCount) => {
const animations = []
// The prop structure also does not encode a count for how many frames there are, so we simply
// stop parsing once we find one that doesn't make sense.
@@ -373,7 +373,7 @@ const decodeAnimations = (data, startEndTableOff, firstCelOff, stateCount) => {
// heuristic rather than failing to parse any animation data.
// It's possible for there to be no frames, which is represented by an offset of 0 (no_animation)
if (startEndTableOff != 0) {
- for (let frameOff = startEndTableOff; (startEndTableOff > firstCelOff) || (frameOff < firstCelOff); frameOff += 2) {
+ for (let frameOff = startEndTableOff; (startEndTableOff > nextBlockOff) || (frameOff < nextBlockOff); frameOff += 2) {
// each animation is two bytes: the starting state, and the ending state
// the first byte can have its high bit set to indicate that the animation should cycle
const cycle = (data.getUint8(frameOff) & 0x80) != 0
@@ -388,12 +388,28 @@ const decodeAnimations = (data, startEndTableOff, firstCelOff, stateCount) => {
return animations
}
+const decodeContentsXY = (data, off, nextBlockOff) => {
+ const offsets = []
+ // The prop structure doesn't encode the capacity of an open container - the only way to know
+ // how big this block is without digging into other files is to use the offset of the first cel as
+ // a boundary, as, in practice, this should be true of all existing props. (An object's capacity is
+ // defined in beta.mud, but that's per-object, not per-image. Building that association would be more
+ // complex than is needed here.)
+ // Non-containers and closed containers will have 0 here (no_cont).
+ if (off != 0) {
+ for (let xyOff = off; xyOff < nextBlockOff; xyOff += 2) {
+ offsets.push({ x: data.getInt8(xyOff), y: data.getInt8(xyOff + 1) })
+ }
+ }
+ return offsets
+}
+
export const decodeProp = (data) => {
const prop = {
data: data,
howHeld: decodeHowHeld(data.getUint8(0)),
colorBitmask: data.getUint8(1),
- containerXYOff: data.getUint8(3), // TODO: parse this when nonzero
+ contentsInFront: (data.getUint8(3) & 0x80) == 0,
walkto: { left: decodeWalkto(data.getUint8(4)), right: decodeWalkto(data.getUint8(5)), yoff: data.getInt8(6) },
celmasks: [],
cels: []
@@ -426,7 +442,9 @@ export const decodeProp = (data) => {
prop.cels.push(decodeCel(new DataView(data.buffer, celOff), (prop.colorBitmask & celbit) != 0))
allCelsMask = (allCelsMask << 1) & 0xff
}
- prop.animations = decodeAnimations(data, graphicStateOff, firstCelOff, stateCount)
+ const contentsXYOff = data.getUint8(3) & 0x7f
+ prop.animations = decodeAnimations(data, graphicStateOff, contentsXYOff == 0 ? firstCelOff : contentsXYOff, stateCount)
+ prop.contentsXY = decodeContentsXY(data, contentsXYOff, firstCelOff)
return prop
}
diff --git a/inspector/navigate.html b/inspector/navigate.html
index 6f20f251..fb54339a 100644
--- a/inspector/navigate.html
+++ b/inspector/navigate.html
@@ -20,7 +20,7 @@
import { html } from "./view.js"
import { h, render } from "preact"
import { useState, useId, useCallback, useMemo } from "preact/hooks"
- import { regionView, regionSearch, itemView } from "./region.js"
+ import { regionView, regionSearch, itemView, standaloneItemView } from "./region.js"
import { contextMap, useJson, useHabitatJson } from "./data.js"
import { direction } from "./view.js"
import { Scale } from "./render.js"
@@ -40,7 +40,14 @@
if (filename) {
const obj = useHabitatJson(filename).find(o => o.ref == dir)
if (obj) {
- return html`through <${Scale.provider} value="1"><${itemView} object=${obj} standalone="true"/>/>
`
+ return html`
+ through
+
+ <${Scale.provider} value="1">
+ <${itemView} object=${obj} viewer=${standaloneItemView}/>
+ />
+
+ `
}
}
}
diff --git a/inspector/region.js b/inspector/region.js
index 80a23129..2fc4b637 100644
--- a/inspector/region.js
+++ b/inspector/region.js
@@ -138,11 +138,12 @@ export const propFromMod = (mod, ref) => {
}
return fnAugment ? useTrap(ref, image.filename, fnAugment) : useBinary(image.filename, decodeProp, null)
}
-const propLocationFromMod = (prop, mod) => {
- const x = (mod.x > 208 ? signedByte(mod.x) : mod.x) / 4
- const y = mod.y % 128
- const zIndex = mod.y > 127 ? (128 + (256 - mod.y)) : mod.y
- return [prop.isTrap ? 0 : x, y, zIndex]
+
+const propLocationFromObjectXY = (prop, modX, modY) => {
+ const x = (modX > 208 ? signedByte(modX) : modX) / 4
+ const y = modY % 128
+ const zIndex = (modY > 127 ? (128 + (256 - modY)) : modY) * 2
+ return [prop.isTrap ? 0 : x, y, prop.contentsInFront ? zIndex : zIndex + 1]
}
const colorsFromMod = (mod) => {
@@ -158,60 +159,109 @@ const colorsFromMod = (mod) => {
return colors
}
-const unwrappedItemView = ({ object, standalone }) => {
- const scale = useContext(Scale)
- const mod = object.mods[0]
- const prop = propFromMod(mod, object.ref)
- if (!prop) {
- return null
- }
- const flipHorizontal = ((mod.orientation ?? 0) & 0x01) != 0
- const grState = mod.gr_state ?? 0
- const regionSpace = { minX: 0, minY: 0, maxX: 160 / 4, maxY: 127 }
+const propFramesFromMod = (prop, mod) => {
const colors = colorsFromMod(mod)
const frames = useMemo(() => {
+ const flipHorizontal = ((mod.orientation ?? 0) & 0x01) != 0
+ const grState = mod.gr_state ?? 0
if (prop.animations.length > 0) {
return framesFromPropAnimation(prop.animations[grState], prop, { colors, flipHorizontal })
} else {
return [frameFromCels(celsFromMask(prop, prop.celmasks[grState]), { colors, flipHorizontal })]
}
}, [prop, mod, colors.charset])
-
- const [propX, propY, propZ] = propLocationFromMod(prop, mod)
- const objectSpace = translateSpace(compositeSpaces(frames), propX, propY)
- const [x, y] = topLeftCanvasOffset(regionSpace, objectSpace)
- const style = !standalone ? `position: absolute; left: ${x * scale}px; top: ${y * scale}px; z-index: ${propZ}` : ""
- const image = html`<${animatedDiv} frames=${frames}/>`
- const connection = mod.connection && contextMap()[mod.connection]
- return html`
-
- ${connection ? html`
${image}` : image}
-
`
+ return frames
}
export const itemView = (props) => {
return html`
<${catcher} filename=${props.object.ref}>
- <${unwrappedItemView} ...${props}/>
+ <${props.viewer} ...${props}/>
+ />`
+}
+
+export const standaloneItemView = ({ object }) => {
+ const mod = object.mods[0]
+ const prop = propFromMod(mod, object.ref)
+ if (!prop) {
+ return null
+ }
+ return html`<${animatedDiv} frames=${propFramesFromMod(prop, mod)}/>`
+}
+
+export const positionedInRegion = ({ space, z, children }) => {
+ const scale = useContext(Scale)
+ const regionSpace = { minX: 0, minY: 0, maxX: 160 / 4, maxY: 127 }
+ const [x, y] = topLeftCanvasOffset(regionSpace, space)
+ const style =`position: absolute; left: ${x * scale}px; top: ${y * scale}px; z-index: ${z}`
+ return html`${children}
`
+}
+
+export const containedItemView = ({ object, containerProp, containerMod, containerSpace }) => {
+ const mod = object.mods[0]
+ const prop = propFromMod(mod, object.ref)
+ if (!prop || containerProp.contentsXY.length < mod.y) {
+ return null
+ }
+ const [containerX, containerY, containerZ] = propLocationFromObjectXY(containerProp, containerMod.x, containerMod.y)
+ const { x: offsetX, y: offsetY } = containerProp.contentsXY[mod.y]
+ const flipHorizontal = ((mod.orientation ?? 0) & 0x01) != 0
+ // offsets are relative to `cel_x_origin` / `cel_y_origin`, which is in "habitat space" but with
+ // the y axis inverted (see render.m:115-121)
+ const frames = propFramesFromMod(prop, mod)
+ const frameSpace = compositeSpaces(frames)
+ // if the contents are drawn in front, the container has its origin offset by the offset of its first cel.
+ const originX = containerProp.contentsInFront ? containerSpace.xOrigin + 2 : 0
+ const originY = containerProp.contentsInFront ? containerSpace.yOrigin : 0
+ const x = containerX + (flipHorizontal ? -originX + offsetX : originX - offsetX)
+ const y = containerY - (offsetY + originY)
+ const z = containerProp.contentsInFront ? containerZ + 1 : containerZ - 1
+ const objectSpace = translateSpace(frameSpace, x, y)
+ return html`
+ <${positionedInRegion} space=${objectSpace} z=${z}>
+ <${animatedDiv} frames=${frames}/>
/>`
}
+export const itemInteraction = ({ mod, children }) => {
+ const connection = mod.connection && contextMap()[mod.connection]
+ if (connection) {
+ return html`${children}`
+ }
+ return children
+}
+
+export const regionItemView = ({ object, contents = [] }) => {
+ const mod = object.mods[0]
+ const prop = propFromMod(mod, object.ref)
+ if (!prop) {
+ return null
+ }
+ const [propX, propY, propZ] = propLocationFromObjectXY(prop, mod.x, mod.y)
+ const frames = propFramesFromMod(prop, mod)
+ const objectSpace = translateSpace(compositeSpaces(frames), propX, propY)
+ const result = [html`
+ <${positionedInRegion} key=${object.ref} space=${objectSpace} z=${propZ}>
+ <${itemInteraction} mod=${mod}>
+ <${animatedDiv} frames=${frames}/>
+ />
+ `]
+ if (prop.contentsXY.length > 0) {
+ for (const item of contents) {
+ result.push(html`<${containedItemView} key=${item.ref} object=${item} containerProp=${prop} containerMod=${mod} containerSpace=${frames[0]}/>`)
+ }
+ }
+ return result
+}
+
export const regionView = ({ filename }) => {
const scale = useContext(Scale)
const objects = useHabitatJson(filename, [])
- let regionRef = null
- const items = objects.flatMap(obj => {
- if (obj.type == "context") {
- regionRef = obj.ref
- } else if (obj.type != "item") {
- logError(`Unknown object type ${obj.type}`, obj.ref)
- } else if (regionRef && obj.in != regionRef) {
- logError(`Object is inside container ${obj.in}; not yet supported`, obj.ref)
- } else {
- return [html`<${itemView} object=${obj}/>`]
- }
- return []
- })
+ const regionRef = objects.find(o => o.type === "context")?.ref
+ const items = objects
+ .filter(obj => obj.type === "item" && obj.in === regionRef)
+ .map(obj => html`<${itemView} viewer=${regionItemView} object=${obj} contents=${objects.filter(o => o.in === obj.ref)} key=${obj.ref}/>`)
+
return html`
${items}
@@ -279,8 +329,9 @@ export const objectDetails = ({ filename }) => {
}
}
details = html`
-
${image.filename}
- <${itemView} object=${obj} standalone="true"/>`
+
+ ${image.filename}
<${itemView} object=${obj} viewer=${standaloneItemView}/>
+ `
}
}
return html`
diff --git a/inspector/render.js b/inspector/render.js
index 323f2634..df0a1616 100644
--- a/inspector/render.js
+++ b/inspector/render.js
@@ -260,7 +260,9 @@ export const frameFromText = (x, y, bytes, charset, pattern, fineXOffset, colors
return compositeLayers(layers)
}
-// Habitat's coordinate space consistently has y=0 for the bottom, and increasing y means going up
+// We try to consistently model Habitat's coordinate space in our rendering code as y=0 for the bottom, with increasing y meaning going up.
+// However, the graphics code converts this internally to a coordinate space where increasing y means going down, and many internal
+// coordinates (cel offsets, etc.) assume this.
export const frameFromCels = (cels, { colors: celColors, paintOrder, firstCelOrigin = true, flipHorizontal }) => {
if (cels.length == 0) {
return null
@@ -319,6 +321,8 @@ export const frameFromCels = (cels, { colors: celColors, paintOrder, firstCelOri
frame.minX = -maxX + 1
frame.maxX = -minX + 1
}
+ frame.xOrigin = xCorrect
+ frame.yOrigin = yCorrect
return translateSpace(frame, -xCorrect, -yCorrect)
}