import { CFG } from './config.js'; import { clamp, random } from './util.js'; import { elementOf } from './elements.js'; let NEXT_ID = 1; export class Particle { constructor(o = {}) { this.id = NEXT_ID++; this.x = o.x ?? 0; this.y = o.y ?? 0; this.vx = 0; this.vy = 0; this.charge = o.charge ?? 0; this.displayCharge = this.charge; // element drives colour / valence / radius / mass unless overridden this.el = o.el || null; const E = this.el ? elementOf(this.el) : null; this.col = E ? E.col : CFG.COL_NEU; this.noble = !!(E && E.noble); this.r = o.r ?? (E ? E.r : CFG.P_RADIUS); this.slots = o.slots ?? (E ? E.slots : 1); this.bonds = []; this.zoneTimer = 0; this.grace = 0; // brief immunity after the player acts this.grabbedBy = null; // 'L' | 'R' | null this.fixed = !!o.fixed; this.tag = o.tag ?? ''; this.mass = o.mass ?? (E ? E.mass : 1); this.spin = random() * Math.PI * 2; this.flash = 0; // ideal angle between two bonds on this atom, radians. see elements.js this.bend = E && E.bend ? (E.bend * Math.PI) / 180 : 0; } get freeSlots() { return Math.max(0, this.slots - this.bonds.length); } get charged() { return this.charge !== 0; } setCharge(c) { c = clamp(c, -1, 1); if (c === this.charge) return false; this.charge = c; this.grace = CFG.ZONE_GRACE; this.zoneTimer = 0; this.flash = 1; return true; } addForce(fx, fy, dt) { if (this.fixed) return; this.vx += (fx / this.mass) * dt; this.vy += (fy / this.mass) * dt; } addImpulse(ix, iy) { if (this.fixed) return; this.vx += ix / this.mass; this.vy += iy / this.mass; } isBondedTo(p) { return this.bonds.includes(p); } } export function bond(a, b) { if (a === b || a.isBondedTo(b)) return false; a.bonds.push(b); b.bonds.push(a); return true; } export function unbond(a, b) { const i = a.bonds.indexOf(b); if (i >= 0) a.bonds.splice(i, 1); const j = b.bonds.indexOf(a); if (j >= 0) b.bonds.splice(j, 1); }