Skip to content

Commit 5d9e288

Browse files
Add StageBrah (#483)
* Create firealarm.js * Create alarm-thumb.svg * Create alarm-thumbnail.svg * Update alarm-thumb.svg * Update alarm-thumbnail.svg * Update alarm-thumb.svg * Create finalthumb.svg * Update and rename finalthumb.svg to thing * Add files via upload * Delete static/images/electricfuzzball_pm/thing * Delete static/images/electricfuzzball_pm/alarm-thumb.svg * Delete static/images/electricfuzzball_pm/alarm-thumbnail.svg * Update firealarm.js * Update extensions.js * Update extensions.js * Update extensions.js * add comma here * Add files via upload * Add files via upload * Update extensions.js --------- Co-authored-by: JeremyGamer13 <69337718+JeremyGamer13@users.noreply.github.com>
1 parent 884df01 commit 5d9e288

3 files changed

Lines changed: 247 additions & 0 deletions

File tree

src/lib/extensions.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -595,4 +595,12 @@ export default [
595595
creator: "ElectricFuzzball_PM" //hey look thats me!!
596596
//Pluey has been implemented.
597597
},
598+
{
599+
name: "Stage Brah",
600+
description: "Spawn Brah cats in your codespace!",
601+
code: "electricfuzzball_pm/StageBrah.js",
602+
banner: "electricfuzzball_pm/StageBrah.svg",
603+
creator: "ElectricFuzzball_PM"
604+
//:brah:
605+
},
598606
];
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
class StageCompanion {
2+
constructor() {
3+
this.canvas = null;
4+
this.ctx = null;
5+
this.img = null;
6+
this.bumpAudio = "https://raw.githubusercontent.com/FloppyDisk-OSC/extensions-assets/main/smb_bump.wav";
7+
this.kickAudio = "https://raw.githubusercontent.com/FloppyDisk-OSC/extensions-assets/main/smb_kick.wav";
8+
this.oneUpAudio = "https://raw.githubusercontent.com/FloppyDisk-OSC/extensions-assets/main/smb_1-up.wav";
9+
this.balls = [];
10+
this.running = false;
11+
this.mouseX = -999;
12+
this.mouseY = -999;
13+
this.mouseRadius = 20;
14+
this.mouseHandler = null;
15+
this.audioCtx = new (window.AudioContext || window.webkitAudioContext)();
16+
}
17+
18+
getInfo() {
19+
return {
20+
id: "stagecompanion",
21+
name: "Stage Brah",
22+
blocks: [
23+
{
24+
opcode: "spawnBall",
25+
blockType: Scratch.BlockType.COMMAND,
26+
text: "spawn brah with speed [SPEED] px/s and size [SIZE]",
27+
arguments: {
28+
SPEED: { type: Scratch.ArgumentType.NUMBER, defaultValue: 2 },
29+
SIZE: { type: Scratch.ArgumentType.NUMBER, defaultValue: 32 }
30+
}
31+
},
32+
{
33+
opcode: "remove",
34+
blockType: Scratch.BlockType.COMMAND,
35+
text: "remove all brahs"
36+
}
37+
]
38+
};
39+
}
40+
41+
spawnBall(args) {
42+
const speed = Number(args.SPEED) || 2;
43+
const size = Number(args.SIZE) || 32;
44+
if (!this.running) {
45+
this.running = true;
46+
this.initCanvas();
47+
this.initImage();
48+
this.initMouseTracking();
49+
this.loop();
50+
}
51+
const angle = Math.random() * Math.PI * 2;
52+
const ball = {
53+
x: 240 + (Math.random() - 0.5) * 50,
54+
y: 180 + (Math.random() - 0.5) * 50,
55+
vx: Math.cos(angle) * speed,
56+
vy: Math.sin(angle) * speed,
57+
size: size,
58+
squash: 1,
59+
squashDir: 0,
60+
consecutiveMouseHits: 0,
61+
comboDisplay: 0,
62+
comboAlpha: 0
63+
};
64+
this.balls.push(ball);
65+
}
66+
67+
initCanvas() {
68+
const stage = document.querySelector("canvas").parentElement;
69+
this.canvas = document.createElement("canvas");
70+
this.canvas.width = 480;
71+
this.canvas.height = 360;
72+
this.canvas.style.position = "absolute";
73+
this.canvas.style.left = "0";
74+
this.canvas.style.top = "0";
75+
this.canvas.style.pointerEvents = "none";
76+
this.canvas.style.zIndex = "10";
77+
stage.appendChild(this.canvas);
78+
this.ctx = this.canvas.getContext("2d");
79+
}
80+
81+
initImage() {
82+
this.img = new Image();
83+
this.img.src = "https://raw.githubusercontent.com/FloppyDisk-OSC/extensions-assets/main/brah.webp";
84+
}
85+
86+
initMouseTracking() {
87+
if (this.mouseHandler) window.removeEventListener("mousemove", this.mouseHandler);
88+
this.mouseHandler = (e) => {
89+
const r = this.canvas.getBoundingClientRect();
90+
this.mouseX = e.clientX - r.left;
91+
this.mouseY = e.clientY - r.top;
92+
};
93+
window.addEventListener("mousemove", this.mouseHandler);
94+
}
95+
96+
remove() {
97+
this.running = false;
98+
if (this.canvas) this.canvas.remove();
99+
this.canvas = null;
100+
this.balls = [];
101+
if (this.mouseHandler) {
102+
window.removeEventListener("mousemove", this.mouseHandler);
103+
this.mouseHandler = null;
104+
}
105+
}
106+
107+
playSound(url, pitch = 1) {
108+
const ctx = this.audioCtx;
109+
fetch(url)
110+
.then(r => r.arrayBuffer())
111+
.then(buf => ctx.decodeAudioData(buf))
112+
.then(decoded => {
113+
const source = ctx.createBufferSource();
114+
source.buffer = decoded;
115+
source.playbackRate.value = pitch;
116+
source.connect(ctx.destination);
117+
source.start();
118+
})
119+
.catch(() => {});
120+
}
121+
122+
loop() {
123+
if (!this.running) return;
124+
this.update();
125+
requestAnimationFrame(() => this.loop());
126+
}
127+
128+
update() {
129+
if (!this.ctx) return;
130+
this.ctx.clearRect(0, 0, 480, 360);
131+
132+
for (let ball of this.balls) {
133+
const half = ball.size / 2;
134+
ball.x += ball.vx;
135+
ball.y += ball.vy;
136+
137+
let wallHit = false;
138+
if (ball.x < half) { ball.x = half; ball.vx *= -1; wallHit = true; }
139+
if (ball.x > 480 - half) { ball.x = 480 - half; ball.vx *= -1; wallHit = true; }
140+
if (ball.y < half) { ball.y = half; ball.vy *= -1; wallHit = true; }
141+
if (ball.y > 360 - half) { ball.y = 360 - half; ball.vy *= -1; wallHit = true; }
142+
if (wallHit) {
143+
this.playSound(this.bumpAudio);
144+
ball.squashDir = 1;
145+
ball.consecutiveMouseHits = 0;
146+
ball.comboDisplay = 0;
147+
ball.comboAlpha = 0;
148+
}
149+
150+
const dx = ball.x - this.mouseX;
151+
const dy = ball.y - this.mouseY;
152+
const dist = Math.hypot(dx, dy);
153+
if (dist < this.mouseRadius + half) {
154+
const nx = dx / dist;
155+
const ny = dy / dist;
156+
const speed = Math.hypot(ball.vx, ball.vy);
157+
ball.vx = nx * speed;
158+
ball.vy = ny * speed;
159+
ball.x = Math.min(Math.max(ball.x + nx * 5, half), 480 - half);
160+
ball.y = Math.min(Math.max(ball.y + ny * 5, half), 360 - half);
161+
162+
ball.consecutiveMouseHits++;
163+
ball.comboDisplay = ball.consecutiveMouseHits;
164+
ball.comboAlpha = 1;
165+
166+
if (ball.consecutiveMouseHits <= 7) {
167+
const pitch = 1 + 0.1 * (ball.consecutiveMouseHits - 1);
168+
this.playSound(this.kickAudio, pitch);
169+
} else {
170+
this.playSound(this.oneUpAudio, 1);
171+
}
172+
173+
ball.squashDir = 1;
174+
}
175+
}
176+
177+
for (let i = 0; i < this.balls.length; i++) {
178+
for (let j = i + 1; j < this.balls.length; j++) {
179+
const a = this.balls[i];
180+
const b = this.balls[j];
181+
const halfA = a.size / 2;
182+
const halfB = b.size / 2;
183+
const dx = b.x - a.x;
184+
const dy = b.y - a.y;
185+
const dist = Math.hypot(dx, dy);
186+
if (dist < (halfA + halfB)) {
187+
const nx = dx / dist;
188+
const ny = dy / dist;
189+
const p = 2 * ((a.vx * nx + a.vy * ny) - (b.vx * nx + b.vy * ny)) / 2;
190+
a.vx -= p * nx;
191+
a.vy -= p * ny;
192+
b.vx += p * nx;
193+
b.vy += p * ny;
194+
195+
const overlap = (halfA + halfB) - dist;
196+
a.x -= nx * overlap / 2;
197+
a.y -= ny * overlap / 2;
198+
b.x += nx * overlap / 2;
199+
b.y += ny * overlap / 2;
200+
201+
this.playSound(this.bumpAudio);
202+
a.squashDir = 1;
203+
b.squashDir = 1;
204+
}
205+
}
206+
}
207+
208+
for (let ball of this.balls) {
209+
const half = ball.size / 2;
210+
if (ball.squashDir > 0) {
211+
ball.squash = 0.7 + Math.random() * 0.6;
212+
ball.squashDir = 0;
213+
} else {
214+
ball.squash += (1 - ball.squash) * 0.2;
215+
}
216+
217+
this.ctx.save();
218+
this.ctx.translate(ball.x, ball.y);
219+
this.ctx.scale(1, ball.squash);
220+
this.ctx.drawImage(this.img, -half, -half, ball.size, ball.size);
221+
this.ctx.restore();
222+
223+
if (ball.comboDisplay > 0 && ball.comboAlpha > 0) {
224+
this.ctx.font = `${Math.floor(ball.size / 2.5)}px Arial`;
225+
this.ctx.fillStyle = `rgba(255,255,255,${ball.comboAlpha})`;
226+
this.ctx.strokeStyle = `rgba(0,0,0,${ball.comboAlpha})`;
227+
this.ctx.lineWidth = 2;
228+
this.ctx.textAlign = "center";
229+
this.ctx.fillText(ball.comboDisplay, ball.x, ball.y - half - 10);
230+
this.ctx.strokeText(ball.comboDisplay, ball.x, ball.y - half - 10);
231+
ball.comboAlpha -= 0.02;
232+
if (ball.comboAlpha < 0) ball.comboAlpha = 0;
233+
}
234+
}
235+
}
236+
}
237+
238+
Scratch.extensions.register(new StageCompanion());

static/images/electricfuzzball_pm/StageBrah.svg

Lines changed: 1 addition & 0 deletions
Loading

0 commit comments

Comments
 (0)