mirror of
https://github.com/xmanrui/OpenClaw-bot-review.git
synced 2026-07-27 22:25:52 +00:00
Update pixel office characters and nav icons
This commit is contained in:
@@ -221,6 +221,7 @@ export default function PixelOfficePage() {
|
||||
const panRef = useRef<{ x: number; y: number }>(cachedPan)
|
||||
const savedLayoutRef = useRef<OfficeLayout | null>(cachedSavedLayout)
|
||||
const animationFrameIdRef = useRef<number | null>(null)
|
||||
const officeReadyRef = useRef<boolean>(false)
|
||||
const prevAgentStatesRef = useRef<Map<string, string>>(new Map(cachedPrevAgentStates))
|
||||
const seenSubagentEventKeysRef = useRef<Map<string, number>>(new Map())
|
||||
|
||||
@@ -382,6 +383,10 @@ export default function PixelOfficePage() {
|
||||
return () => mql.removeEventListener("change", apply)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
officeReadyRef.current = officeReady
|
||||
}, [officeReady])
|
||||
|
||||
// Load saved layout and sound preference
|
||||
useEffect(() => {
|
||||
const loadLayout = async () => {
|
||||
@@ -443,6 +448,23 @@ export default function PixelOfficePage() {
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const office = officeRef.current
|
||||
if (!officeReady || !office || cachedAgents.length === 0) return
|
||||
|
||||
for (const [agentId, charId] of agentIdMapRef.current) {
|
||||
office.removeAllSubagents(charId)
|
||||
office.removeAgent(charId)
|
||||
agentIdMapRef.current.delete(agentId)
|
||||
}
|
||||
nextIdRef.current.current = 1
|
||||
|
||||
setAgents(cachedAgents)
|
||||
syncAgentsToOffice(cachedAgents, office, agentIdMapRef.current, nextIdRef.current)
|
||||
cachedAgentIdMap = new Map(agentIdMapRef.current)
|
||||
cachedNextCharacterId = nextIdRef.current.current
|
||||
}, [officeReady])
|
||||
|
||||
useEffect(() => {
|
||||
if (soundOn) {
|
||||
void playBackgroundMusic()
|
||||
@@ -718,7 +740,7 @@ export default function PixelOfficePage() {
|
||||
useEffect(() => {
|
||||
if (cachedAgents.length > 0) {
|
||||
setAgents(cachedAgents)
|
||||
if (officeRef.current) {
|
||||
if (officeRef.current && officeReadyRef.current) {
|
||||
syncAgentsToOffice(cachedAgents, officeRef.current, agentIdMapRef.current, nextIdRef.current)
|
||||
}
|
||||
}
|
||||
@@ -731,7 +753,7 @@ export default function PixelOfficePage() {
|
||||
cachedAgents = newAgents
|
||||
|
||||
const office = officeRef.current
|
||||
if (office) {
|
||||
if (office && officeReadyRef.current) {
|
||||
syncAgentsToOffice(newAgents, office, agentIdMapRef.current, nextIdRef.current)
|
||||
cachedAgentIdMap = new Map(agentIdMapRef.current)
|
||||
cachedNextCharacterId = nextIdRef.current.current
|
||||
|
||||
+199
-10
@@ -10,27 +10,216 @@ const BUGS_ENABLED_KEY = "pixel-office-bugs-enabled";
|
||||
const BUGS_COUNT_KEY = "pixel-office-bugs-count";
|
||||
const BUGS_MAX = 400;
|
||||
|
||||
const NAV_ITEMS = [
|
||||
type NavIconName = "agents" | "pixelOffice" | "models" | "sessions" | "stats" | "alerts" | "skills";
|
||||
type PixelTone = "base" | "shade" | "light";
|
||||
type PixelRect = { x: number; y: number; w?: number; h?: number; tone?: PixelTone; opacity?: number };
|
||||
type PixelPalette = { base: string; shade: string; light: string };
|
||||
|
||||
function PixelSvg({ pixels, className, palette }: { pixels: PixelRect[]; className?: string; palette: PixelPalette }) {
|
||||
const fillForTone = (tone: PixelTone = "base") => {
|
||||
if (tone === "shade") return palette.shade;
|
||||
if (tone === "light") return palette.light;
|
||||
return palette.base;
|
||||
};
|
||||
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 16 16"
|
||||
aria-hidden="true"
|
||||
className={className}
|
||||
style={{ imageRendering: "pixelated", shapeRendering: "crispEdges" }}
|
||||
>
|
||||
{pixels.map((pixel, index) => (
|
||||
<rect
|
||||
key={index}
|
||||
x={pixel.x}
|
||||
y={pixel.y}
|
||||
width={pixel.w ?? 1}
|
||||
height={pixel.h ?? 1}
|
||||
fill={fillForTone(pixel.tone)}
|
||||
opacity={pixel.opacity ?? 1}
|
||||
/>
|
||||
))}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function navPalette(active: boolean): PixelPalette {
|
||||
return active
|
||||
? { base: "var(--accent)", shade: "color-mix(in srgb, var(--accent) 72%, black)", light: "color-mix(in srgb, var(--accent) 55%, white)" }
|
||||
: { base: "var(--text)", shade: "color-mix(in srgb, var(--text) 62%, black)", light: "color-mix(in srgb, var(--text) 35%, white)" };
|
||||
}
|
||||
|
||||
function NavPixelIcon({ name, active }: { name: NavIconName; active: boolean }) {
|
||||
const baseClass = "h-4 w-4";
|
||||
const palette = navPalette(active);
|
||||
|
||||
switch (name) {
|
||||
case "agents":
|
||||
return (
|
||||
<PixelSvg
|
||||
className={baseClass}
|
||||
palette={palette}
|
||||
pixels={[
|
||||
{ x: 5, y: 1, w: 6, h: 1, tone: "light" },
|
||||
{ x: 4, y: 2, w: 8, h: 1, tone: "base" },
|
||||
{ x: 3, y: 3, w: 10, h: 2, tone: "base" },
|
||||
{ x: 4, y: 5, w: 8, h: 1, tone: "shade" },
|
||||
{ x: 2, y: 7, w: 12, h: 1, tone: "base" },
|
||||
{ x: 1, y: 8, w: 3, h: 5, tone: "shade" },
|
||||
{ x: 6, y: 8, w: 4, h: 2, tone: "light", opacity: 0.95 },
|
||||
{ x: 12, y: 8, w: 3, h: 5, tone: "shade" },
|
||||
{ x: 5, y: 10, w: 6, h: 4, tone: "base" },
|
||||
{ x: 6, y: 6, w: 1, h: 1, tone: "light", opacity: 0.65 },
|
||||
{ x: 9, y: 6, w: 1, h: 1, tone: "light", opacity: 0.65 },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
case "pixelOffice":
|
||||
return (
|
||||
<PixelSvg
|
||||
className={baseClass}
|
||||
palette={palette}
|
||||
pixels={[
|
||||
{ x: 1, y: 4, w: 14, h: 1, tone: "shade" },
|
||||
{ x: 1, y: 5, w: 2, h: 8, tone: "base" },
|
||||
{ x: 13, y: 5, w: 2, h: 8, tone: "base" },
|
||||
{ x: 5, y: 1, w: 6, h: 2, tone: "light" },
|
||||
{ x: 4, y: 3, w: 8, h: 1, tone: "base" },
|
||||
{ x: 4, y: 7, w: 3, h: 3, tone: "shade" },
|
||||
{ x: 9, y: 7, w: 3, h: 3, tone: "shade" },
|
||||
{ x: 6, y: 10, w: 4, h: 3, tone: "light", opacity: 0.9 },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
case "models":
|
||||
return (
|
||||
<PixelSvg
|
||||
className={baseClass}
|
||||
palette={palette}
|
||||
pixels={[
|
||||
{ x: 5, y: 1, w: 6, h: 1, tone: "light" },
|
||||
{ x: 3, y: 3, w: 10, h: 1, tone: "base" },
|
||||
{ x: 2, y: 5, w: 12, h: 1, tone: "shade" },
|
||||
{ x: 3, y: 7, w: 10, h: 1, tone: "base" },
|
||||
{ x: 5, y: 9, w: 6, h: 1, tone: "light" },
|
||||
{ x: 4, y: 2, w: 1, h: 1, tone: "light", opacity: 0.8 },
|
||||
{ x: 11, y: 2, w: 1, h: 1, tone: "light", opacity: 0.8 },
|
||||
{ x: 1, y: 4, w: 1, h: 2, tone: "shade", opacity: 0.9 },
|
||||
{ x: 14, y: 4, w: 1, h: 2, tone: "shade", opacity: 0.9 },
|
||||
{ x: 4, y: 11, w: 1, h: 2, tone: "shade", opacity: 0.9 },
|
||||
{ x: 11, y: 11, w: 1, h: 2, tone: "shade", opacity: 0.9 },
|
||||
{ x: 6, y: 13, w: 4, h: 1, tone: "base" },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
case "sessions":
|
||||
return (
|
||||
<PixelSvg
|
||||
className={baseClass}
|
||||
palette={palette}
|
||||
pixels={[
|
||||
{ x: 2, y: 3, w: 10, h: 7, tone: "base" },
|
||||
{ x: 4, y: 10, w: 4, h: 2, tone: "shade" },
|
||||
{ x: 10, y: 10, w: 2, h: 2, tone: "shade" },
|
||||
{ x: 4, y: 5, w: 6, h: 1, tone: "light", opacity: 0.6 },
|
||||
{ x: 4, y: 7, w: 4, h: 1, tone: "light", opacity: 0.6 },
|
||||
{ x: 11, y: 6, w: 3, h: 5, tone: "shade", opacity: 0.9 },
|
||||
{ x: 12, y: 5, w: 2, h: 1, tone: "light", opacity: 0.85 },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
case "stats":
|
||||
return (
|
||||
<PixelSvg
|
||||
className={baseClass}
|
||||
palette={palette}
|
||||
pixels={[
|
||||
{ x: 2, y: 12, w: 12, h: 1, tone: "shade", opacity: 0.7 },
|
||||
{ x: 3, y: 9, w: 2, h: 3, tone: "base" },
|
||||
{ x: 7, y: 6, w: 2, h: 6, tone: "base" },
|
||||
{ x: 11, y: 3, w: 2, h: 9, tone: "light" },
|
||||
{ x: 2, y: 4, w: 2, h: 2, tone: "shade", opacity: 0.65 },
|
||||
{ x: 5, y: 2, w: 2, h: 2, tone: "light", opacity: 0.65 },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
case "alerts":
|
||||
return (
|
||||
<PixelSvg
|
||||
className={baseClass}
|
||||
palette={palette}
|
||||
pixels={[
|
||||
{ x: 7, y: 1, w: 2, h: 1, tone: "light" },
|
||||
{ x: 5, y: 2, w: 6, h: 1, tone: "base" },
|
||||
{ x: 4, y: 3, w: 8, h: 1, tone: "base" },
|
||||
{ x: 4, y: 4, w: 8, h: 5, tone: "shade" },
|
||||
{ x: 3, y: 9, w: 10, h: 2, tone: "base" },
|
||||
{ x: 6, y: 12, w: 4, h: 1, tone: "light" },
|
||||
{ x: 5, y: 13, w: 6, h: 1, tone: "shade", opacity: 0.8 },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
case "skills":
|
||||
return (
|
||||
<PixelSvg
|
||||
className={baseClass}
|
||||
palette={palette}
|
||||
pixels={[
|
||||
{ x: 6, y: 1, w: 4, h: 2, tone: "light" },
|
||||
{ x: 4, y: 3, w: 8, h: 2, tone: "base" },
|
||||
{ x: 2, y: 5, w: 12, h: 6, tone: "shade" },
|
||||
{ x: 4, y: 11, w: 8, h: 2, tone: "base" },
|
||||
{ x: 6, y: 13, w: 4, h: 2, tone: "light" },
|
||||
{ x: 7, y: 6, w: 2, h: 4, tone: "base", opacity: 0.5 },
|
||||
{ x: 5, y: 7, w: 6, h: 2, tone: "light", opacity: 0.45 },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function NavItemIcon({ name, active }: { name: NavIconName; active: boolean }) {
|
||||
return (
|
||||
<span
|
||||
className={`inline-flex h-8 w-8 items-center justify-center border transition-colors ${
|
||||
active
|
||||
? "border-[var(--accent)]/45 bg-[var(--accent)]/14"
|
||||
: "border-[var(--border)] bg-[var(--bg)]/88"
|
||||
}`}
|
||||
style={{
|
||||
borderRadius: 0,
|
||||
boxShadow: active
|
||||
? "inset 0 1px 0 rgba(255,255,255,0.12), inset 0 -2px 0 rgba(0,0,0,0.18), 0 0 0 1px rgba(0,0,0,0.04)"
|
||||
: "inset 0 1px 0 rgba(255,255,255,0.08), inset 0 -2px 0 rgba(0,0,0,0.1)",
|
||||
}}
|
||||
>
|
||||
<NavPixelIcon name={name} active={active} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const NAV_ITEMS: { group: string; items: { href: string; icon: NavIconName; labelKey: string }[] }[] = [
|
||||
{
|
||||
group: "nav.overview",
|
||||
items: [
|
||||
{ href: "/", icon: "🤖", labelKey: "nav.agents" },
|
||||
{ href: "/pixel-office", icon: "🎮", labelKey: "nav.pixelOffice" },
|
||||
{ href: "/models", icon: "🧠", labelKey: "nav.models" },
|
||||
{ href: "/", icon: "agents", labelKey: "nav.agents" },
|
||||
{ href: "/pixel-office", icon: "pixelOffice", labelKey: "nav.pixelOffice" },
|
||||
{ href: "/models", icon: "models", labelKey: "nav.models" },
|
||||
],
|
||||
},
|
||||
{
|
||||
group: "nav.monitor",
|
||||
items: [
|
||||
{ href: "/sessions", icon: "💬", labelKey: "nav.sessions" },
|
||||
{ href: "/stats", icon: "📊", labelKey: "nav.stats" },
|
||||
{ href: "/alerts", icon: "🔔", labelKey: "nav.alerts" },
|
||||
{ href: "/sessions", icon: "sessions", labelKey: "nav.sessions" },
|
||||
{ href: "/stats", icon: "stats", labelKey: "nav.stats" },
|
||||
{ href: "/alerts", icon: "alerts", labelKey: "nav.alerts" },
|
||||
],
|
||||
},
|
||||
{
|
||||
group: "nav.config",
|
||||
items: [
|
||||
{ href: "/skills", icon: "🧩", labelKey: "nav.skills" },
|
||||
{ href: "/skills", icon: "skills", labelKey: "nav.skills" },
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -341,7 +530,7 @@ export function Sidebar() {
|
||||
: "text-[var(--text-muted)] hover:text-[var(--text)] hover:bg-[var(--bg)]"
|
||||
}`}
|
||||
>
|
||||
<span className="text-base">{item.icon}</span>
|
||||
<NavItemIcon name={item.icon} active={active} />
|
||||
{t(item.labelKey)}
|
||||
</Link>
|
||||
);
|
||||
@@ -513,7 +702,7 @@ export function Sidebar() {
|
||||
gap: collapsed ? 0 : 10,
|
||||
}}
|
||||
>
|
||||
<span className="text-base">{item.icon}</span>
|
||||
<NavItemIcon name={item.icon} active={active} />
|
||||
{!collapsed && t(item.labelKey)}
|
||||
</Link>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { TILE_SIZE, MATRIX_EFFECT_DURATION, CharacterState, Direction } from '../types'
|
||||
import {
|
||||
PALETTE_COUNT,
|
||||
HUE_SHIFT_MIN_DEG,
|
||||
HUE_SHIFT_RANGE_DEG,
|
||||
WAITING_BUBBLE_DURATION_SEC,
|
||||
@@ -15,6 +14,7 @@ import {
|
||||
} from '../constants'
|
||||
import type { Character, Seat, FurnitureInstance, TileType as TileTypeVal, OfficeLayout, PlacedFurniture } from '../types'
|
||||
import { createCharacter, updateCharacter } from './characters'
|
||||
import { CHARACTER_PALETTES, getAvailableCharacterVariantCount } from '../sprites/spriteData'
|
||||
import { matrixEffectSeeds } from './matrixEffect'
|
||||
import { isWalkable, getWalkableTiles, findPath } from '../layout/tileMap'
|
||||
import {
|
||||
@@ -371,23 +371,26 @@ export class OfficeState {
|
||||
|
||||
/**
|
||||
* Pick a diverse palette for a new agent based on currently active agents.
|
||||
* First 6 agents each get a unique skin (random order). Beyond 6, skins
|
||||
* First round uses each available character variant once (random order).
|
||||
* Beyond that, variants
|
||||
* repeat in balanced rounds with a random hue shift (≥45°).
|
||||
*/
|
||||
private pickDiversePalette(): { palette: number; hueShift: number } {
|
||||
// Count how many non-sub-agents use each base palette (0-5)
|
||||
const counts = new Array(PALETTE_COUNT).fill(0) as number[]
|
||||
const paletteCount = getAvailableCharacterVariantCount()
|
||||
const counts = new Array(paletteCount).fill(0) as number[]
|
||||
for (const ch of this.characters.values()) {
|
||||
if (ch.isSubagent) continue
|
||||
counts[ch.palette]++
|
||||
counts[ch.palette % paletteCount]++
|
||||
}
|
||||
const minCount = Math.min(...counts)
|
||||
// Available = palettes at the minimum count (least used)
|
||||
// Available = variants at the minimum count (least used)
|
||||
const available: number[] = []
|
||||
for (let i = 0; i < PALETTE_COUNT; i++) {
|
||||
for (let i = 0; i < paletteCount; i++) {
|
||||
if (counts[i] === minCount) available.push(i)
|
||||
}
|
||||
const palette = available[Math.floor(Math.random() * available.length)]
|
||||
const extraVariants = available.filter((index) => index >= CHARACTER_PALETTES.length)
|
||||
const preferred = minCount === 0 && extraVariants.length > 0 ? extraVariants : available
|
||||
const palette = preferred[Math.floor(Math.random() * preferred.length)]
|
||||
// First round (minCount === 0): no hue shift. Subsequent rounds: random ≥45°.
|
||||
let hueShift = 0
|
||||
if (minCount > 0) {
|
||||
|
||||
@@ -7,13 +7,9 @@ import { setWallSprites } from '../wallTiles'
|
||||
* Load a PNG image and convert it to SpriteData (2D array of hex color strings).
|
||||
* Transparent pixels become '' (empty string).
|
||||
*/
|
||||
function pngToSpriteData(img: HTMLImageElement): SpriteData {
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = img.width
|
||||
canvas.height = img.height
|
||||
function canvasToSpriteData(canvas: HTMLCanvasElement): SpriteData {
|
||||
const ctx = canvas.getContext('2d')!
|
||||
ctx.drawImage(img, 0, 0)
|
||||
const imageData = ctx.getImageData(0, 0, img.width, img.height)
|
||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
|
||||
const { data, width, height } = imageData
|
||||
|
||||
const result: string[][] = []
|
||||
@@ -33,6 +29,15 @@ function pngToSpriteData(img: HTMLImageElement): SpriteData {
|
||||
return result
|
||||
}
|
||||
|
||||
function pngToSpriteData(img: HTMLImageElement): SpriteData {
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = img.width
|
||||
canvas.height = img.height
|
||||
const ctx = canvas.getContext('2d')!
|
||||
ctx.drawImage(img, 0, 0)
|
||||
return canvasToSpriteData(canvas)
|
||||
}
|
||||
|
||||
function loadImage(src: string): Promise<HTMLImageElement> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image()
|
||||
@@ -42,6 +47,76 @@ function loadImage(src: string): Promise<HTMLImageElement> {
|
||||
})
|
||||
}
|
||||
|
||||
function normalizedSpriteData(img: HTMLImageElement, targetWidth?: number, targetHeight?: number): SpriteData {
|
||||
const canvas = document.createElement('canvas')
|
||||
const shouldResize =
|
||||
typeof targetWidth === 'number' &&
|
||||
typeof targetHeight === 'number' &&
|
||||
img.width !== targetWidth &&
|
||||
img.height !== targetHeight &&
|
||||
img.width % targetWidth === 0 &&
|
||||
img.height % targetHeight === 0
|
||||
|
||||
if (shouldResize) {
|
||||
canvas.width = targetWidth
|
||||
canvas.height = targetHeight
|
||||
const ctx = canvas.getContext('2d')!
|
||||
ctx.imageSmoothingEnabled = false
|
||||
ctx.drawImage(img, 0, 0, targetWidth, targetHeight)
|
||||
return canvasToSpriteData(canvas)
|
||||
}
|
||||
|
||||
return pngToSpriteData(img)
|
||||
}
|
||||
|
||||
function stripOpaqueSheetBackground(sprite: SpriteData): SpriteData {
|
||||
if (sprite.length === 0 || sprite[0].length === 0) return sprite
|
||||
if (sprite.some((row) => row.some((pixel) => pixel === ''))) return sprite
|
||||
|
||||
const height = sprite.length
|
||||
const width = sprite[0].length
|
||||
const result = sprite.map((row) => [...row])
|
||||
const visited = Array.from({ length: height }, () => Array(width).fill(false))
|
||||
const queue: Array<[number, number]> = []
|
||||
|
||||
const brightness = (hex: string): number => {
|
||||
const r = parseInt(hex.slice(1, 3), 16)
|
||||
const g = parseInt(hex.slice(3, 5), 16)
|
||||
const b = parseInt(hex.slice(5, 7), 16)
|
||||
return 0.299 * r + 0.587 * g + 0.114 * b
|
||||
}
|
||||
|
||||
const corners = [sprite[0][0], sprite[0][width - 1], sprite[height - 1][0], sprite[height - 1][width - 1]]
|
||||
const threshold = Math.max(140, Math.min(...corners.map((pixel) => brightness(pixel))) - 12)
|
||||
|
||||
const enqueue = (x: number, y: number) => {
|
||||
if (visited[y][x]) return
|
||||
if (brightness(result[y][x]) < threshold) return
|
||||
visited[y][x] = true
|
||||
queue.push([x, y])
|
||||
}
|
||||
|
||||
for (let x = 0; x < width; x++) {
|
||||
enqueue(x, 0)
|
||||
enqueue(x, height - 1)
|
||||
}
|
||||
for (let y = 0; y < height; y++) {
|
||||
enqueue(0, y)
|
||||
enqueue(width - 1, y)
|
||||
}
|
||||
|
||||
while (queue.length > 0) {
|
||||
const [x, y] = queue.shift()!
|
||||
result[y][x] = ''
|
||||
if (x > 0) enqueue(x - 1, y)
|
||||
if (x + 1 < width) enqueue(x + 1, y)
|
||||
if (y > 0) enqueue(x, y - 1)
|
||||
if (y + 1 < height) enqueue(x, y + 1)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a sub-region from a SpriteData array.
|
||||
*/
|
||||
@@ -74,16 +149,33 @@ function parseCharacterSheet(sheet: SpriteData): LoadedCharacterData {
|
||||
|
||||
/**
|
||||
* Load character PNGs from /assets/pixel-office/characters/ and register them.
|
||||
* Falls back silently to hardcoded templates if loading fails.
|
||||
* Loads the default set plus any extra contiguous char_N.png files.
|
||||
* Falls back silently to hardcoded templates if the base set fails.
|
||||
*/
|
||||
export async function loadCharacterPNGs(): Promise<boolean> {
|
||||
try {
|
||||
const characters: LoadedCharacterData[] = []
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const baseCharacterCount = 6
|
||||
const maxCharacterCount = 64
|
||||
const CHARACTER_SHEET_WIDTH = 112
|
||||
const CHARACTER_SHEET_HEIGHT = 96
|
||||
|
||||
for (let i = 0; i < baseCharacterCount; i++) {
|
||||
const img = await loadImage(`/assets/pixel-office/characters/char_${i}.png`)
|
||||
const sheet = pngToSpriteData(img)
|
||||
const sheet = stripOpaqueSheetBackground(normalizedSpriteData(img, CHARACTER_SHEET_WIDTH, CHARACTER_SHEET_HEIGHT))
|
||||
characters.push(parseCharacterSheet(sheet))
|
||||
}
|
||||
|
||||
for (let i = baseCharacterCount; i < maxCharacterCount; i++) {
|
||||
try {
|
||||
const img = await loadImage(`/assets/pixel-office/characters/char_${i}.png`)
|
||||
const sheet = stripOpaqueSheetBackground(normalizedSpriteData(img, CHARACTER_SHEET_WIDTH, CHARACTER_SHEET_HEIGHT))
|
||||
characters.push(parseCharacterSheet(sheet))
|
||||
} catch {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
setCharacterTemplates(characters)
|
||||
return true
|
||||
} catch (e) {
|
||||
|
||||
@@ -1123,6 +1123,10 @@ export interface LoadedCharacterData {
|
||||
|
||||
let loadedCharacters: LoadedCharacterData[] | null = null
|
||||
|
||||
export function getAvailableCharacterVariantCount(): number {
|
||||
return loadedCharacters?.length ?? CHARACTER_PALETTES.length
|
||||
}
|
||||
|
||||
/** Set pre-colored character sprites loaded from PNG assets. Call this when characterSpritesLoaded message arrives. */
|
||||
export function setCharacterTemplates(data: LoadedCharacterData[]): void {
|
||||
loadedCharacters = data
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.6 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.7 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
Reference in New Issue
Block a user