Skip to content

Commit

Permalink
Add comments to some dense math stuff
Browse files Browse the repository at this point in the history
  • Loading branch information
tiliv authored Oct 22, 2024
1 parent 767ecf9 commit 315f73d
Showing 1 changed file with 56 additions and 9 deletions.
65 changes: 56 additions & 9 deletions src/components/DisplayWorld.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,27 +71,54 @@ export default function DisplayWorld({
setZoneBuffer(null);
return;
}
const { box, buffer, maxWidth, directions, attributes: { fg='f00', bg=null }={} } = zone;
const { box, buffer, maxWidth, directions, attributes: { fg='f00', bg=null }={} } = zone;
const [startY, startX] = [box[0] - origin.y - 1, box[1] - origin.x - 1];

// `animation` counts across each frame then repeats, but we want to total
// up all prior frames as well.
let offset = [0, 0];
let offset = [0, 0]; // total of prior loops, plus current loop
if (directions) {
const loops = Math.floor(animation / directions.length);

// The current frames which are being used by the current loop of the animation.
// Each frame in directions is a [dy,dx] pair that describes one part of the defined
// animations frames. [dy,dx] is a single step, but all are applied additively.
const frames = directions.slice(0, (animation % directions.length) + 1);

// Because `frames` is just the current loop progress,
// the offset result of all prior loops should be applied too.
// We reduce the direction list to one [dy,dx] vector by summing.
const priorOffset = directions
.map(([dy, dx]) => [dy * loops, dx * loops])
.reduce(([y, x], [dy, dx]) => [y + dy, x + dx], [0, 0])
.map(([dy, dx]) => [dy * loops, dx * loops]) // Amplify per finished loop
.reduce(([y, x], [dy, dx]) => [y + dy, x + dx], [0, 0]) // Sum separately
;

// Sum the current loop's progress as a single [dy,dx] result,
// starting from the priorOffset above.
offset = frames.reduce(([y, x], [dy, dx]) => [y + dy, x + dx], priorOffset);
}

// The sprite is a 2d array, but each array is probably truncating blank spaces
// at the end of the line and is short the real screen width.
// Note that if the sprite is smaller than the screen dimensions anyway,
// none of the sprite's rows will long enough in the first place.
//
// Here, we build a viewport-sized 2d array and then fill it with the tiled sprite,
// while treating truncated sprite positions as blank.
// There is math here because the entire `priorOffset` found above pushes the tiled
// sprite around indefinitely, guaranteeing our [1,1] viewport position won't be
// drawing [1,1] of the tile template.
const rendered = Array.from(
{ length: height },
(_, y) => Array.from({ length: width }, (_, x) => {
// y,x are abstractly walking across the viewport in zero-indexed array space.

// r,c is the unique coordinate on the map that represents the top-left corner.
const [r, c] = [y + origin.y + 1, x + origin.x + 1];

// If the current zone is not applicable, ignore the attempt for this coord.
if (r < box[0] || r > box[2] || c < box[1] || c > box[3]) return '';

// The buffer-space br,bc is the position in the sprite that should be used for
// this y,x screen position. If any animation exists, this result changes over time.
const br = ((y - startY + offset[0]) % size[0] + size[0]) % size[0];
const bc = ((x - startX + offset[1]) % size[1] + size[1]) % size[1];
const row = buffer[br % buffer.length];
Expand All @@ -109,12 +136,15 @@ export default function DisplayWorld({
window.dispatchEvent(new CustomEvent('origin', { detail: origin }));
}, [origin])

// Find responders to ambient events
// Find responders to ambient events, if present on the screen.
useEffect(() => {
const ambientHandler = ({ detail: { name }}) => {
Object.entries(interactions)
.filter(([, { type, [name]: data }]) => type === TYPES.NPC && data)
.forEach(([, interaction]) => {
// Auto-throw an interaction-style event that normally the player
// triggers, not the NPC. The `start` field declares an action is
// already in progress as of now.
const event = new CustomEvent('interaction', {
detail: { ...interaction, start: name },
});
Expand All @@ -126,6 +156,9 @@ export default function DisplayWorld({
}, [interactions]);

// Build the full-screen opacity buffer and copy over player & target sprites.
// `incidental` interactions have no options available, like a wall that simply
// blocks your movement. The coordinate will be highlighted anyway, but it will
// not dim the whole screen like it needs your attention.
useEffect(() => {
if (!target || target.incidental) {
setActiveBuffer(null);
Expand All @@ -142,16 +175,24 @@ export default function DisplayWorld({
const buffers = [];

if (battle) {
// Battle view compresses the screen into one horizontal strip.
// The spacers are rows that are empty but get shown above the
// battle view.
const spacers = Array.from({ length: parseInt(height / 2) }, () => Array.from({ length: width }, () => ' '));

// The viewport already offers a 3-layer system of opacity for distance.
const strips = [
(layers.foreground || []).join(''),
(layers.background1 || []).join(''),
(layers.background2 || []).join(''),
];
const y = position.y + 1;

// Extreme shorthand for this npc's position and if it's in the battle view
const prefix = `^(${y}|${y-1}|${y-2}),`;
const see = Object.entries(interactions).filter(([k]) => k.match(prefix));
const see = Object.entries(interactions).filter(([k]) => k.match(prefix));
see.forEach(([coord, data]) => {
// Put the npc sprite on the opacity layer it currently belongs to
const [r, c] = coord.split(',').map(Number);
const x = (c - 1) % width;
const layer = Math.abs(r - position.y - 1);
Expand All @@ -164,16 +205,22 @@ export default function DisplayWorld({
{ fg: '#000', buffer: [...spacers, ' '.repeat(local.x) + marker] },
);
} else {

// When not in battle mode, this is the standard buffer stack.
buffers.push(...[
{ fg: '#555', buffer: layers.solid },
{ fg: '#888', buffer: layers.passable },
{ fg: '#f50', buffer: interactionBuffer },
{ fg: '#000', buffer: layers.objects },
{ fg: '#000', buffer: [
...Array.from({ length: local.y }, () => ''),
' '.repeat(local.x) + marker,
' '.repeat(local.x) + marker, // draw player on their row
]},

// The current viewport
activeBuffer && { bg: '#ccc7', fg: '#000', buffer: activeBuffer },

// The weather or whatever
zoneBuffer,
].filter(Boolean));
}
Expand Down

0 comments on commit 315f73d

Please sign in to comment.