Initial commit

This commit is contained in:
LZH-YS1998
2026-07-01 17:56:31 +08:00
commit d78931979d
731 changed files with 311088 additions and 0 deletions
@@ -0,0 +1,215 @@
import type { Direction, SeatDef, InteractableDef } from '../types'
import { OFFICE_COLS, OFFICE_ROWS } from '../config'
import { getOffices, parseOfficeMapStr, type OfficeConfig } from './OfficeStore'
export interface ZoneDef {
name: string
bounds: { x: number; y: number; w: number; h: number }
seats: SeatDef[]
interactables: InteractableDef[]
doorways: { id: string; tileX: number; tileY: number }[]
}
function seat(id: string, tileX: number, tileY: number, facing: Direction): SeatDef {
return { id, tileX, tileY, facing, assigned: false, assignedTo: null }
}
function interactable(id: string, tileX: number, tileY: number, type: string): InteractableDef {
return { id, tileX, tileY, type }
}
interface ZoneTemplate {
name: string
localBounds: { x: number; y: number; w: number; h: number }
interactables: { id: string; localX: number; localY: number; type: string }[]
doorways: { id: string; localX: number; localY: number }[]
}
const ZONE_TEMPLATES: Record<string, ZoneTemplate> = {
meetingRoom: {
name: 'Meeting Room',
localBounds: { x: 7, y: 3, w: 6, h: 5 },
interactables: [{ id: 'whiteboard', localX: 9, localY: 1, type: 'whiteboard' }],
doorways: [{ id: 'door-meeting', localX: 9, localY: 9 }],
},
workspace: {
name: 'Workspace',
localBounds: { x: 1, y: 10, w: 11, h: 7 },
interactables: [{ id: 'printer', localX: 10, localY: 10, type: 'printer' }],
doorways: [{ id: 'door-ws', localX: 9, localY: 10 }],
},
breakRoom: {
name: 'Break Room',
localBounds: { x: 13, y: 10, w: 6, h: 8 },
interactables: [
{ id: 'coffee-machine', localX: 18, localY: 10, type: 'coffee_machine' },
{ id: 'fridge', localX: 18, localY: 11, type: 'fridge' },
],
doorways: [{ id: 'door-break', localX: 13, localY: 10 }],
},
leaderOffice: {
name: 'Leader Office',
localBounds: { x: 13, y: 18, w: 6, h: 6 },
interactables: [],
doorways: [{ id: 'door-leader', localX: 16, localY: 17 }],
},
lobby: {
name: 'Lobby',
localBounds: { x: 1, y: 19, w: 12, h: 5 },
interactables: [],
doorways: [{ id: 'entrance', localX: 10, localY: 18 }],
},
}
function inferFacing(col: number, row: number, zoneName: string): Direction {
switch (zoneName) {
case 'workspace':
return 'up'
case 'meetingRoom': {
const tableCenterX = 9.5
return col < tableCenterX ? 'right' : 'left'
}
case 'breakRoom': {
const tableCenterX = 15.5
return col < tableCenterX ? 'right' : 'left'
}
case 'leaderOffice':
return 'up'
default:
return 'down'
}
}
function classifyLocalSeat(col: number, row: number): string {
for (const [name, z] of Object.entries(ZONE_TEMPLATES)) {
const { x, y, w, h } = z.localBounds
if (col >= x && col < x + w && row >= y && row < y + h) return name
}
return 'lobby'
}
export function buildZonesForOffice(office: OfficeConfig): Record<string, ZoneDef> {
const off = office.offsetCol
const zones: Record<string, ZoneDef> = {}
for (const [zoneKey, tmpl] of Object.entries(ZONE_TEMPLATES)) {
const globalKey = `${office.id}-${zoneKey}`
zones[globalKey] = {
name: `${tmpl.name} (${office.name})`,
bounds: {
x: tmpl.localBounds.x + off,
y: tmpl.localBounds.y,
w: tmpl.localBounds.w,
h: tmpl.localBounds.h,
},
seats: [],
interactables: tmpl.interactables.map(i =>
interactable(`${office.id}-${i.id}`, i.localX + off, i.localY, i.type),
),
doorways: tmpl.doorways.map(d => ({
id: `${office.id}-${d.id}`,
tileX: d.localX + off,
tileY: d.localY,
})),
}
}
const counters: Record<string, number> = {}
for (const [col, row] of office.seats) {
const localZone = classifyLocalSeat(col, row)
const globalKey = `${office.id}-${localZone}`
if (!zones[globalKey]) continue
counters[globalKey] = (counters[globalKey] ?? 0) + 1
const idx = counters[globalKey]
const prefix = localZone === 'workspace' ? 'desk' : localZone === 'meetingRoom' ? 'meeting' : localZone === 'breakRoom' ? 'break' : localZone === 'leaderOffice' ? 'leader' : 'lobby'
const facing = inferFacing(col, row, localZone)
zones[globalKey].seats.push(seat(`${office.id}-${prefix}-${idx}`, col + off, row, facing))
}
return zones
}
export function buildAllZones(offices?: OfficeConfig[]): Record<string, ZoneDef> {
const all = offices ?? getOffices()
const zones: Record<string, ZoneDef> = {}
for (const office of all) {
Object.assign(zones, buildZonesForOffice(office))
}
return zones
}
export let ZONES: Record<string, ZoneDef> = buildAllZones()
export function reloadZones(offices?: OfficeConfig[]) {
ZONES = buildAllZones(offices)
}
export function getOfficeZoneKey(officeId: string, zoneName: string): string {
return `${officeId}-${zoneName}`
}
export function getOfficeDeskSeats(officeId: string): SeatDef[] {
const desks = ZONES[`${officeId}-workspace`]?.seats ?? []
const leaders = ZONES[`${officeId}-leaderOffice`]?.seats ?? []
return [...desks, ...leaders]
}
export function getOfficeAllSeats(officeId: string): SeatDef[] {
return Object.entries(ZONES)
.filter(([k]) => k.startsWith(`${officeId}-`))
.flatMap(([, z]) => z.seats)
}
export function getAllDeskSeats(): SeatDef[] {
return Object.entries(ZONES)
.filter(([k]) => k.endsWith('-workspace'))
.flatMap(([, z]) => z.seats)
}
export function getMeetingSeats(officeId?: string): SeatDef[] {
if (officeId) return ZONES[`${officeId}-meetingRoom`]?.seats ?? []
return Object.entries(ZONES)
.filter(([k]) => k.endsWith('-meetingRoom'))
.flatMap(([, z]) => z.seats)
}
export function getAllSeats(): SeatDef[] {
return Object.values(ZONES).flatMap(z => z.seats)
}
export function randomTileInZone(zoneKey: string): { x: number; y: number } | null {
const zone = ZONES[zoneKey]
if (!zone) return null
const { x, y, w, h } = zone.bounds
const officeId = zoneKey.split('-').slice(0, 2).join('-')
const offices = getOffices()
const office = offices.find(o => o.id === officeId)
if (!office) return { x: x + 1, y: y + 1 }
const grid = parseOfficeMapStr(office.mapStr)
const off = office.offsetCol
const walkable: { x: number; y: number }[] = []
for (let row = y; row < y + h; row++) {
for (let col = x; col < x + w; col++) {
const localCol = col - off
if (localCol >= 0 && localCol < OFFICE_COLS && row < OFFICE_ROWS && grid[row]?.[localCol] === 0) {
walkable.push({ x: col, y: row })
}
}
}
if (walkable.length === 0) return null
return walkable[Math.floor(Math.random() * walkable.length)]
}
export function getDoorwayTargets(zoneKey: string): { x: number; y: number }[] {
const zone = ZONES[zoneKey]
if (!zone) return []
return zone.doorways.map(d => ({ x: d.tileX, y: d.tileY }))
}
export function getOfficeLobbyDoorways(officeId: string): { x: number; y: number }[] {
const zone = ZONES[`${officeId}-lobby`]
if (!zone) return []
return zone.doorways.map(d => ({ x: d.tileX, y: d.tileY }))
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,159 @@
import { OFFICE_COLS, OFFICE_ROWS, GAP_COLS } from '../config'
export const DEFAULT_MAP_STR: string[] = [
'####################',
'####################',
'####################',
'#######......#######',
'#######..##..#######',
'#######..##..#######',
'#######..##..#######',
'#######......#######',
'#########.##########',
'#########.##########',
'#.............#....#',
'#..................#',
'#.#########....##..#',
'#...........#......#',
'#...........#..##..#',
'#.#########.#......#',
'#...........#......#',
'#...........####.###',
'##########..####.###',
'##########..#......#',
'#...........#..#...#',
'#...####....#.###..#',
'#...........#......#',
'#.##....#####....###',
'####################',
]
export const DEFAULT_SEATS: [number, number][] = [
[8, 4], [8, 5], [8, 6], [11, 4], [11, 5], [11, 6],
[3, 13], [6, 13], [9, 13], [3, 16], [6, 16], [9, 16],
[14, 12], [17, 12], [14, 14], [17, 14],
[15, 22],
]
export interface OfficeConfig {
id: string
name: string
offsetCol: number
mapStr: string[]
seats: [number, number][]
assignedAgents: string[]
}
const STORAGE_KEY = 'office-multi-config'
function makeDefaultOffices(): OfficeConfig[] {
const step = OFFICE_COLS + GAP_COLS
return [
{ id: 'office-0', name: 'Office A', offsetCol: 0, mapStr: [...DEFAULT_MAP_STR], seats: [...DEFAULT_SEATS], assignedAgents: [] },
{ id: 'office-1', name: 'Office B', offsetCol: step, mapStr: [...DEFAULT_MAP_STR], seats: [...DEFAULT_SEATS], assignedAgents: [] },
{ id: 'office-2', name: 'Office C', offsetCol: step * 2, mapStr: [...DEFAULT_MAP_STR], seats: [...DEFAULT_SEATS], assignedAgents: [] },
]
}
export const DEFAULT_OFFICES = makeDefaultOffices()
export function getOffices(): OfficeConfig[] {
try {
const stored = localStorage.getItem(STORAGE_KEY)
if (stored) {
const parsed = JSON.parse(stored) as OfficeConfig[]
if (Array.isArray(parsed) && parsed.length > 0) return parsed
}
} catch { /* ignore */ }
return makeDefaultOffices()
}
export function saveOffices(offices: OfficeConfig[]) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(offices))
}
export function getOfficeById(id: string): OfficeConfig | undefined {
return getOffices().find(o => o.id === id)
}
export function renameOffice(id: string, name: string) {
const offices = getOffices()
const office = offices.find(o => o.id === id)
if (office) {
office.name = name
saveOffices(offices)
}
}
export function assignAgent(officeId: string, agentId: string) {
const offices = getOffices()
for (const o of offices) {
o.assignedAgents = o.assignedAgents.filter(a => a !== agentId)
}
const target = offices.find(o => o.id === officeId)
if (target) target.assignedAgents.push(agentId)
saveOffices(offices)
}
export function unassignAgent(agentId: string) {
const offices = getOffices()
for (const o of offices) {
o.assignedAgents = o.assignedAgents.filter(a => a !== agentId)
}
saveOffices(offices)
}
export function getAgentOffice(agentId: string): OfficeConfig | undefined {
return getOffices().find(o => o.assignedAgents.includes(agentId))
}
export function updateOfficeMap(officeId: string, mapStr: string[], seats: [number, number][]) {
const offices = getOffices()
const office = offices.find(o => o.id === officeId)
if (office) {
office.mapStr = mapStr
office.seats = seats
saveOffices(offices)
}
}
export function parseOfficeMapStr(mapStr: string[]): number[][] {
const grid: number[][] = []
for (let r = 0; r < OFFICE_ROWS; r++) {
const row: number[] = []
const line = r < mapStr.length ? mapStr[r] : '#'.repeat(OFFICE_COLS)
for (let c = 0; c < OFFICE_COLS; c++) {
row.push(c < line.length && line[c] === '.' ? 0 : 1)
}
grid.push(row)
}
return grid
}
export function buildCompositeGrid(offices: OfficeConfig[]): number[][] {
const worldCols = offices.length > 0
? offices[offices.length - 1].offsetCol + OFFICE_COLS
: OFFICE_COLS
const grid: number[][] = []
for (let r = 0; r < OFFICE_ROWS; r++) {
grid.push(new Array(worldCols).fill(1))
}
for (const office of offices) {
const officeGrid = parseOfficeMapStr(office.mapStr)
for (let r = 0; r < OFFICE_ROWS; r++) {
for (let c = 0; c < OFFICE_COLS; c++) {
grid[r][office.offsetCol + c] = officeGrid[r][c]
}
}
}
return grid
}
export function getWorkspaceSeatCount(office: OfficeConfig): number {
let count = 0
for (const [col, row] of office.seats) {
if (row >= 10 && row <= 17 && col < 12) count++
else if (row >= 18 && row <= 23 && col >= 13 && col <= 18) count++
}
return count
}