anvilsign in

collin/mahjong · 4d0d70a0

Draw what a throw was aimed at against where it was actually sent

Collin Richards · 2026-08-21 18:55 UTC · 4d0d70a02c066e39e8fa0ee1936ebba02cdcd21f · parent 90552988 · browse files

modifiedREADME.md+1 −0
⋯ 519 unchanged lines
520520 | box hugging each pool tile | the rotated rectangle it really collides with |
521521 | dotted box round the moving tile | the upright box the straight edges use instead. It comes apart from the rotated one as the tile turns |
522522
523+| blue rays from a throw | faint: the flick, as the tile was let go of. Bright: the line it was actually **sent** along, labelled with why. A throw with standing wall in front of it is lobbed rather than slid, and one whose line misses the square is re-aimed at the middle of it — the tile then leaves on a line nobody threw, which reads exactly like a bounce off something invisible |
523524 | yellow cross | where contact was *actually* made, fading over a second or so. One of these sitting in open felt with no box under it is a bounce off something that isn't a collider at all — a different bug from a box in the wrong place |
524525
525526 Colliders mode is also a **throwing sandbox**: a flick throws for real but is not a
⋯ 47 unchanged lines
modifiedsrc/table/pool.ts+29 −0
⋯ 105 unchanged lines
106106 h: number;
107107 }
108108
109+/** One throw, as the overlay wants to see it: let go of at (x, y) along
110+ * (fx, fy), and actually sent along (vx, vy). `how` says why they differ. */
111+export interface ThrowMark {
112+ x: number;
113+ y: number;
114+ fx: number;
115+ fy: number;
116+ vx: number;
117+ vy: number;
118+ how: string;
119+}
120+
109121 /** A throw that has been let go of, waiting for the discard it belongs to. */
110122 interface Release {
111123 seat: SeatId;
⋯ 35 unchanged lines
147159 /** Tiles on their way off the table. Drawn, but out of the physics. */
148160 private leaving: Leaving[] = [];
149161 /**
162+ * The last few throws: where each was let go of, the line it was let go
163+ * *along*, and the line it actually left on.
164+ *
165+ * Read by the collider overlay and by nothing else. A throw is not always
166+ * sent where it was aimed — a flick with standing wall in front of it is
167+ * lobbed rather than slid, and one whose line misses the square altogether is
168+ * re-aimed at the middle of it — and a tile that leaves on a line nobody
169+ * threw reads exactly like a tile that bounced off something invisible.
170+ */
171+ throws: ThrowMark[] = [];
172+ /**
150173 * Sandbox throws: real tiles in the real world, but belonging to no discard,
151174 * so the reconciler never looks at them — nothing in `entries` means nothing
152175 * to cull. See `throwLoose`.
⋯ 469 unchanged lines
622645 const speed = Math.hypot(vx, vy);
623646 let flipTurns = 0;
624647 let flipOver = 0;
648+ let how = set ? 'slide, from inside the square' : 'slide';
625649
626650 if (!slide) {
627651 // Lofted over a standing wall, and it has to come *down* inside the
⋯ 5 unchanged lines
633657 // limp one drops in over the near wall, a hard one carries to the far
634658 // side and barges through whatever is lying there, and neither of them
635659 // can leave the tile outside.
660+ how = 'lob';
636661 const dir = speed > 0 ? { x: vx / speed, y: vy / speed } : { x: 0, y: -1 };
637662 const span = spanThrough(geo.pool, thrown, dir);
638663 const centre = centreOf(geo.pool);
⋯ 3 unchanged lines
642667 carry = Math.min(Math.max(near, carry), Math.max(near, span.far - geo.tile.h));
643668 } else {
644669 // Flicked somewhere that is not the square at all — put it in anyway.
670+ how = 'lob, RE-AIMED at the middle';
645671 carry = Math.hypot(centre.x - thrown.x, centre.y - thrown.y);
646672 dir.x = (centre.x - thrown.x) / (carry || 1);
647673 dir.y = (centre.y - thrown.y) / (carry || 1);
⋯ 14 unchanged lines
662688 }
663689 if (!TUMBLE) flipTurns = 0;
664690
691+ this.throws.push({ x: thrown.x, y: thrown.y, fx: thrown.vx, fy: thrown.vy, vx, vy, how });
692+ if (this.throws.length > 6) this.throws.shift();
693+
665694 return addBody(this.world!, {
666695 tile: tile,
667696 seat: thrown.seat,
⋯ 81 unchanged lines
modifiedsrc/ui/Pool.tsx+23 −0
⋯ 452 unchanged lines
453453 ctx.fillRect(live.x - 1.5, live.y - 1.5, 3, 3);
454454 }
455455
456+ // How the last few throws actually left. Faint line: the flick, as it was
457+ // let go of. Bright line: where the tile was actually sent. When those two
458+ // disagree the tile leaves on a line nobody threw, which is the other way a
459+ // bounce can be a bounce off nothing — see TablePool.throws.
460+ ctx.font = '9px ui-monospace, monospace';
461+ ctx.textBaseline = 'bottom';
462+ for (const t of pool.throws) {
463+ const ray = (dx: number, dy: number, len: number) => {
464+ const d = Math.hypot(dx, dy) || 1;
465+ ctx.beginPath();
466+ ctx.moveTo(t.x, t.y);
467+ ctx.lineTo(t.x + (dx / d) * len, t.y + (dy / d) * len);
468+ ctx.stroke();
469+ };
470+ ctx.strokeStyle = 'rgba(90, 200, 250, 0.35)';
471+ ray(t.fx, t.fy, 90);
472+ ctx.strokeStyle = 'rgba(90, 200, 250, 0.9)';
473+ ray(t.vx, t.vy, 130);
474+ ctx.fillStyle = 'rgba(90, 200, 250, 0.9)';
475+ ctx.fillText(t.how, t.x + 6, t.y - 4);
476+ ctx.fillRect(t.x - 2, t.y - 2, 4, 4);
477+ }
478+
456479 // Where contact was actually made, fading out. One of these with no box under
457480 // it is the interesting case: a bounce off something that is not drawn is a
458481 // bounce off something that is not a collider.
⋯ 223 unchanged lines