|
| 1 | +// Name: RPG Collisions |
| 2 | +// ID: rpgcollisions |
| 3 | +// Description: Tag sprites as Wall, Player, or None and get automatic RPG-style solid collisions — no more walking through walls. |
| 4 | +// By: Gatoc_Dev |
| 5 | +// License: MIT |
| 6 | + |
| 7 | +(function (Scratch) { |
| 8 | + 'use strict'; |
| 9 | + |
| 10 | + if (!Scratch.extensions.unsandboxed) { |
| 11 | + throw new Error('RPG Collisions must run unsandboxed'); |
| 12 | + } |
| 13 | + |
| 14 | + const vm = Scratch.vm; |
| 15 | + const runtime = vm.runtime; |
| 16 | + |
| 17 | + const TAG_WALL = 'wall'; |
| 18 | + const TAG_PLAYER = 'player'; |
| 19 | + const TAG_NONE = 'none'; |
| 20 | + |
| 21 | + const ICON = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgdmlld0JveD0iMCAwIDQwIDQwIj4KPHJlY3QgeD0iMSIgeT0iMSIgd2lkdGg9IjM4IiBoZWlnaHQ9IjM4IiByeD0iOCIgZmlsbD0iIzRDQUY1MCIgc3Ryb2tlPSIjMkU3RDMyIiBzdHJva2Utd2lkdGg9IjIiLz4KPHJlY3QgeD0iNyIgeT0iOSIgd2lkdGg9IjEwIiBoZWlnaHQ9IjYiIGZpbGw9IiNFOEY1RTkiIHN0cm9rZT0iIzJFN0QzMiIgc3Ryb2tlLXdpZHRoPSIxIi8+CjxyZWN0IHg9IjE5IiB5PSI5IiB3aWR0aD0iMTAiIGhlaWdodD0iNiIgZmlsbD0iI0U4RjVFOSIgc3Ryb2tlPSIjMkU3RDMyIiBzdHJva2Utd2lkdGg9IjEiLz4KPHJlY3QgeD0iMTMiIHk9IjE3IiB3aWR0aD0iMTAiIGhlaWdodD0iNiIgZmlsbD0iI0U4RjVFOSIgc3Ryb2tlPSIjMkU3RDMyIiBzdHJva2Utd2lkdGg9IjEiLz4KPHJlY3QgeD0iMjUiIHk9IjE3IiB3aWR0aD0iOCIgaGVpZ2h0PSI2IiBmaWxsPSIjRThGNUU5IiBzdHJva2U9IiMyRTdEMzIiIHN0cm9rZS13aWR0aD0iMSIvPgo8cmVjdCB4PSI3IiB5PSIxNyIgd2lkdGg9IjQiIGhlaWdodD0iNiIgZmlsbD0iI0U4RjVFOSIgc3Ryb2tlPSIjMkU3RDMyIiBzdHJva2Utd2lkdGg9IjEiLz4KPHJlY3QgeD0iNyIgeT0iMjUiIHdpZHRoPSIxMCIgaGVpZ2h0PSI2IiBmaWxsPSIjRThGNUU5IiBzdHJva2U9IiMyRTdEMzIiIHN0cm9rZS13aWR0aD0iMSIvPgo8cmVjdCB4PSIxOSIgeT0iMjUiIHdpZHRoPSIxMCIgaGVpZ2h0PSI2IiBmaWxsPSIjRThGNUU5IiBzdHJva2U9IiMyRTdEMzIiIHN0cm9rZS13aWR0aD0iMSIvPgo8Y2lyY2xlIGN4PSIzMCIgY3k9IjMwIiByPSI0LjUiIGZpbGw9IiMxQjVFMjAiLz4KPC9zdmc+Cg=='; |
| 22 | + |
| 23 | + class RPGCollisions { |
| 24 | + constructor() { |
| 25 | + this.MICRO_STEP = 2; |
| 26 | + } |
| 27 | + |
| 28 | + getInfo() { |
| 29 | + return { |
| 30 | + id: 'rpgcollisions', |
| 31 | + name: 'RPG Collisions', |
| 32 | + color1: '#4CAF50', |
| 33 | + color2: '#43A047', |
| 34 | + color3: '#2E7D32', |
| 35 | + blockIconURI: ICON, |
| 36 | + menuIconURI: ICON, |
| 37 | + blocks: [ |
| 38 | + { |
| 39 | + opcode: 'setTag', |
| 40 | + blockType: Scratch.BlockType.COMMAND, |
| 41 | + text: 'tag this sprite as [TAG]', |
| 42 | + arguments: { |
| 43 | + TAG: { type: Scratch.ArgumentType.STRING, menu: 'tags', defaultValue: TAG_WALL } |
| 44 | + } |
| 45 | + }, |
| 46 | + { |
| 47 | + opcode: 'getTag', |
| 48 | + blockType: Scratch.BlockType.REPORTER, |
| 49 | + text: 'tag of this sprite' |
| 50 | + }, |
| 51 | + { |
| 52 | + opcode: 'isSpriteTagged', |
| 53 | + blockType: Scratch.BlockType.BOOLEAN, |
| 54 | + text: '[SPRITE] is tagged as [TAG] ?', |
| 55 | + arguments: { |
| 56 | + SPRITE: { type: Scratch.ArgumentType.STRING, menu: 'spriteMenu' }, |
| 57 | + TAG: { type: Scratch.ArgumentType.STRING, menu: 'tags', defaultValue: TAG_WALL } |
| 58 | + } |
| 59 | + }, |
| 60 | + { |
| 61 | + opcode: 'enableCollisions', |
| 62 | + blockType: Scratch.BlockType.COMMAND, |
| 63 | + text: 'enable this sprite from rpg collisions' |
| 64 | + }, |
| 65 | + { |
| 66 | + opcode: 'disableCollisions', |
| 67 | + blockType: Scratch.BlockType.COMMAND, |
| 68 | + text: 'disable this sprite from rpg collisions' |
| 69 | + }, |
| 70 | + { |
| 71 | + opcode: 'collisionsEnabled', |
| 72 | + blockType: Scratch.BlockType.BOOLEAN, |
| 73 | + text: 'rpg collisions enabled for this sprite?' |
| 74 | + }, |
| 75 | + '---', |
| 76 | + { |
| 77 | + opcode: 'moveAvoidingWalls', |
| 78 | + blockType: Scratch.BlockType.COMMAND, |
| 79 | + text: 'move [STEPS] steps avoiding walls', |
| 80 | + arguments: { |
| 81 | + STEPS: { type: Scratch.ArgumentType.NUMBER, defaultValue: 10 } |
| 82 | + } |
| 83 | + }, |
| 84 | + { |
| 85 | + opcode: 'stepInDirection', |
| 86 | + blockType: Scratch.BlockType.COMMAND, |
| 87 | + text: 'step [STEPS] steps in direction [DIR] avoiding walls', |
| 88 | + arguments: { |
| 89 | + STEPS: { type: Scratch.ArgumentType.NUMBER, defaultValue: 10 }, |
| 90 | + DIR: { type: Scratch.ArgumentType.ANGLE, defaultValue: 90 } |
| 91 | + } |
| 92 | + }, |
| 93 | + { |
| 94 | + opcode: 'moveTowardsAvoidingWalls', |
| 95 | + blockType: Scratch.BlockType.COMMAND, |
| 96 | + text: 'move up to [STEPS] steps towards x: [X] y: [Y] avoiding walls', |
| 97 | + arguments: { |
| 98 | + STEPS: { type: Scratch.ArgumentType.NUMBER, defaultValue: 10 }, |
| 99 | + X: { type: Scratch.ArgumentType.NUMBER, defaultValue: 0 }, |
| 100 | + Y: { type: Scratch.ArgumentType.NUMBER, defaultValue: 0 } |
| 101 | + } |
| 102 | + }, |
| 103 | + { |
| 104 | + opcode: 'pushOutOfWalls', |
| 105 | + blockType: Scratch.BlockType.COMMAND, |
| 106 | + text: 'push out of any wall I\'m stuck in' |
| 107 | + }, |
| 108 | + '---', |
| 109 | + { |
| 110 | + opcode: 'touchingWall', |
| 111 | + blockType: Scratch.BlockType.BOOLEAN, |
| 112 | + text: 'touching a wall?' |
| 113 | + }, |
| 114 | + { |
| 115 | + opcode: 'wouldTouchWallAt', |
| 116 | + blockType: Scratch.BlockType.BOOLEAN, |
| 117 | + text: 'would touch a wall at x: [X] y: [Y] ?', |
| 118 | + arguments: { |
| 119 | + X: { type: Scratch.ArgumentType.NUMBER, defaultValue: 0 }, |
| 120 | + Y: { type: Scratch.ArgumentType.NUMBER, defaultValue: 0 } |
| 121 | + } |
| 122 | + }, |
| 123 | + { |
| 124 | + opcode: 'whenTouchingWall', |
| 125 | + blockType: Scratch.BlockType.HAT, |
| 126 | + text: 'when I touch a wall', |
| 127 | + isEdgeActivated: true |
| 128 | + } |
| 129 | + ], |
| 130 | + menus: { |
| 131 | + tags: { |
| 132 | + acceptReporters: false, |
| 133 | + items: [ |
| 134 | + { text: 'wall', value: TAG_WALL }, |
| 135 | + { text: 'player', value: TAG_PLAYER }, |
| 136 | + { text: 'none (decoration/UI)', value: TAG_NONE } |
| 137 | + ] |
| 138 | + }, |
| 139 | + spriteMenu: { |
| 140 | + acceptReporters: true, |
| 141 | + items: 'getSpriteMenu' |
| 142 | + } |
| 143 | + } |
| 144 | + }; |
| 145 | + } |
| 146 | + |
| 147 | + // ---------- menu helpers ---------- |
| 148 | + |
| 149 | + getSpriteMenu() { |
| 150 | + const names = runtime.targets |
| 151 | + .filter(t => t.isOriginal && !t.isStage) |
| 152 | + .map(t => t.getName()); |
| 153 | + return names.length ? names : ['']; |
| 154 | + } |
| 155 | + |
| 156 | + |
| 157 | + getTagOf(target) { |
| 158 | + if (!target) return TAG_NONE; |
| 159 | + return target.rpgCollisionTag || TAG_NONE; |
| 160 | + } |
| 161 | + |
| 162 | + setTag(args, util) { |
| 163 | + const tag = String(args.TAG); |
| 164 | + util.target.rpgCollisionTag = (tag === TAG_WALL || tag === TAG_PLAYER) ? tag : TAG_NONE; |
| 165 | + } |
| 166 | + |
| 167 | + getTag(args, util) { |
| 168 | + return this.getTagOf(util.target); |
| 169 | + } |
| 170 | + |
| 171 | + isSpriteTagged(args) { |
| 172 | + const spriteName = String(args.SPRITE); |
| 173 | + const target = runtime.getSpriteTargetByName(spriteName); |
| 174 | + return this.getTagOf(target) === String(args.TAG); |
| 175 | + } |
| 176 | +. |
| 177 | + |
| 178 | + isEnabledTarget(target) { |
| 179 | + if (!target) return false; |
| 180 | + return target.rpgCollisionEnabled !== false; |
| 181 | + } |
| 182 | + |
| 183 | + enableCollisions(args, util) { |
| 184 | + util.target.rpgCollisionEnabled = true; |
| 185 | + } |
| 186 | + |
| 187 | + disableCollisions(args, util) { |
| 188 | + util.target.rpgCollisionEnabled = false; |
| 189 | + } |
| 190 | + |
| 191 | + collisionsEnabled(args, util) { |
| 192 | + return this.isEnabledTarget(util.target); |
| 193 | + } |
| 194 | + |
| 195 | + |
| 196 | + |
| 197 | + getWallDrawableIDs(excludeTarget) { |
| 198 | + const ids = []; |
| 199 | + for (const t of runtime.targets) { |
| 200 | + if (t === excludeTarget) continue; |
| 201 | + if (t.isStage) continue; |
| 202 | + if (this.getTagOf(t) === TAG_WALL && this.isEnabledTarget(t)) { |
| 203 | + ids.push(t.drawableID); |
| 204 | + } |
| 205 | + } |
| 206 | + return ids; |
| 207 | + } |
| 208 | + |
| 209 | + isTouchingWalls(target) { |
| 210 | + if (!runtime.renderer) return false; |
| 211 | + if (!this.isEnabledTarget(target)) return false; |
| 212 | + const wallIDs = this.getWallDrawableIDs(target); |
| 213 | + if (wallIDs.length === 0) return false; |
| 214 | + return runtime.renderer.isTouchingDrawables(target.drawableID, wallIDs); |
| 215 | + } |
| 216 | + |
| 217 | + |
| 218 | + slideMove(target, dxUnit, dyUnit, distance) { |
| 219 | + const step = this.MICRO_STEP; |
| 220 | + let moved = 0; |
| 221 | + const total = Math.abs(distance); |
| 222 | + const sign = distance < 0 ? -1 : 1; |
| 223 | + |
| 224 | + while (moved < total) { |
| 225 | + const thisStep = Math.min(step, total - moved); |
| 226 | + const oldX = target.x; |
| 227 | + const oldY = target.y; |
| 228 | + const nx = oldX + dxUnit * thisStep * sign; |
| 229 | + const ny = oldY + dyUnit * thisStep * sign; |
| 230 | + target.setXY(nx, ny); |
| 231 | + if (this.isTouchingWalls(target)) { |
| 232 | + target.setXY(oldX, oldY); |
| 233 | + break; |
| 234 | + } |
| 235 | + moved += thisStep; |
| 236 | + } |
| 237 | + return moved; |
| 238 | + } |
| 239 | + |
| 240 | + |
| 241 | + |
| 242 | + moveAvoidingWalls(args, util) { |
| 243 | + const target = util.target; |
| 244 | + const steps = Number(args.STEPS) || 0; |
| 245 | + const radians = (90 - target.direction) * Math.PI / 180; |
| 246 | + const dx = Math.cos(radians); |
| 247 | + const dy = Math.sin(radians); |
| 248 | + this.slideMove(target, dx, dy, steps); |
| 249 | + } |
| 250 | + |
| 251 | + stepInDirection(args, util) { |
| 252 | + const target = util.target; |
| 253 | + const steps = Number(args.STEPS) || 0; |
| 254 | + const dir = Number(args.DIR) || 0; |
| 255 | + const radians = (90 - dir) * Math.PI / 180; |
| 256 | + const dx = Math.cos(radians); |
| 257 | + const dy = Math.sin(radians); |
| 258 | + this.slideMove(target, dx, dy, steps); |
| 259 | + } |
| 260 | + |
| 261 | + moveTowardsAvoidingWalls(args, util) { |
| 262 | + const target = util.target; |
| 263 | + const targetX = Number(args.X) || 0; |
| 264 | + const targetY = Number(args.Y) || 0; |
| 265 | + const diffX = targetX - target.x; |
| 266 | + const diffY = targetY - target.y; |
| 267 | + const distToPoint = Math.sqrt(diffX * diffX + diffY * diffY); |
| 268 | + if (distToPoint < 0.001) return; |
| 269 | + |
| 270 | + const dxUnit = diffX / distToPoint; |
| 271 | + const dyUnit = diffY / distToPoint; |
| 272 | + const steps = Math.min(Number(args.STEPS) || 0, distToPoint); |
| 273 | + this.slideMove(target, dxUnit, dyUnit, steps); |
| 274 | + } |
| 275 | + |
| 276 | + pushOutOfWalls(args, util) { |
| 277 | + const target = util.target; |
| 278 | + if (!this.isTouchingWalls(target)) return; |
| 279 | + |
| 280 | + . |
| 281 | + const maxRadius = 64; |
| 282 | + for (let radius = this.MICRO_STEP; radius <= maxRadius; radius += this.MICRO_STEP) { |
| 283 | + for (let angle = 0; angle < 360; angle += 15) { |
| 284 | + const radians = angle * Math.PI / 180; |
| 285 | + const nx = target.x + Math.cos(radians) * radius; |
| 286 | + const ny = target.y + Math.sin(radians) * radius; |
| 287 | + const oldX = target.x; |
| 288 | + const oldY = target.y; |
| 289 | + target.setXY(nx, ny); |
| 290 | + if (!this.isTouchingWalls(target)) return; |
| 291 | + target.setXY(oldX, oldY); |
| 292 | + } |
| 293 | + } |
| 294 | + } |
| 295 | + |
| 296 | + |
| 297 | + |
| 298 | + touchingWall(args, util) { |
| 299 | + return this.isTouchingWalls(util.target); |
| 300 | + } |
| 301 | + |
| 302 | + wouldTouchWallAt(args, util) { |
| 303 | + const target = util.target; |
| 304 | + const oldX = target.x; |
| 305 | + const oldY = target.y; |
| 306 | + target.setXY(Number(args.X) || 0, Number(args.Y) || 0); |
| 307 | + const result = this.isTouchingWalls(target); |
| 308 | + target.setXY(oldX, oldY); |
| 309 | + return result; |
| 310 | + } |
| 311 | + |
| 312 | + whenTouchingWall(args, util) { |
| 313 | + return this.isTouchingWalls(util.target); |
| 314 | + } |
| 315 | + } |
| 316 | + |
| 317 | + Scratch.extensions.register(new RPGCollisions()); |
| 318 | +})(Scratch); |
0 commit comments