diff --git a/src/app.html b/src/app.html
index 9129a0114..c94979023 100644
--- a/src/app.html
+++ b/src/app.html
@@ -3,7 +3,7 @@
- PenguinMod Extra Extensions
+ PenguinMod Extra Extensions
diff --git a/src/lib/extensions.js b/src/lib/extensions.js
index dadef9288..5f1d4f06a 100644
--- a/src/lib/extensions.js
+++ b/src/lib/extensions.js
@@ -443,6 +443,18 @@ export default [
creatorAlias: "gaimerI17",
note: "Extension thumbnail made by Dillon."
},
+ {
+ name: "Raycasting",
+ description: "Throw invisible rays and report information about the sprites it collided with. ",
+ code: "GatocDev/RayCasting.js",
+ banner: "GatocDev/RayCasting.png",
+ creator: "GatocDev",
+
+ creatorAlias: "Gatoc_Dev",
+ notes: "Thumbnail(s) by Steve0Greatness and NotDillon", // Optional. Allows you to note anyone else who helped you or any small info.
+ unstable: false,
+ isGitHub: false,
+},
/* these extensions are completely dead as of now
{
name: "Online Captcha",
diff --git a/static/extensions/GatocDev/RayCasting.js b/static/extensions/GatocDev/RayCasting.js
new file mode 100644
index 000000000..7d99a0b5f
--- /dev/null
+++ b/static/extensions/GatocDev/RayCasting.js
@@ -0,0 +1,466 @@
+(function(Scratch) {
+ 'use strict';
+
+ class RaycastExtension {
+ constructor() {
+ this.rayData = [];
+ this.maxRays = 360;
+ this.disabledSprites = new Set();
+ }
+
+ getInfo() {
+ return {
+ id: 'raycast',
+ name: 'Raycast',
+ color1: '#FF6B35',
+ color2: '#F7931E',
+ color3: '#FFD23F',
+ blocks: [
+ {
+ opcode: 'castRay',
+ blockType: Scratch.BlockType.REPORTER,
+ text: 'cast ray from X [X] Y [Y] direction [DIRECTION] distance [DISTANCE]',
+ arguments: {
+ X: {
+ type: Scratch.ArgumentType.NUMBER,
+ defaultValue: 0
+ },
+ Y: {
+ type: Scratch.ArgumentType.NUMBER,
+ defaultValue: 0
+ },
+ DIRECTION: {
+ type: Scratch.ArgumentType.ANGLE,
+ defaultValue: 90
+ },
+ DISTANCE: {
+ type: Scratch.ArgumentType.NUMBER,
+ defaultValue: 200
+ }
+ }
+ },
+
+ {
+ opcode: 'castMultipleRays',
+ blockType: Scratch.BlockType.COMMAND,
+ text: 'cast [COUNT] rays from X [X] Y [Y] spread [SPREAD] degrees distance [DISTANCE]',
+ arguments: {
+ COUNT: {
+ type: Scratch.ArgumentType.NUMBER,
+ defaultValue: 8
+ },
+ X: {
+ type: Scratch.ArgumentType.NUMBER,
+ defaultValue: 0
+ },
+ Y: {
+ type: Scratch.ArgumentType.NUMBER,
+ defaultValue: 0
+ },
+ SPREAD: {
+ type: Scratch.ArgumentType.NUMBER,
+ defaultValue: 360
+ },
+ DISTANCE: {
+ type: Scratch.ArgumentType.NUMBER,
+ defaultValue: 200
+ }
+ }
+ },
+
+ '---',
+ {
+ opcode: 'getHitSpriteX',
+ blockType: Scratch.BlockType.REPORTER,
+ text: 'hit sprite X at ray [INDEX]',
+ arguments: {
+ INDEX: {
+ type: Scratch.ArgumentType.NUMBER,
+ defaultValue: 1
+ }
+ }
+ },
+ {
+ opcode: 'getHitSpriteY',
+ blockType: Scratch.BlockType.REPORTER,
+ text: 'hit sprite Y at ray [INDEX]',
+ arguments: {
+ INDEX: {
+ type: Scratch.ArgumentType.NUMBER,
+ defaultValue: 1
+ }
+ }
+ },
+ {
+ opcode: 'getHitSpriteSize',
+ blockType: Scratch.BlockType.REPORTER,
+ text: 'hit sprite size at ray [INDEX]',
+ arguments: {
+ INDEX: {
+ type: Scratch.ArgumentType.NUMBER,
+ defaultValue: 1
+ }
+ }
+ },
+ {
+ opcode: 'getHitSpriteName',
+ blockType: Scratch.BlockType.REPORTER,
+ text: 'hit sprite name at ray [INDEX]',
+ arguments: {
+ INDEX: {
+ type: Scratch.ArgumentType.NUMBER,
+ defaultValue: 1
+ }
+ }
+ },
+ {
+ opcode: 'getAllHitSprites',
+ blockType: Scratch.BlockType.REPORTER,
+ text: 'all hit sprites at ray [INDEX]',
+ arguments: {
+ INDEX: {
+ type: Scratch.ArgumentType.NUMBER,
+ defaultValue: 1
+ }
+ }
+ },
+ {
+ opcode: 'getHitDistance',
+ blockType: Scratch.BlockType.REPORTER,
+ text: 'hit distance at ray [INDEX]',
+ arguments: {
+ INDEX: {
+ type: Scratch.ArgumentType.NUMBER,
+ defaultValue: 1
+ }
+ }
+ },
+ '---',
+ {
+ opcode: 'getRayCount',
+ blockType: Scratch.BlockType.REPORTER,
+ text: 'number of active rays'
+ },
+ {
+ opcode: 'rayHitSprite',
+ blockType: Scratch.BlockType.BOOLEAN,
+ text: 'ray [INDEX] hit a sprite?',
+ arguments: {
+ INDEX: {
+ type: Scratch.ArgumentType.NUMBER,
+ defaultValue: 1
+ }
+ }
+ },
+ {
+ opcode: 'getRayX',
+ blockType: Scratch.BlockType.REPORTER,
+ text: 'ray [INDEX] end X',
+ arguments: {
+ INDEX: {
+ type: Scratch.ArgumentType.NUMBER,
+ defaultValue: 1
+ }
+ }
+ },
+ {
+ opcode: 'getRayY',
+ blockType: Scratch.BlockType.REPORTER,
+ text: 'ray [INDEX] end Y',
+ arguments: {
+ INDEX: {
+ type: Scratch.ArgumentType.NUMBER,
+ defaultValue: 1
+ }
+ }
+ },
+ '---',
+ {
+ opcode: 'disableThisSprite',
+ blockType: Scratch.BlockType.COMMAND,
+ text: 'disable this sprite from ray detection'
+ },
+ {
+ opcode: 'enableThisSprite',
+ blockType: Scratch.BlockType.COMMAND,
+ text: 'enable this sprite for ray detection'
+ },
+ {
+ opcode: 'clearRays',
+ blockType: Scratch.BlockType.COMMAND,
+ text: 'clear all rays'
+ }
+ ]
+ };
+ }
+
+ castRay(args, util) {
+ const startX = Scratch.Cast.toNumber(args.X);
+ const startY = Scratch.Cast.toNumber(args.Y);
+ const direction = Scratch.Cast.toNumber(args.DIRECTION);
+ const maxDistance = Scratch.Cast.toNumber(args.DISTANCE);
+
+ const rayResult = this.performRaycast(startX, startY, direction, maxDistance, util);
+
+ this.rayData = [rayResult];
+
+ if (rayResult.hitSprites && rayResult.hitSprites.length > 0) {
+ return JSON.stringify(rayResult.hitSprites);
+ } else {
+ return JSON.stringify([]);
+ }
+ }
+
+ castMultipleRays(args, util) {
+ const count = Math.min(Math.max(1, Scratch.Cast.toNumber(args.COUNT)), this.maxRays);
+ const startX = Scratch.Cast.toNumber(args.X);
+ const startY = Scratch.Cast.toNumber(args.Y);
+ const spread = Scratch.Cast.toNumber(args.SPREAD);
+ const maxDistance = Scratch.Cast.toNumber(args.DISTANCE);
+
+ this.rayData = [];
+
+ const angleStep = count > 1 ? spread / (count - 1) : 0;
+ const startAngle = -spread / 2;
+
+ for (let i = 0; i < count; i++) {
+ const direction = startAngle + (angleStep * i);
+ const rayResult = this.performRaycast(startX, startY, direction, maxDistance, util);
+ this.rayData.push(rayResult);
+ }
+ }
+
+ performRaycast(startX, startY, direction, maxDistance, util) {
+ const runtime = util.runtime;
+
+ const angleRad = ((direction - 90) * Math.PI) / 180;
+ const deltaX = Math.cos(angleRad);
+ const deltaY = Math.sin(angleRad);
+
+ let hitSprites = [];
+ let closestHit = null;
+ let closestDistance = maxDistance;
+
+ const allSprites = runtime.targets.filter(target => {
+ return !target.isStage &&
+ target !== util.target &&
+ target.visible &&
+ !this.disabledSprites.has(target.getName());
+ });
+
+ // Collect all hits along the ray path
+ for (const target of allSprites) {
+ const hitResult = this.checkRayTargetIntersection(
+ startX, startY, deltaX, deltaY, maxDistance, target
+ );
+
+ if (hitResult) {
+ hitSprites.push({
+ name: target.getName(),
+ distance: hitResult.distance,
+ hitX: hitResult.hitX,
+ hitY: hitResult.hitY,
+ spriteX: target.x,
+ spriteY: target.y,
+ spriteSize: target.size
+ });
+
+ // Still track closest hit for compatibility
+ if (hitResult.distance < closestDistance) {
+ closestDistance = hitResult.distance;
+ closestHit = {
+ hit: true,
+ spriteName: target.getName(),
+ spriteX: target.x,
+ spriteY: target.y,
+ spriteSize: target.size,
+ hitX: hitResult.hitX,
+ hitY: hitResult.hitY,
+ distance: hitResult.distance
+ };
+ }
+ }
+ }
+
+ // Sort hits by distance (closest first)
+ hitSprites.sort((a, b) => a.distance - b.distance);
+
+ if (closestHit) {
+ return {
+ ...closestHit,
+ hitSprites: hitSprites.map(hit => hit.name)
+ };
+ } else {
+ return {
+ hit: false,
+ spriteName: '',
+ spriteX: 0,
+ spriteY: 0,
+ spriteSize: 0,
+ hitX: startX + deltaX * maxDistance,
+ hitY: startY + deltaY * maxDistance,
+ distance: maxDistance,
+ hitSprites: []
+ };
+ }
+ }
+
+ checkRayTargetIntersection(startX, startY, deltaX, deltaY, maxDistance, target) {
+ const spriteX = target.x;
+ const spriteY = target.y;
+ const spriteSize = target.size / 100;
+
+ // Get sprite dimensions
+ let bounds = null;
+ try {
+ if (target.getBounds) {
+ bounds = target.getBounds();
+ } else if (target.drawable && target.drawable.getBounds) {
+ bounds = target.drawable.getBounds();
+ }
+ } catch (e) {
+ // Bounds not available
+ }
+
+ let width, height;
+ if (bounds && bounds.width && bounds.height) {
+ width = bounds.width;
+ height = bounds.height;
+ } else {
+ const costume = target.getCostume();
+ if (costume && costume.size) {
+ width = costume.size[0] * spriteSize;
+ height = costume.size[1] * spriteSize;
+ } else {
+ width = 60 * spriteSize;
+ height = 60 * spriteSize;
+ }
+ }
+
+ width = Math.max(width, 1);
+ height = Math.max(height, 1);
+
+ // Calculate sprite bounds
+ const left = spriteX - (width / 2);
+ const right = spriteX + (width / 2);
+ const bottom = spriteY - (height / 2);
+ const top = spriteY + (height / 2);
+
+ // Simple step-by-step ray marching to avoid teleporting
+ const stepSize = 1; // Small step size for accuracy
+ const steps = Math.ceil(maxDistance / stepSize);
+
+ for (let i = 1; i <= steps; i++) {
+ const distance = i * stepSize;
+ if (distance > maxDistance) break;
+
+ const checkX = startX + deltaX * distance;
+ const checkY = startY + deltaY * distance;
+
+ // Check if this point is inside the sprite bounds
+ if (checkX >= left && checkX <= right &&
+ checkY >= bottom && checkY <= top) {
+ return {
+ hitX: checkX,
+ hitY: checkY,
+ distance: distance
+ };
+ }
+ }
+
+ return null;
+ }
+
+ getHitSpriteX(args) {
+ const index = Math.floor(Scratch.Cast.toNumber(args.INDEX)) - 1;
+ if (index >= 0 && index < this.rayData.length && this.rayData[index].hit) {
+ return this.rayData[index].spriteX;
+ }
+ return 0;
+ }
+
+ getHitSpriteY(args) {
+ const index = Math.floor(Scratch.Cast.toNumber(args.INDEX)) - 1;
+ if (index >= 0 && index < this.rayData.length && this.rayData[index].hit) {
+ return this.rayData[index].spriteY;
+ }
+ return 0;
+ }
+
+ getHitSpriteSize(args) {
+ const index = Math.floor(Scratch.Cast.toNumber(args.INDEX)) - 1;
+ if (index >= 0 && index < this.rayData.length && this.rayData[index].hit) {
+ return this.rayData[index].spriteSize;
+ }
+ return 0;
+ }
+
+ getHitSpriteName(args) {
+ const index = Math.floor(Scratch.Cast.toNumber(args.INDEX)) - 1;
+ if (index >= 0 && index < this.rayData.length && this.rayData[index].hit) {
+ return this.rayData[index].spriteName;
+ }
+ return '';
+ }
+
+ getAllHitSprites(args) {
+ const index = Math.floor(Scratch.Cast.toNumber(args.INDEX)) - 1;
+ if (index >= 0 && index < this.rayData.length && this.rayData[index].hitSprites) {
+ return JSON.stringify(this.rayData[index].hitSprites);
+ }
+ return JSON.stringify([]);
+ }
+
+ getHitDistance(args) {
+ const index = Math.floor(Scratch.Cast.toNumber(args.INDEX)) - 1;
+ if (index >= 0 && index < this.rayData.length) {
+ return this.rayData[index].distance;
+ }
+ return 0;
+ }
+
+ getRayCount() {
+ return this.rayData.length;
+ }
+
+ rayHitSprite(args) {
+ const index = Math.floor(Scratch.Cast.toNumber(args.INDEX)) - 1;
+ if (index >= 0 && index < this.rayData.length) {
+ return this.rayData[index].hit;
+ }
+ return false;
+ }
+
+ getRayX(args) {
+ const index = Math.floor(Scratch.Cast.toNumber(args.INDEX)) - 1;
+ if (index >= 0 && index < this.rayData.length) {
+ return this.rayData[index].hitX;
+ }
+ return 0;
+ }
+
+ getRayY(args) {
+ const index = Math.floor(Scratch.Cast.toNumber(args.INDEX)) - 1;
+ if (index >= 0 && index < this.rayData.length) {
+ return this.rayData[index].hitY;
+ }
+ return 0;
+ }
+
+ disableThisSprite(args, util) {
+ const spriteName = util.target.getName();
+ this.disabledSprites.add(spriteName);
+ }
+
+ enableThisSprite(args, util) {
+ const spriteName = util.target.getName();
+ this.disabledSprites.delete(spriteName);
+ }
+
+ clearRays() {
+ this.rayData = [];
+ }
+ }
+
+ Scratch.extensions.register(new RaycastExtension());
+})(Scratch);
diff --git a/static/images/GatocDev/RayCasting.png b/static/images/GatocDev/RayCasting.png
new file mode 100644
index 000000000..3d2fa8655
Binary files /dev/null and b/static/images/GatocDev/RayCasting.png differ