| ⋯ 5 unchanged lines |
| 6 | 6 | | RING, |
| 7 | 7 | | WALL_SIDES, |
| 8 | 8 | | wallStacks, |
| 9 | | - | type Placed, |
| 10 | 9 | | type Rhombus, |
| 11 | 10 | | type WallProgress, |
| 12 | 11 | | } from '../game/wall'; |
| ⋯ 20 unchanged lines |
| 33 | 32 | | const ring = useRef<HTMLDivElement>(null); |
| 34 | 33 | | const { opening, cell, shape } = useSquare(ring); |
| 35 | 34 | | const sides = ringLayout(wallStacks(state), RING); |
| 36 | | - | const { slide, take } = useSliding(ring, shape, cell, sides); |
| 35 | + | const { slide, take } = useSliding(ring, shape, cell); |
| 36 | + | // Where they have been pushed to, for `/wall.html` to read back off the DOM |
| 37 | + | // rather than have it threaded through as a prop nothing else wants. |
| 38 | + | const nudged = slide.map((n) => `${Math.round(n.along)},${Math.round(n.across)}`).join(' '); |
| 37 | 39 | | |
| 38 | 40 | | return ( |
| 39 | 41 | | <div |
| 40 | 42 | | className="wall-ring" |
| 41 | 43 | | ref={ring} |
| 44 | + | data-nudged={nudged} |
| 42 | 45 | | aria-label={t('牌牆', 'The wall')} |
| 43 | 46 | | style={ |
| 44 | 47 | | opening |
| ⋯ 24 unchanged lines |
| 69 | 72 | | '--vx': `${shape.places[side].x}px`, |
| 70 | 73 | | '--vy': `${shape.places[side].y}px`, |
| 71 | 74 | | '--dir': `${shape.places[side].dir}deg`, |
| 72 | | - | '--slide': `${slide[side]}px`, |
| 75 | + | '--along': `${slide[side].along}px`, |
| 76 | + | '--across': `${slide[side].across}px`, |
| 73 | 77 | | } as React.CSSProperties) |
| 74 | 78 | | : undefined |
| 75 | 79 | | } |
| ⋯ 30 unchanged lines |
| 106 | 110 | | * is still the next draw, exactly as washing the discards about moves no |
| 107 | 111 | | * tile that anybody is counting. |
| 108 | 112 | | * |
| 109 | | - | * A wall slides only into the room it has: from where it was built to where its |
| 110 | | - | * own stacks run out. It cannot leave its line, and it cannot climb over the |
| 111 | | - | * one beside it. |
| 113 | + | * Both ways, and off its line as well as along it: the point of being able to |
| 114 | + | * push one is finding where it should have been standing, and a wall held to |
| 115 | + | * its own line can only answer half of that. `along` runs the way the wall |
| 116 | + | * points, `across` is square to it — its own frame, so a wall at any angle |
| 117 | + | * moves the way the finger does. |
| 112 | 118 | | * |
| 113 | 119 | | * The pool is told, because a wall that has moved is a wall a thrown tile |
| 114 | 120 | | * bounces off somewhere else, and the pool only re-measures when it has reason |
| ⋯ 3 unchanged lines |
| 118 | 124 | | ring: React.RefObject<HTMLDivElement | null>, |
| 119 | 125 | | shape: Rhombus | null, |
| 120 | 126 | | cell: number, |
| 121 | | - | sides: Placed[][], |
| 122 | 127 | | ) { |
| 123 | | - | const [slide, setSlide] = useState<number[]>([0, 0, 0, 0]); |
| 124 | | - | const drag = useRef<{ side: number; ax: number; ay: number; from: number; at: number } | null>( |
| 125 | | - | null, |
| 126 | | - | ); |
| 128 | + | const [slide, setSlide] = useState<Nudge[]>(() => [0, 1, 2, 3].map(() => ({ along: 0, across: 0 }))); |
| 129 | + | const drag = useRef<{ |
| 130 | + | side: number; |
| 131 | + | ax: number; |
| 132 | + | ay: number; |
| 133 | + | fromAlong: number; |
| 134 | + | fromAcross: number; |
| 135 | + | at: Nudge; |
| 136 | + | } | null>(null); |
| 127 | 137 | | // What the drag needs to know, kept current without the listeners below |
| 128 | 138 | | // having to be torn down and put back on every render to see it. |
| 129 | | - | const now = useRef({ shape, cell, sides, slide }); |
| 130 | | - | now.current = { shape, cell, sides, slide }; |
| 131 | | - | |
| 132 | | - | // A wall that has lost stacks has exactly that much line to be pushed along. |
| 133 | | - | const roomOn = (side: number) => |
| 134 | | - | Math.max(0, (STACKS_PER_SIDE - now.current.sides[side].length) * now.current.cell); |
| 139 | + | const now = useRef({ shape, cell, slide }); |
| 140 | + | now.current = { shape, cell, slide }; |
| 135 | 141 | | |
| 136 | 142 | | const moved = () => ring.current?.dispatchEvent(new CustomEvent('wallmoved', { bubbles: true })); |
| 137 | 143 | | |
| 138 | 144 | | const take = (side: number) => (e: React.PointerEvent<HTMLDivElement>) => { |
| 139 | 145 | | const place = now.current.shape?.places[side]; |
| 140 | | - | if (!place || e.button !== 0 || roomOn(side) <= 0) return; |
| 146 | + | if (!place || e.button !== 0) return; |
| 141 | 147 | | // A wall being pushed, not the table being reached into. |
| 142 | 148 | | e.stopPropagation(); |
| 143 | 149 | | e.preventDefault(); |
| ⋯ 9 unchanged lines |
| 153 | 159 | | side, |
| 154 | 160 | | ax, |
| 155 | 161 | | ay, |
| 156 | | - | from: e.clientX * ax + e.clientY * ay, |
| 162 | + | fromAlong: e.clientX * ax + e.clientY * ay, |
| 163 | + | // Across is the wall's own normal: its direction turned a quarter on. |
| 164 | + | fromAcross: -e.clientX * ay + e.clientY * ax, |
| 157 | 165 | | at: now.current.slide[side], |
| 158 | 166 | | }; |
| 159 | 167 | | }; |
| ⋯ 4 unchanged lines |
| 164 | 172 | | const move = (e: PointerEvent) => { |
| 165 | 173 | | const d = drag.current; |
| 166 | 174 | | if (!d) return; |
| 167 | | - | // Only what the finger did *along* the wall counts; across it, nothing. |
| 168 | | - | const along = e.clientX * d.ax + e.clientY * d.ay; |
| 169 | | - | const to = Math.min(0, Math.max(-roomOn(d.side), d.at + (along - d.from))); |
| 175 | + | // The finger, in the wall's own frame: how far along it, and how far off. |
| 176 | + | const bound = STACKS_PER_SIDE * now.current.cell * 2; |
| 177 | + | const clamp = (v: number) => Math.min(bound, Math.max(-bound, v)); |
| 178 | + | const to: Nudge = { |
| 179 | + | along: clamp(d.at.along + (e.clientX * d.ax + e.clientY * d.ay - d.fromAlong)), |
| 180 | + | across: clamp(d.at.across + (-e.clientX * d.ay + e.clientY * d.ax - d.fromAcross)), |
| 181 | + | }; |
| 170 | 182 | | let changed = false; |
| 171 | 183 | | setSlide((was) => { |
| 172 | | - | if (was[d.side] === to) return was; |
| 184 | + | const had = was[d.side]; |
| 185 | + | if (had.along === to.along && had.across === to.across) return was; |
| 173 | 186 | | changed = true; |
| 174 | 187 | | return was.map((v, i) => (i === d.side ? to : v)); |
| 175 | 188 | | }); |
| ⋯ 18 unchanged lines |
| 194 | 207 | | return { slide, take }; |
| 195 | 208 | | } |
| 196 | 209 | | |
| 210 | + | /** How far a wall has been pushed from where it was built, in its own frame. */ |
| 211 | + | export interface Nudge { |
| 212 | + | along: number; |
| 213 | + | across: number; |
| 214 | + | } |
| 215 | + | |
| 197 | 216 | | interface Square { |
| 198 | 217 | | /** The opening the square leaves, once there is a middle to measure. */ |
| 199 | 218 | | opening: { w: number; h: number } | null; |
| ⋯ 81 unchanged lines |