mirror of
https://github.com/xmanrui/OpenClaw-bot-review.git
synced 2026-07-27 22:25:52 +00:00
feat(pixel-office): add wandering lobster with forward-facing direction
This commit is contained in:
@@ -94,6 +94,7 @@ export function createCharacter(
|
||||
matrixEffectSeeds: [],
|
||||
interactionTarget: null,
|
||||
isCat: false,
|
||||
isLobster: false,
|
||||
codeSnippets: [],
|
||||
photoComments: [],
|
||||
isViewingPhoto: false,
|
||||
@@ -111,8 +112,8 @@ export function updateCharacter(
|
||||
): void {
|
||||
ch.frameTimer += dt
|
||||
|
||||
// Cat-specific update: always wander, never sit
|
||||
if (ch.isCat) {
|
||||
// Pet-specific update: always wander, never sit
|
||||
if (ch.isCat || ch.isLobster) {
|
||||
updateCat(ch, dt, walkableTiles, tileMap, blockedTiles)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -101,6 +101,7 @@ export class OfficeState {
|
||||
subagentMeta: Map<number, { parentAgentId: number; parentToolId: string }> = new Map()
|
||||
private nextSubagentId = -1
|
||||
private static CAT_ID = -9999
|
||||
private static LOBSTER_ID = -9998
|
||||
|
||||
constructor(layout?: OfficeLayout) {
|
||||
this.layout = layout || createDefaultLayout()
|
||||
@@ -112,6 +113,7 @@ export class OfficeState {
|
||||
this.interactionPoints = getInteractionPoints(this.layout.furniture, this.tileMap, this.blockedTiles)
|
||||
this.doorwayTiles = getDoorwayTiles(this.layout)
|
||||
this.spawnCat()
|
||||
this.spawnLobster()
|
||||
}
|
||||
|
||||
/** Rebuild all derived state from a new layout. Reassigns existing characters.
|
||||
@@ -353,6 +355,24 @@ export class OfficeState {
|
||||
this.characters.set(id, ch)
|
||||
}
|
||||
|
||||
/** Spawn the office lobster at a random walkable tile */
|
||||
spawnLobster(): void {
|
||||
const id = OfficeState.LOBSTER_ID
|
||||
if (this.characters.has(id)) return
|
||||
const spawn = this.walkableTiles.length > 0
|
||||
? this.walkableTiles[Math.floor(Math.random() * this.walkableTiles.length)]
|
||||
: { col: 1, row: 1 }
|
||||
const ch = createCharacter(id, 0, null, null, 0)
|
||||
ch.isLobster = true
|
||||
ch.tileCol = spawn.col
|
||||
ch.tileRow = spawn.row
|
||||
ch.x = spawn.col * TILE_SIZE + TILE_SIZE / 2
|
||||
ch.y = spawn.row * TILE_SIZE + TILE_SIZE / 2
|
||||
ch.state = CharacterState.IDLE
|
||||
ch.wanderTimer = 1 + Math.random() * 3
|
||||
this.characters.set(id, ch)
|
||||
}
|
||||
|
||||
removeAgent(id: number): void {
|
||||
const ch = this.characters.get(id)
|
||||
if (!ch) return
|
||||
@@ -754,7 +774,7 @@ export class OfficeState {
|
||||
}
|
||||
|
||||
// Photo comment particles for characters viewing the photograph
|
||||
if (ch.isViewingPhoto && ch.state === CharacterState.IDLE && !ch.isCat) {
|
||||
if (ch.isViewingPhoto && ch.state === CharacterState.IDLE && !ch.isCat && !ch.isLobster) {
|
||||
for (const pc of ch.photoComments) pc.age += dt
|
||||
ch.photoComments = ch.photoComments.filter(pc => pc.age < PHOTO_COMMENT_LIFETIME)
|
||||
if (ch.photoComments.length < 2 && Math.random() < dt * PHOTO_COMMENT_SPAWN_RATE) {
|
||||
@@ -778,7 +798,7 @@ export class OfficeState {
|
||||
}
|
||||
|
||||
// Code snippet particles for working characters
|
||||
if (ch.isActive && ch.state === CharacterState.TYPE && !ch.isCat) {
|
||||
if (ch.isActive && ch.state === CharacterState.TYPE && !ch.isCat && !ch.isLobster) {
|
||||
// Age existing snippets and remove expired ones
|
||||
for (const s of ch.codeSnippets) s.age += dt
|
||||
ch.codeSnippets = ch.codeSnippets.filter(s => s.age < CODE_SNIPPET_LIFETIME)
|
||||
@@ -809,9 +829,9 @@ export class OfficeState {
|
||||
getCharacterAt(worldX: number, worldY: number): number | null {
|
||||
const chars = this.getCharacters().sort((a, b) => b.y - a.y)
|
||||
for (const ch of chars) {
|
||||
// Skip characters that are despawning or cats
|
||||
// Skip characters that are despawning or pets
|
||||
if (ch.matrixEffect === 'despawn') continue
|
||||
if (ch.isCat) continue
|
||||
if (ch.isCat || ch.isLobster) continue
|
||||
// Character sprite is 16x24, anchored bottom-center
|
||||
// Apply sitting offset to match visual position
|
||||
const sittingOffset = ch.state === CharacterState.TYPE ? CHARACTER_SITTING_OFFSET_PX : 0
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { TileType, TILE_SIZE, CharacterState } from '../types'
|
||||
import { TileType, TILE_SIZE, CharacterState, Direction } from '../types'
|
||||
import type { TileType as TileTypeVal, FurnitureInstance, Character, SpriteData, Seat, FloorColor } from '../types'
|
||||
import { getCachedSprite, getOutlineSprite } from '../sprites/spriteCache'
|
||||
import { getCharacterSprites, BUBBLE_PERMISSION_SPRITE, BUBBLE_WAITING_SPRITE } from '../sprites/spriteData'
|
||||
@@ -297,6 +297,32 @@ export function renderScene(
|
||||
|
||||
// Characters
|
||||
for (const ch of characters) {
|
||||
const charZY = ch.y + TILE_SIZE / 2 + CHARACTER_Z_SORT_OFFSET
|
||||
|
||||
if (ch.isLobster) {
|
||||
const lobsterX = Math.round(offsetX + ch.x * zoom)
|
||||
const lobsterY = Math.round(offsetY + ch.y * zoom + 2 * zoom)
|
||||
const lobsterAngle =
|
||||
ch.dir === Direction.RIGHT ? Math.PI / 2 :
|
||||
ch.dir === Direction.DOWN ? Math.PI :
|
||||
ch.dir === Direction.LEFT ? -Math.PI / 2 :
|
||||
0
|
||||
drawables.push({
|
||||
zY: charZY,
|
||||
draw: (c) => {
|
||||
c.save()
|
||||
c.translate(lobsterX, lobsterY)
|
||||
c.rotate(lobsterAngle)
|
||||
c.textAlign = 'center'
|
||||
c.textBaseline = 'middle'
|
||||
c.font = `${Math.max(14, Math.round(9 * zoom))}px serif`
|
||||
c.fillText('🦞', 0, 0)
|
||||
c.restore()
|
||||
},
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
const sprites = ch.isCat ? getCatSprites() : getCharacterSprites(ch.palette, ch.hueShift)
|
||||
const spriteData = getCharacterSprite(ch, sprites)
|
||||
const cached = getCachedSprite(spriteData, zoom)
|
||||
@@ -309,7 +335,6 @@ export function renderScene(
|
||||
// Sort characters by bottom of their tile (not center) so they render
|
||||
// in front of same-row furniture (e.g. chairs) but behind furniture
|
||||
// at lower rows (e.g. desks, bookshelves that occlude from below).
|
||||
const charZY = ch.y + TILE_SIZE / 2 + CHARACTER_Z_SORT_OFFSET
|
||||
|
||||
// Matrix spawn/despawn effect — skip outline, use per-pixel rendering
|
||||
if (ch.matrixEffect) {
|
||||
|
||||
@@ -199,6 +199,7 @@ export interface Character {
|
||||
matrixEffectSeeds: number[]
|
||||
interactionTarget: { col: number; row: number; facingDir: Direction; furnitureType?: string } | null
|
||||
isCat: boolean
|
||||
isLobster: boolean
|
||||
codeSnippets: Array<{ text: string; age: number; x: number; y: number }>
|
||||
photoComments: Array<{ text: string; age: number; x: number }>
|
||||
isViewingPhoto: boolean
|
||||
|
||||
Reference in New Issue
Block a user