Files
the-sims-online-2d/js/world.js
T

343 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ============================================================
* world.js — Lot: tiles, walls, object placement, pathfinding
* ============================================================ */
'use strict';
const ekey = (x, y, e) => `${x},${y},${e}`; // wall edge key ('n' | 'w')
const ckey = (x, y) => x + ',' + y;
const DIAGS = [{ dx: 1, dy: 1 }, { dx: -1, dy: 1 }, { dx: 1, dy: -1 }, { dx: -1, dy: -1 }];
class World {
constructor(w = LOT_W, h = LOT_H) {
this.w = w; this.h = h;
this.floor = new Array(w * h).fill(0); // index into FLOORS (0=grass)
this.walls = new Map(); // ekey -> {kind:'wall'|'door'|'window', color}
this.objects = []; // placed GameObjects
this.cellObj = new Map(); // ckey -> object
this.roomScore = new Array(w * h).fill(50); // environment score per tile
this.dirtPuddle = []; // fading puddles
this.mailbox = { x: Math.floor(w / 2), y: h - 1 }; // visual + bills flavor
}
inside(x, y) { return x >= 0 && y >= 0 && x < this.w && y < this.h; }
/* ---------------- walls ---------------- */
wallAt(x, y, e) { return this.walls.get(ekey(x, y, e)); }
/** Edge between two orthogonal neighbors */
sharedEdge(ax, ay, bx, by) {
if (bx === ax + 1) return { x: bx, y: by, e: 'w' };
if (bx === ax - 1) return { x: ax, y: ay, e: 'w' };
if (by === ay + 1) return { x: ax, y: by, e: 'n' };
if (by === ay - 1) return { x: ax, y: ay, e: 'n' };
return null;
}
edgeBlocked(ax, ay, bx, by) {
const ed = this.sharedEdge(ax, ay, bx, by);
if (!ed) return true;
const w = this.wallAt(ed.x, ed.y, ed.e);
return !!w && w.kind !== 'door';
}
placeWall(x, y, e, kind = 'wall', silent = false) {
if (!this.inside(x, y) && !(e === 'n' && y === this.h)) return false;
const cur = this.wallAt(x, y, e);
if (cur && cur.kind === kind && cur.color === G.wallColor) return false;
this.walls.set(ekey(x, y, e), { kind, color: G.wallColor });
if (!silent) Bus.emit('worldChanged');
return true;
}
removeWall(x, y, e) {
const k = ekey(x, y, e);
if (this.walls.has(k)) { this.walls.delete(k); Bus.emit('worldChanged'); return true; }
return false;
}
setFloor(x, y, fid) {
if (!this.inside(x, y)) return;
this.floor[y * this.w + x] = fid;
Bus.emit('worldChanged');
}
/* ---------------- objects ---------------- */
objCells(obj) {
const cells = [];
for (let dy = 0; dy < obj.h; dy++)
for (let dx = 0; dx < obj.w; dx++)
cells.push([obj.x + dx, obj.y + dy]);
return cells;
}
objAt(x, y) {
const o = this.cellObj.get(ckey(x, y));
return o || null;
}
canPlace(def, x, y, rot) {
const w = rot ? def.h : def.w, h = rot ? def.w : def.h;
for (let dy = 0; dy < h; dy++) for (let dx = 0; dx < w; dx++) {
const tx = x + dx, ty = y + dy;
if (!this.inside(tx, ty)) return false;
if (this.objAt(tx, ty)) return false;
// don't allow placing on a tile occupied by a sim body
for (const s of G.sims) {
if (!s.atHome) continue;
if (Math.round(s.x) === tx && Math.round(s.y) === ty) return false;
}
// wall objects must hug a wall edge behind them (mirror/painting)
if (def.wallObj) {
const hasWall = this.wallAt(tx, ty, 'n') || this.wallAt(tx - 1, ty, 'w') ||
this.wallAt(tx, ty + 1, 'n') || this.wallAt(tx + 1, ty, 'w');
if (!hasWall) return false;
}
}
return true;
}
placeObject(defId, x, y, rot = 0, opts = {}) {
const def = OBJECTS[defId];
if (!def) return null;
const obj = {
id: uid(), defId, x, y,
rot,
w: rot ? def.h : def.w,
h: rot ? def.w : def.h,
usedBy: null, // sim currently using
dirty: 0, // toilets / trash fill level 0..1
lightOn: false,
...opts,
};
this.objects.push(obj);
for (const [cx, cy] of this.objCells(obj)) this.cellObj.set(ckey(cx, cy), obj);
Bus.emit('worldChanged');
Bus.emit('objectsChanged');
return obj;
}
removeObject(obj) {
if (!obj) return;
this.objects = this.objects.filter(o => o !== obj);
for (const [cx, cy] of this.objCells(obj)) {
if (this.cellObj.get(ckey(cx, cy)) === obj) this.cellObj.delete(ckey(cx, cy));
}
if (obj.usedBy && obj.usedBy.action) obj.usedBy.cancelAction('Object sold');
Bus.emit('worldChanged');
Bus.emit('objectsChanged');
}
findFreeSpotNear(x, y, maxR = 8) {
for (let r = 1; r <= maxR; r++) {
const cands = [];
for (let dy = -r; dy <= r; dy++) for (let dx = -r; dx <= r; dx++) {
if (Math.max(Math.abs(dx), Math.abs(dy)) !== r) continue;
const tx = x + dx, ty = y + dy;
if (this.inside(tx, ty) && this.tileWalkable(tx, ty)) cands.push([tx, ty]);
}
if (cands.length) return choice(cands);
}
return null;
}
/** best standing tile adjacent to an object's footprint */
useSpotNear(obj, fromX, fromY) {
let best = null, bd = 1e9;
for (let dy = -1; dy <= obj.h; dy++) for (let dx = -1; dx <= obj.w; dx++) {
const onFootprint = dx >= 0 && dx < obj.w && dy >= 0 && dy < obj.h;
if (onFootprint) continue;
const tx = obj.x + dx, ty = obj.y + dy;
if (!this.inside(tx, ty) || !this.tileWalkable(tx, ty)) continue;
const d = dist2(tx, ty, fromX, fromY);
if (d < bd) { bd = d; best = [tx, ty]; }
}
return best;
}
findObjects(pred) { return this.objects.filter(pred); }
/* ---------------- walkability & pathfinding ---------------- */
tileWalkable(x, y) {
if (!this.inside(x, y)) return false;
if (this.objAt(x, y)) return false;
return true;
}
/** A* path from (sx,sy) to (tx,ty). 8-directional, no corner cutting.
* Returns array of [x,y] incl. endpoints, or null. */
findPath(sx, sy, tx, ty) {
sx = Math.round(sx); sy = Math.round(sy); tx = Math.round(tx); ty = Math.round(ty);
if (!this.inside(sx, sy)) return null;
if (!this.inside(tx, ty) || !this.tileWalkable(tx, ty)) {
const alt = this.findFreeSpotNear(tx, ty, 6);
if (!alt) return null;
tx = alt[0]; ty = alt[1];
}
if (sx === tx && sy === ty) return [[tx, ty]];
const open = [{ x: sx, y: sy, g: 0, f: Math.sqrt(dist2(sx, sy, tx, ty)), parent: null }];
const seen = new Map([[ckey(sx, sy), 0]]);
let goal = null, guard = 0;
while (open.length && guard++ < 9000) {
let bi = 0;
for (let i = 1; i < open.length; i++) if (open[i].f < open[bi].f) bi = i;
const n = open.splice(bi, 1)[0];
if (n.x === tx && n.y === ty) { goal = n; break; }
for (let di = 0; di < 8; di++) {
const diag = di >= 4;
const d = diag ? DIAGS[di - 4] : DIRS[di];
const nx = n.x + d.dx, ny = n.y + d.dy;
if (!this.inside(nx, ny) || !this.tileWalkable(nx, ny)) continue;
if (diag) {
// both orthogonal legs must be open (tiles + edges)
if (!this.tileWalkable(n.x + d.dx, n.y)) continue;
if (!this.tileWalkable(n.x, n.y + d.dy)) continue;
if (this.edgeBlocked(n.x, n.y, n.x + d.dx, n.y)) continue;
if (this.edgeBlocked(n.x, n.y, n.x, n.y + d.dy)) continue;
if (this.edgeBlocked(n.x + d.dx, n.y, nx, ny)) continue;
if (this.edgeBlocked(n.x, n.y + d.dy, nx, ny)) continue;
} else if (this.edgeBlocked(n.x, n.y, nx, ny)) continue;
const g = n.g + (diag ? 1.45 : 1);
const k = ckey(nx, ny);
if (seen.has(k) && seen.get(k) <= g) continue;
seen.set(k, g);
open.push({ x: nx, y: ny, g, f: g + Math.sqrt(dist2(nx, ny, tx, ty)), parent: n });
}
}
if (!goal) return null;
const path = [];
for (let n = goal; n; n = n.parent) path.unshift([n.x, n.y]);
return path;
}
/* ---------------- environment score ---------------- */
recomputeRoom() {
const W = this.w, H = this.h;
this.roomScore.fill(28); // bare-lot baseline
// floors & walls make rooms feel finished
for (let y = 0; y < H; y++) for (let x = 0; x < W; x++) {
let s = 24;
if (this.floor[y * W + x] > 0) s += 14;
if (this.wallAt(x, y, 'n')) s += 5;
if (this.wallAt(x, y, 'w')) s += 5;
this.roomScore[y * W + x] += s * 0.35;
}
// object auras
for (const o of this.objects) {
const def = OBJECTS[o.defId];
const env = def.env || 0;
if (!env) continue;
const cx = o.x + o.w / 2, cy = o.y + o.h / 2, R = 6;
for (let y = Math.max(0, Math.floor(cy - R)); y <= Math.min(H - 1, cy + R); y++)
for (let x = Math.max(0, Math.floor(cx - R)); x <= Math.min(W - 1, cx + R); x++) {
const d = Math.sqrt(dist2(x + .5, y + .5, cx, cy));
if (d > R) continue;
this.roomScore[y * W + x] += env * (1 - d / R) * 0.9;
}
}
// dirt stinks
for (const o of this.objects) {
if ((o.defId === 'trash' || o.defId === 'toilet') && o.dirty > 0.5) {
for (let dy = -3; dy <= 3; dy++) for (let dx = -3; dx <= 3; dx++) {
const x = o.x + dx, y = o.y + dy;
if (this.inside(x, y))
this.roomScore[y * W + x] -= (1 - Math.sqrt(dx * dx + dy * dy) / 4) * 22 * o.dirty;
}
}
}
// dirty dishes stink up the place too
const dTotal = (typeof dishTotal === 'function') ? dishTotal() : 0;
if (dTotal > 0) {
const penalty = Math.min(26, dTotal * 2.2);
for (const p of G.dishPiles) {
for (let dy = -3; dy <= 3; dy++) for (let dx = -3; dx <= 3; dx++) {
const x = p.x + dx, y = p.y + dy;
if (this.inside(x, y))
this.roomScore[y * W + x] -= penalty * Math.max(0, 1 - Math.sqrt(dx * dx + dy * dy) / 4) / Math.max(1, p.n);
}
}
}
for (let i = 0; i < this.roomScore.length; i++)
this.roomScore[i] = clamp(this.roomScore[i], 0, 100);
}
roomAt(x, y) {
x = clamp(Math.round(x), 0, this.w - 1); y = clamp(Math.round(y), 0, this.h - 1);
return this.roomScore[y * this.w + x];
}
serialize() {
return {
w: this.w, h: this.h,
floor: Array.from(this.floor),
walls: Array.from(this.walls.entries()),
roomScore: Array.from(this.roomScore),
objects: this.objects.map(o => ({ id:o.id, defId:o.defId, x:o.x, y:o.y, rot:o.rot, dirty:o.dirty, broken:!!o.broken, groceries:o.groceries||0 })),
mailbox: this.mailbox,
};
}
static deserialize(d) {
const wd = new World(d.w, d.h);
wd.floor = d.floor.slice();
wd.walls = new Map(d.walls);
wd.roomScore = d.roomScore ? d.roomScore.slice() : wd.roomScore;
wd.mailbox = d.mailbox;
for (const od of d.objects) {
const def = OBJECTS[od.defId]; if (!def) continue;
const o = wd.placeObject(od.defId, od.x, od.y, od.rot);
if (o) { o.dirty = od.dirty || 0; o.broken = !!od.broken; if (od.groceries) o.groceries = od.groceries; }
}
return wd;
}
}
/* ============================================================
* Starter house — cozy 1-bed bungalow so play starts instantly
* ============================================================ */
function buildStarterHouse(world) {
const X0 = 11, Y0 = 10, Wd = 11, Ht = 9; // outer rect
// floors: wood main, tile bath/kitchen
for (let y = Y0; y < Y0 + Ht; y++)
for (let x = X0; x < X0 + Wd; x++)
world.setFloor(x, y, 1);
// walls — outer shell sits ON the boundary lines around tiles [X0..X0+Wd-1]×[Y0..Y0+Ht-1]
for (let x = X0; x < X0 + Wd; x++) {
world.placeWall(x, Y0, 'n', 'wall', true); // north (top)
world.placeWall(x, Y0 + Ht, 'n', 'wall', true); // south (bottom)
}
for (let y = Y0; y < Y0 + Ht; y++) {
world.placeWall(X0, y, 'w', 'wall', true); // west (left)
world.placeWall(X0 + Wd, y, 'w', 'wall', true); // east (right)
}
// interior walls: bath top-left (3x4), bedroom right side
const BX = X0, BY = Y0, BW = 3, BH = 4; // bathroom zone
for (let x = BX; x < BX + BW; x++) world.placeWall(x, BY + BH, 'n', 'wall', true);
for (let y = BY; y < BY + BH; y++) world.placeWall(BX + BW, y, 'w', 'wall', true);
const RX = X0 + 7; // bedroom divider
for (let y = Y0 + 4; y < Y0 + Ht; y++) world.placeWall(RX, y, 'w', 'wall', true);
// doors: front door south center, bath door, bedroom door
world.placeWall(X0 + 5, Y0 + Ht, 'n', 'door', true);
world.placeWall(BX + 1, BY + BH, 'n', 'door', true);
world.placeWall(RX, Y0 + 5, 'w', 'door', true);
// windows
for (const [wx, wy, we] of [[X0 + 3, Y0, 'n'], [X0 + 7, Y0, 'n'], [X0, Y0 + 6, 'w'], [X0 + Wd, Y0 + 2, 'w']])
world.placeWall(wx, wy, we, 'window', true);
const P = (id, x, y, rot = 0) => world.placeObject(id, x, y, rot);
// bathroom
P('toilet', X0, Y0); P('shower', X0 + 1, Y0); P('sink', X0 + 2, Y0);
P('mirror', X0 + 2, Y0 + 1);
// bedroom (cols 19..21)
P('bedDouble', X0 + 8, Y0 + 6); P('lamp', X0 + 8, Y0 + 4);
// kitchen along bottom, gaps at x=X0+3 and x=X0+6 keep lanes open
P('fridge', X0, Y0 + Ht - 1); P('stove', X0 + 1, Y0 + Ht - 1);
P('counter', X0 + 2, Y0 + Ht - 1);
P('trash', X0 + 4, Y0 + Ht - 1);
// dining
P('table', X0 + 4, Y0 + 6); P('chair', X0 + 3, Y0 + 6, 1); P('chair', X0 + 4, Y0 + 5);
// living room (west), TV tucked along south wall with open approach
P('tv', X0, Y0 + 7);
P('sofa', X0 + 2, Y0 + 7, 1);
P('coffeeTable', X0, Y0 + 5);
P('bookshelf', X0 + 6, Y0 + 1); P('phone', X0 + 3, Y0 + 1);
P('plant', X0 + 7, Y0 + 3); P('lamp', X0, Y0 + 4);
// outside decor
P('plant', X0 - 1, Y0 + Ht); P('plant', X0 + Wd, Y0 + Ht);
// paint bath + kitchen tile
for (let y = Y0; y < Y0 + 4; y++) for (let x = X0; x < X0 + 3; x++) world.setFloor(x, y, 2);
for (let x = X0; x <= X0 + 4; x++) world.setFloor(x, Y0 + Ht - 1, 2);
// mailbox by the front walk
world.mailbox = { x: X0 + 6, y: Y0 + Ht + 2 };
world.recomputeRoom();
}