Initial commit
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user