feat(sagi): custom pets — Codex pet bundles, images, featured downloads
The mascot can wear a pet. Right-click it for Hide pet, Size (48-250 px), Choose pet (Sagi, featured, your own - deletable), Upload your own..., Import .codex-pet bundle..., Find more on codex-pets.net and Pet settings.... The worn pet and its size are viewer prefs (agenthub.pet, agenthub.pet.size); the pets live on the server under <data dir>/pets/<id>/ (server/lib/pets.js, /api/pets). A Codex pet is the .codex-pet.zip format: pet.json + a 1536x1872 spritesheet.webp, 8x9 frames of 192x208, one row per state. PetSprite.tsx plays the row of the mascot's mood; an uploaded image bobs. Only the two bundle files are read, sizes are checked from the zip directory before inflating, the sheet's WebP header must match, uploads are raster only (no SVG), ids are made by the server, every write is same-origin. The four featured pets (Goku, Bac Ho, Border Collie, 11) are codex-pets.net uploads with no redistribution licence, so none is bundled: a pick downloads it from codex-pets.net by a fixed id.
This commit is contained in:
@@ -85,9 +85,9 @@ function defaultPos(): SagiPos {
|
||||
}
|
||||
|
||||
/** Resting top-left screen coords for a docked position. */
|
||||
function restingScreen(pos: SagiPos) {
|
||||
const avail = Math.max(0, vh() - SAGI_SIZE - 2 * SAGI_MARGIN);
|
||||
const left = pos.side === "left" ? SAGI_MARGIN : vw() - SAGI_SIZE - SAGI_MARGIN;
|
||||
function restingScreen(pos: SagiPos, size: number) {
|
||||
const avail = Math.max(0, vh() - size - 2 * SAGI_MARGIN);
|
||||
const left = pos.side === "left" ? SAGI_MARGIN : vw() - size - SAGI_MARGIN;
|
||||
const top = SAGI_MARGIN + pos.y * avail;
|
||||
return { left, top };
|
||||
}
|
||||
@@ -108,7 +108,8 @@ export interface SagiPlacement {
|
||||
consumeDrag: () => boolean;
|
||||
}
|
||||
|
||||
export function useSagiPosition(): SagiPlacement {
|
||||
/** `size`: the avatar's current footprint (the pet size pref), in px. */
|
||||
export function useSagiPosition(size: number = SAGI_SIZE): SagiPlacement {
|
||||
const [pos, setPos] = useState<SagiPos>(() => sagiPrefs.getPos() ?? defaultPos());
|
||||
const [drag, setDrag] = useState<{ left: number; top: number } | null>(null);
|
||||
const [, force] = useState(0); // re-derive resting coords on resize
|
||||
@@ -127,7 +128,7 @@ export function useSagiPosition(): SagiPlacement {
|
||||
return () => window.removeEventListener("resize", onResize);
|
||||
}, []);
|
||||
|
||||
const resting = restingScreen(pos);
|
||||
const resting = restingScreen(pos, size);
|
||||
const screen = drag ?? resting;
|
||||
|
||||
const onPointerDown = useCallback(
|
||||
@@ -146,40 +147,46 @@ export function useSagiPosition(): SagiPlacement {
|
||||
[screen.left, screen.top]
|
||||
);
|
||||
|
||||
const onPointerMove = useCallback((e: ReactPointerEvent) => {
|
||||
const start = startRef.current;
|
||||
if (!start) return;
|
||||
const dx = e.clientX - start.px;
|
||||
const dy = e.clientY - start.py;
|
||||
if (!movedRef.current && Math.hypot(dx, dy) < DRAG_THRESHOLD) return;
|
||||
movedRef.current = true;
|
||||
const left = Math.min(vw() - SAGI_SIZE - SAGI_MARGIN, Math.max(SAGI_MARGIN, start.left + dx));
|
||||
const top = Math.min(vh() - SAGI_SIZE - SAGI_MARGIN, Math.max(SAGI_MARGIN, start.top + dy));
|
||||
liveRef.current = { left, top };
|
||||
setDrag({ left, top });
|
||||
}, []);
|
||||
const onPointerMove = useCallback(
|
||||
(e: ReactPointerEvent) => {
|
||||
const start = startRef.current;
|
||||
if (!start) return;
|
||||
const dx = e.clientX - start.px;
|
||||
const dy = e.clientY - start.py;
|
||||
if (!movedRef.current && Math.hypot(dx, dy) < DRAG_THRESHOLD) return;
|
||||
movedRef.current = true;
|
||||
const left = Math.min(vw() - size - SAGI_MARGIN, Math.max(SAGI_MARGIN, start.left + dx));
|
||||
const top = Math.min(vh() - size - SAGI_MARGIN, Math.max(SAGI_MARGIN, start.top + dy));
|
||||
liveRef.current = { left, top };
|
||||
setDrag({ left, top });
|
||||
},
|
||||
[size]
|
||||
);
|
||||
|
||||
const onPointerUp = useCallback((e: ReactPointerEvent) => {
|
||||
try {
|
||||
(e.currentTarget as Element).releasePointerCapture?.(e.pointerId);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
const live = liveRef.current;
|
||||
if (live) {
|
||||
draggedRef.current = true;
|
||||
const side: "left" | "right" = live.left + SAGI_SIZE / 2 < vw() / 2 ? "left" : "right";
|
||||
const avail = Math.max(1, vh() - SAGI_SIZE - 2 * SAGI_MARGIN);
|
||||
const y = Math.min(1, Math.max(0, (live.top - SAGI_MARGIN) / avail));
|
||||
const next: SagiPos = { side, y };
|
||||
sagiPrefs.setPos(next);
|
||||
setPos(next);
|
||||
setDrag(null); // leave drag mode; resting coords (with transition) take over
|
||||
}
|
||||
liveRef.current = null;
|
||||
startRef.current = null;
|
||||
movedRef.current = false;
|
||||
}, []);
|
||||
const onPointerUp = useCallback(
|
||||
(e: ReactPointerEvent) => {
|
||||
try {
|
||||
(e.currentTarget as Element).releasePointerCapture?.(e.pointerId);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
const live = liveRef.current;
|
||||
if (live) {
|
||||
draggedRef.current = true;
|
||||
const side: "left" | "right" = live.left + size / 2 < vw() / 2 ? "left" : "right";
|
||||
const avail = Math.max(1, vh() - size - 2 * SAGI_MARGIN);
|
||||
const y = Math.min(1, Math.max(0, (live.top - SAGI_MARGIN) / avail));
|
||||
const next: SagiPos = { side, y };
|
||||
sagiPrefs.setPos(next);
|
||||
setPos(next);
|
||||
setDrag(null); // leave drag mode; resting coords (with transition) take over
|
||||
}
|
||||
liveRef.current = null;
|
||||
startRef.current = null;
|
||||
movedRef.current = false;
|
||||
},
|
||||
[size]
|
||||
);
|
||||
|
||||
const consumeDrag = useCallback(() => {
|
||||
const was = draggedRef.current;
|
||||
@@ -190,9 +197,9 @@ export function useSagiPosition(): SagiPlacement {
|
||||
return {
|
||||
left: screen.left,
|
||||
top: screen.top,
|
||||
size: SAGI_SIZE,
|
||||
size,
|
||||
side: pos.side,
|
||||
openUp: screen.top + SAGI_SIZE / 2 > vh() / 2,
|
||||
openUp: screen.top + size / 2 > vh() / 2,
|
||||
dragging: drag !== null,
|
||||
onPointerDown,
|
||||
onPointerMove,
|
||||
|
||||
Reference in New Issue
Block a user