1import { facingFor, FONT } from './model.js';23/**4 * Draw the ordered object pass for a board.5 *6 * The pass owns stack offsets, transforms, and glyph facing. Board decoration7 * and transient effects stay in the parent renderer, so new entity types can8 * be added without growing the frame and overlay code.9 */10export function drawEntities(ctx, state, model, options, draw) {11 const { geometry, colors, objects, sorted, stacks, activeIds, walls } = model;12 const stackIndices = new Map();13 for (const object of sorted) {14 const key = `${object.x},${object.y}`;15 const index = stackIndices.get(key) || 0;16 stackIndices.set(key, index + 1);17 const count = stacks.get(key) || 1;18 const visual = model.visualOf(object);19 if (object.kind === 'object' && model.wallNouns.includes(object.noun)) {20 draw.wall(ctx, object, geometry, walls, colors, visual);21 continue;22 }23 const offset = count > 1 ? index * 0.09 : 0;24 const cx = geometry.x + (visual.x + 0.5 + offset) * geometry.cell;25 const cy = geometry.y + (visual.y + 0.5 - offset) * geometry.cell;26 const scaleX = Number.isFinite(visual.scaleX) ? visual.scaleX : 1;27 const scaleY = Number.isFinite(visual.scaleY) ? visual.scaleY : 1;28 if (scaleX !== 1 || scaleY !== 1) {29 ctx.save();30 ctx.translate(cx, cy);31 ctx.scale(scaleX, scaleY);32 ctx.translate(-cx, -cy);33 }34 if (object.kind === 'word') {35 ctx.save();36 if (count > 1) ctx.rotate((index % 2 ? 1 : -1) * 0.07);37 if (visual.moving && visual.pushed && Number.isFinite(visual.skid)) {38 ctx.rotate(visual.skid);39 }40 draw.tile(41 ctx,42 object.text,43 cx,44 cy,45 geometry.cell,46 activeIds.has(object.id),47 colors,48 );49 ctx.restore();50 if (scaleX !== 1 || scaleY !== 1) ctx.restore();51 continue;52 }53 const props = model.propertiesById.get(object.id) || new Set();54 const { glyph, mirror } = facingFor(object, visual, options);55 if (props.has('YOU')) {56 ctx.fillStyle = `${colors.YOU}66`;57 ctx.beginPath();58 ctx.ellipse(59 cx,60 geometry.y + (object.y + 0.82) * geometry.cell,61 geometry.cell * 0.26,62 geometry.cell * 0.06,63 0,64 0,65 Math.PI * 2,66 );67 ctx.fill();68 }69 ctx.font = `${Math.max(14, geometry.cell * 0.6)}px "Apple Color Emoji", "Segoe UI Emoji", sans-serif`;70 ctx.textAlign = 'center';71 ctx.textBaseline = 'middle';72 ctx.fillStyle = colors.fg;73 const glyphScaleX = Number.isFinite(visual.glyphScaleX)74 ? visual.glyphScaleX75 : 1;76 // Mirror the emoji from its declared native direction; keep the shadow,77 // cell geometry, and text unchanged.78 if (mirror || glyphScaleX !== 1) {79 ctx.save();80 ctx.translate(cx, cy);81 ctx.scale((mirror ? -1 : 1) * glyphScaleX, 1);82 ctx.translate(-cx, -cy);83 ctx.fillText(glyph, cx, cy);84 ctx.restore();85 } else {86 ctx.fillText(glyph, cx, cy);87 }88 if (count > 2 && index === 0) {89 ctx.font = `800 ${Math.max(10, geometry.cell * 0.16)}px ${FONT}`;90 ctx.fillText(91 `×${count}`,92 geometry.x + (object.x + 0.86) * geometry.cell,93 geometry.y + (object.y + 0.86) * geometry.cell,94 );95 }96 if (scaleX !== 1 || scaleY !== 1) ctx.restore();97 }98}