diff --git a/extensions/SharkPool/Looks-Expanded.js b/extensions/SharkPool/Looks-Expanded.js new file mode 100644 index 0000000000..7bdf6e3e8b --- /dev/null +++ b/extensions/SharkPool/Looks-Expanded.js @@ -0,0 +1,1076 @@ +// Name: Looks Expanded +// ID: SPlooksExpanded +// Description: Expansion of the Looks Category +// By: SharkPool +// By: CST1229 +// Licence: MIT + +// Version V.1.0.3 + +(function (Scratch) { + "use strict"; + if (!Scratch.extensions.unsandboxed) + throw new Error("Looks Expanded must run unsandboxed!"); + + const menuIconURI = + "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI5Ny43MzkiIGhlaWdodD0iOTcuNzM5IiB2aWV3Qm94PSIwIDAgOTcuNzM5IDk3LjczOSI+PGcgc3Ryb2tlLW1pdGVybGltaXQ9IjEwIj48cGF0aCBkPSJNMCA0OC44N0MwIDIxLjg4IDIxLjg4IDAgNDguODcgMHM0OC44NyAyMS44OCA0OC44NyA0OC44Ny0yMS44OCA0OC44Ny00OC44NyA0OC44N1MwIDc1Ljg2IDAgNDguODciIGZpbGw9IiM2MzQyYTYiLz48cGF0aCBkPSJNNS43ODIgNDguODdjMC0yMy43OTcgMTkuMjkxLTQzLjA4OCA0My4wODgtNDMuMDg4UzkxLjk1OCAyNS4wNzMgOTEuOTU4IDQ4Ljg3IDcyLjY2NyA5MS45NTggNDguODcgOTEuOTU4IDUuNzgyIDcyLjY2NyA1Ljc4MiA0OC44NyIgZmlsbD0iIzk2ZiIvPjxwYXRoIGQ9Ik0xNi4xODYgNDQuOTk2YzQuNTMyLTUuMzEgMTYuMjE4LTE2Ljg3NCAzMi4xNzYtMTcuMDM0IDE3LjExNy0uMTcyIDI5LjMzNCAxMi41MzkgMzMuNTIgMTcuNjA0IDEuMDM5IDEuMjU4IDEuMSAyLjc2NC4xNjcgMy45MjctMy45MzUgNC45MDEtMTUuODk4IDE3LjY4Mi0zMy42ODcgMTcuNzY3LTE2Ljk1Ni4wOC0yOC43My0xMi41OS0zMi43MzYtMTcuNjI0LS45OTMtMS4yNDctLjc5My0zLjA1NC41Ni00LjY0IiBmaWxsPSIjZmZmIi8+PHBhdGggZD0iTTM1LjE0NiA0Ny42MWMwLTcuNTM2IDYuMTEtMTMuNjQ1IDEzLjY0NS0xMy42NDUgNy41MzYgMCAxMy42NDUgNi4xMSAxMy42NDUgMTMuNjQ1IDAgNy41MzYtNi4xMSAxMy42NDUtMTMuNjQ1IDEzLjY0NS03LjUzNiAwLTEzLjY0NS02LjExLTEzLjY0NS0xMy42NDUiIGZpbGw9IiM5NmYiLz48cGF0aCBkPSJNNDEuMzQyIDQ3LjYxYTcuNDQ5IDcuNDQ5IDAgMSAxIDE0Ljg5OCAwIDcuNDQ5IDcuNDQ5IDAgMCAxLTE0Ljg5OCAwIiBmaWxsPSIjZmZmIi8+PHBhdGggZD0iTTY1LjY1IDc4Ljc5YTIuOTIgMi45MiAwIDAgMS0yLjkxOC0yLjkydi02LjcxMmgtNi43MTNhMi45MiAyLjkyIDAgMCAxLTIuOTE5LTIuOTE5VjY0LjA1YTIuOTIgMi45MiAwIDAgMSAyLjkxOS0yLjkxOGg2LjcxM3YtNi43MTNBMi45MiAyLjkyIDAgMCAxIDY1LjY1IDUxLjVoMi4xOWEyLjkyIDIuOTIgMCAwIDEgMi45MTggMi45MTl2Ni43MTNoNi43MTNhMi45MiAyLjkyIDAgMCAxIDIuOTE5IDIuOTE4djIuMTlhMi45MiAyLjkyIDAgMCAxLTIuOTIgMi45MThoLTYuNzEydjYuNzEzYTIuOTIgMi45MiAwIDAgMS0yLjkxOSAyLjkxOXoiIGZpbGw9IiNmZmYiIHN0cm9rZT0iIzk2ZiIgc3Ryb2tlLXdpZHRoPSI4Ii8+PHBhdGggZD0iTTY1LjY1IDc4Ljc5YTIuOTIgMi45MiAwIDAgMS0yLjkxOC0yLjkydi02LjcxMmgtNi43MTNhMi45MiAyLjkyIDAgMCAxLTIuOTE5LTIuOTE5VjY0LjA1YTIuOTIgMi45MiAwIDAgMSAyLjkxOS0yLjkxOGg2LjcxM3YtNi43MTNBMi45MiAyLjkyIDAgMCAxIDY1LjY1IDUxLjVoMi4xOWEyLjkyIDIuOTIgMCAwIDEgMi45MTggMi45MTl2Ni43MTNoNi43MTNhMi45MiAyLjkyIDAgMCAxIDIuOTE5IDIuOTE4djIuMTlhMi45MiAyLjkyIDAgMCAxLTIuOTIgMi45MThoLTYuNzEydjYuNzEzYTIuOTIgMi45MiAwIDAgMS0yLjkxOSAyLjkxOXoiIGZpbGw9IiNmZmYiLz48L2c+PC9zdmc+"; + + const vm = Scratch.vm; + const Cast = Scratch.Cast; + const runtime = vm.runtime; + const looksCore = runtime.ext_scratch3_looks; + const isPM = Scratch.extensions.isPenguinMod; + + const render = vm.renderer; + const twgl = render.exports.twgl; + + const drawableKey = Symbol("SPlooksKey"); + const MAX_REPLACERS = 15; + const newUniforms = [ + "u_replaceColorFromSP", + "u_replaceColorToSP", + "u_replaceThresholdSP", + "u_numReplacersSP", + "u_warpSP", + "u_maskTextureSP", + "u_shouldMaskSP", + "u_tintColorSP", + "u_saturateSP", + "u_opaqueSP", + "u_contrastSP", + "u_posterizeSP", + "u_sepiaSP", + "u_bloomSP", + ]; + const defaultNewEffects = { + warp: [0.5, -0.5, -0.5, -0.5, -0.5, 0.5, 0.5, 0.5], + tint: [1, 1, 1, 1], + replacers: [], + maskTexture: "", + oldMask: "", + shouldMask: 0, + newEffects: { + saturation: 1, + opaque: 0, + contrast: 1, + posterize: 0, + sepia: 0, + bloom: 0, + }, + }; + + let currentShader; + + /* patch for new effects */ + function initDrawable(drawable) { + if (!drawable[drawableKey]) + drawable[drawableKey] = structuredClone(defaultNewEffects); + } + + const ogGetShader = render._shaderManager.getShader; + render._shaderManager.getShader = function (drawMode, effectBits) { + const shader = ogGetShader.call(this, drawMode, effectBits); + const gl = render._gl; + + // add uniforms to the existing shader + newUniforms.forEach((name) => { + shader.uniformSetters[name] = gl.getUniformLocation(shader.program, name); + }); + currentShader = shader; + return shader; + }; + + // Clear the renderer"s shader cache since we"re patching shaders + for (const cache of Object.values(render._shaderManager._shaderCache)) { + for (const programInfo of cache) { + if (programInfo) render.gl.deleteProgram(programInfo.program); + } + cache.length = 0; + } + + let patchShaders = false; + const ogCreateProgramInfo = twgl.createProgramInfo; + twgl.createProgramInfo = function (...args) { + // perform a string find-and-replace on the shader text + if (patchShaders && args[1] && args[1][0] && args[1][1]) { + args[1][0] = args[1][0] + // replace attribute properties with variables we can modify + .replaceAll("vec4(a_position", "vec4(positionSP") + .replace("v_texCoord = a_texCoord;", "") + .replace( + "#if !(defined(DRAW_MODE_line) || defined(DRAW_MODE_background))", + "#if 1" + ) + .replace( + `void main() {`, + `uniform vec2 u_warpSP[4]; + +void main() { + vec2 positionSP = a_position; + #ifndef DRAW_MODE_background + v_texCoord = a_texCoord; + #endif + + float u = v_texCoord.x; + float v = v_texCoord.y; + + // apply position warp (bilinear) + vec2 warpedPos = + (1.0 - u) * (1.0 - v) * u_warpSP[0] + u * (1.0 - v) * u_warpSP[1] + + u * v * u_warpSP[2] + (1.0 - u) * v * u_warpSP[3]; + + // compute w for perspective correction + float w = (1.0 - u) * (1.0 - v) + u * (1.0 - v) + u * v + (1.0 - u) * v; + + positionSP = warpedPos / max(w, 1e-5); + + #ifdef DRAW_MODE_background + gl_Position = vec4(positionSP * 2.0, 0, 1); + #else + gl_Position = u_projectionMatrix * u_modelMatrix * vec4(positionSP, 0, 1); + #endif` + ); + + args[1][1] = args[1][1] + .replace( + `uniform sampler2D u_skin;`, + `uniform sampler2D u_skin; +uniform sampler2D u_maskTextureSP; +uniform float u_shouldMaskSP; + +#define MAX_REPLACERS 15 +uniform vec3 u_replaceColorFromSP[MAX_REPLACERS]; +uniform vec4 u_replaceColorToSP[MAX_REPLACERS]; +uniform float u_replaceThresholdSP[MAX_REPLACERS]; +uniform int u_numReplacersSP; + +uniform vec4 u_tintColorSP; +uniform float u_saturateSP; +uniform float u_opaqueSP; +uniform float u_contrastSP; +uniform float u_posterizeSP; +uniform float u_sepiaSP; +uniform float u_bloomSP; + +vec3 spRGB2HSV(vec3 c) { + vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0); + vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g)); + vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r)); + + float d = q.x - min(q.w, q.y); + float e = 1.0e-10; + return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x); +} +vec3 spHSV2RGB(vec3 c) { + vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); + vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); + return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y); +}` + ) + .replace( + `gl_FragColor.rgb = clamp(gl_FragColor.rgb / (gl_FragColor.a + epsilon), 0.0, 1.0);`, + `gl_FragColor.rgb = clamp(gl_FragColor.rgb / (gl_FragColor.a + epsilon), 0.0, 1.0); +vec3 finalColor = gl_FragColor.rgb; +float finalAlpha = gl_FragColor.a; + +if (u_shouldMaskSP > 0.5 && finalAlpha > 0.0) { + vec4 maskColor = texture2D(u_maskTextureSP, texcoord0); + maskColor.rgb = clamp(maskColor.rgb / (maskColor.a + epsilon), 0.0, 1.0); + finalAlpha *= maskColor.a; +} + +for (int i = 0; i < MAX_REPLACERS; i++) { + if (i >= u_numReplacersSP) break; + + float dist = distance(finalColor, u_replaceColorFromSP[i]); + if (dist <= u_replaceThresholdSP[i]) { + float strength = 1.0 - (dist / (u_replaceThresholdSP[i] + 1.0)); + finalColor = mix(finalColor, u_replaceColorToSP[i].rgb, strength); + if (u_replaceColorToSP[i].a < 1.0 && strength > 0.01) { + finalAlpha = clamp(mix(finalAlpha, u_replaceColorToSP[i].a, strength), 0.0, 1.0); + } + } +} + +vec3 hsv = spRGB2HSV(finalColor); +if (u_saturateSP < 0.0) { + hsv.x = mod(hsv.x + 0.5, 1.0); + hsv.y *= -u_saturateSP; +} else { + hsv.y *= u_saturateSP; +} +finalColor = spHSV2RGB(hsv); +finalColor = (finalColor - 0.5) * u_contrastSP + 0.5; +if (u_posterizeSP > 0.0) finalColor = floor(finalColor * u_posterizeSP) / u_posterizeSP; + +if (u_sepiaSP > 0.0) { + vec3 sepiaColor = vec3( + dot(finalColor, vec3(0.393, 0.769, 0.189)), + dot(finalColor, vec3(0.349, 0.686, 0.168)), + dot(finalColor, vec3(0.272, 0.534, 0.131)) + ); + finalColor = mix(finalColor, sepiaColor, u_sepiaSP); +} +if (u_bloomSP > 0.0) { + vec3 bloom = max(finalColor - 0.4, 0.0); + + bloom += texture2D(u_skin, v_texCoord + vec2( 0.001, 0.001)).rgb; + bloom += texture2D(u_skin, v_texCoord + vec2(-0.001, 0.001)).rgb; + bloom += texture2D(u_skin, v_texCoord + vec2( 0.001, -0.001)).rgb; + bloom += texture2D(u_skin, v_texCoord + vec2(-0.001, -0.001)).rgb; + bloom *= 0.25; + + finalColor += bloom * u_bloomSP; + finalColor = clamp(finalColor, 0.0, 1.0); +} + +gl_FragColor.rgb = finalColor * u_tintColorSP.rgb; +float baseAlpha = finalAlpha; +if (baseAlpha > 0.0 && baseAlpha < 1.0) baseAlpha = mix(baseAlpha, 1.0, u_opaqueSP); +gl_FragColor.a = baseAlpha;` + ) + .replaceAll( + // The unpremultiply code will now always run due to palette replacement stuff. + // This is a bit more inefficient, but whatever. + "#if defined(ENABLE_color) || defined(ENABLE_brightness)", + // i have no idea how webgl works, and i don"t want to have to remove the #endif somehow + // just do something that will always be true -CST + "#if defined(MAX_REPLACERS)" + ); + } + return ogCreateProgramInfo.apply(this, args); + }; + const ogBuildShader = render._shaderManager._buildShader; + render._shaderManager._buildShader = function (...args) { + try { + patchShaders = true; + return ogBuildShader.apply(this, args); + } finally { + patchShaders = false; + } + }; + + const ogGetUniforms = render.exports.Drawable.prototype.getUniforms; + render.exports.Drawable.prototype.getUniforms = function () { + const gl = render.gl; + const uniforms = ogGetUniforms.call(this); + if (!currentShader) return uniforms; + + initDrawable(this); + const effectData = this[drawableKey]; + const replacers = effectData.replacers; + + const replaceFrom = new Float32Array(MAX_REPLACERS * 3).fill(0); + const replaceTo = new Float32Array(MAX_REPLACERS * 4).fill(0); + const replaceThresh = new Float32Array(MAX_REPLACERS).fill(1); + if (replacers.length > 0) { + for (let i = 0; i < Math.min(replacers.length, MAX_REPLACERS); i++) { + replaceFrom.set(replacers[i].targetVert, i * 3); + replaceTo.set(replacers[i].replaceVert, i * 4); + replaceThresh[i] = replacers[i].soft; + } + } + + if (effectData.shouldMask) { + gl.activeTexture(gl.TEXTURE30); + gl.bindTexture(gl.TEXTURE_2D, effectData._maskTexture); + gl.uniform1i(currentShader.uniformSetters["u_maskTextureSP"], 30); + gl.activeTexture(gl.TEXTURE0); + } + + const newUniformSetters = { + u_replaceColorFromSP: { method: "uniform3fv", value: replaceFrom }, + u_replaceColorToSP: { method: "uniform4fv", value: replaceTo }, + u_replaceThresholdSP: { method: "uniform1fv", value: replaceThresh }, + u_numReplacersSP: { + method: "uniform1i", + value: replacers ? Math.min(replacers.length, MAX_REPLACERS) : 0, + }, + u_tintColorSP: { method: "uniform4fv", value: effectData.tint }, + u_warpSP: { method: "uniform2fv", value: effectData.warp }, + u_shouldMaskSP: { method: "uniform1f", value: effectData.shouldMask }, + u_saturateSP: { + method: "uniform1f", + value: effectData.newEffects.saturation, + }, + u_opaqueSP: { method: "uniform1f", value: effectData.newEffects.opaque }, + u_contrastSP: { + method: "uniform1f", + value: effectData.newEffects.contrast, + }, + u_posterizeSP: { + method: "uniform1f", + value: effectData.newEffects.posterize, + }, + u_sepiaSP: { method: "uniform1f", value: effectData.newEffects.sepia }, + u_bloomSP: { method: "uniform1f", value: effectData.newEffects.bloom }, + }; + + Object.entries(newUniformSetters).forEach(([key, { method, value }]) => { + if (currentShader.uniformSetters[key]) + gl[method](currentShader.uniformSetters[key], value); + }); + return uniforms; + }; + + // reset on stop/start/clear + const ogClearEffects = vm.exports.RenderedTarget.prototype.clearEffects; + vm.exports.RenderedTarget.prototype.clearEffects = function () { + const drawable = render._allDrawables[this.drawableID]; + drawable[drawableKey] = structuredClone(defaultNewEffects); + ogClearEffects.call(this); + }; + + // manipulate bounds for warping + const radianConverter = Math.PI / 180; + function rotatePoint(x, y, cx, cy, rads) { + const cos = Math.cos(rads), + sin = Math.sin(rads); + const dx = x - cx, + dy = y - cy; + return { + x: cx + dx * cos - dy * sin, + y: cy + dx * sin + dy * cos, + }; + } + function warpBounds(drawable, bounds) { + if (!drawable[drawableKey]) return bounds; + + let warpVals = drawable[drawableKey].warp; + if (warpVals.join(",") === defaultNewEffects.warp.join(",")) return bounds; + + // original getBounds already accounts for rotation, so we have to make our own system + // for getting the non-rotated scale and position + warpVals = warpVals.map((v, i) => (i > 0 && i < 5 ? v * -1 : v)); + const angle = (drawable._direction - 90) * radianConverter; + const [x, y] = drawable._position; + const width = drawable.skin.size[0] * (drawable.scale[0] / 200); + const height = drawable.skin.size[1] * (drawable.scale[1] / 200); + + const points = [ + { x: warpVals[0] * 2 * -width + x, y: warpVals[1] * -2 * height - y }, + { x: warpVals[2] * 2 * width + x, y: warpVals[3] * -2 * height - y }, + { x: warpVals[4] * 2 * width + x, y: warpVals[5] * -2 * -height - y }, + { x: warpVals[6] * 2 * -width + x, y: warpVals[7] * -2 * -height - y }, + ]; + + const rotatedPoints = points.map((p) => + rotatePoint(p.x, p.y, x, -y, angle) + ); + const xs = rotatedPoints.map((p) => p.x); + const ys = rotatedPoints.map((p) => p.y); + + bounds.left = Math.min(...xs); + bounds.top = -Math.min(...ys); + bounds.right = Math.max(...xs); + bounds.bottom = -Math.max(...ys); + return bounds; + } + + const ogGetBounds = render.exports.Drawable.prototype.getBounds; + render.exports.Drawable.prototype.getBounds = function () { + return warpBounds(this, ogGetBounds.call(this)); + }; + const ogGetAABB = render.exports.Drawable.prototype.getAABB; + render.exports.Drawable.prototype.getAABB = function () { + return warpBounds(this, ogGetAABB.call(this)); + }; + + // update the pen shader + if (runtime.ext_pen && runtime.ext_pen._penSkinId > -1) { + const penSkin = render._allSkins[runtime.ext_pen._penSkinId]; + const gl = render.gl; + penSkin._lineShader = render._shaderManager.getShader("line", 0); + penSkin._drawTextureShader = render._shaderManager.getShader("default", 0); + penSkin.a_position_loc = gl.getAttribLocation( + penSkin._lineShader.program, + "a_position" + ); + penSkin.a_lineColor_loc = gl.getAttribLocation( + penSkin._lineShader.program, + "a_lineColor" + ); + penSkin.a_lineThicknessAndLength_loc = gl.getAttribLocation( + penSkin._lineShader.program, + "a_lineThicknessAndLength" + ); + penSkin.a_penPoints_loc = gl.getAttribLocation( + penSkin._lineShader.program, + "a_penPoints" + ); + } + + // this will allow clones to inherit parent effects + const ogInitDrawable = vm.exports.RenderedTarget.prototype.initDrawable; + vm.exports.RenderedTarget.prototype.initDrawable = function (layerGroup) { + ogInitDrawable.call(this, layerGroup); + if (this.isOriginal) return; + + const parentSprite = this.sprite.clones[0]; // clone[0] is always the original + const parentDrawable = render._allDrawables[parentSprite.drawableID]; + if (!parentDrawable[drawableKey]) return; + + const drawable = render._allDrawables[this.drawableID]; + drawable[drawableKey] = structuredClone(parentDrawable[drawableKey]); + }; + + /* patch for "when costume switches" event */ + const ogSetCoreCostume = looksCore.constructor.prototype._setCostume; + ogSetCoreCostume.constructor.prototype._setCostume = function ( + target, + requestedCostume, + optZeroIndex + ) { + ogSetCoreCostume.call(this, target, requestedCostume, optZeroIndex); + runtime.startHats("SPlooksExpanded_whenCostumeSwitch", { + COSTUME: target.getCurrentCostume()?.name || "", + }); + }; + const ogSetSpriteCostume = vm.exports.RenderedTarget.prototype.setCostume; + vm.exports.RenderedTarget.prototype.setCostume = function (index) { + ogSetSpriteCostume.call(this, index); + runtime.startHats("SPlooksExpanded_whenCostumeSwitch", { + COSTUME: this.getCurrentCostume()?.name || "", + }); + }; + + class SPlooksExpanded { + getInfo() { + return { + id: "SPlooksExpanded", + name: Scratch.translate("Looks Expanded"), + color1: "#9966FF", + color2: "#855CD6", + color3: "#774DCB", + menuIconURI, + blocks: [ + { + opcode: "getSpeech", + blockType: Scratch.BlockType.REPORTER, + extensions: ["colours_looks"], + text: Scratch.translate("speech from [TARGET]"), + arguments: { + TARGET: { type: Scratch.ArgumentType.STRING, menu: "TARGETS" }, + }, + }, + "---", + { + opcode: "costumeCnt", + blockType: Scratch.BlockType.REPORTER, + extensions: ["colours_looks"], + text: Scratch.translate("# of costumes in [TARGET]"), + arguments: { + TARGET: { type: Scratch.ArgumentType.STRING, menu: "TARGETS" }, + }, + }, + { + opcode: "costumeInfo", + blockType: Scratch.BlockType.REPORTER, + extensions: ["colours_looks"], + text: Scratch.translate("[INFO] of costume # [NUM] in [TARGET]"), + arguments: { + INFO: { type: Scratch.ArgumentType.STRING, menu: "COSTUME_DATA" }, + NUM: { type: Scratch.ArgumentType.NUMBER, defaultValue: 1 }, + TARGET: { type: Scratch.ArgumentType.STRING, menu: "TARGETS" }, + }, + }, + { + opcode: "setTargetCostume", + blockType: Scratch.BlockType.COMMAND, + extensions: ["colours_looks"], + text: Scratch.translate("switch costume of [TARGET] to [VALUE]"), + arguments: { + TARGET: { type: Scratch.ArgumentType.STRING, menu: "TARGETS" }, + VALUE: { type: Scratch.ArgumentType.STRING, defaultValue: "..." }, + }, + }, + { + opcode: "whenCostumeSwitch", + blockType: Scratch.BlockType.EVENT, + extensions: ["colours_event"], + isEdgeActivated: false, + text: Scratch.translate("when costume switches to [COSTUME]"), + arguments: { + COSTUME: { type: Scratch.ArgumentType.STRING, menu: "COSTUMES" }, + }, + }, + "---", + { + opcode: "setSpriteEffect", + blockType: Scratch.BlockType.COMMAND, + extensions: ["colours_looks"], + text: Scratch.translate("set [EFFECT] of [TARGET] to [VALUE]"), + arguments: { + EFFECT: { + type: Scratch.ArgumentType.STRING, + menu: "EFFECT_MENU", + }, + TARGET: { type: Scratch.ArgumentType.STRING, menu: "TARGETS" }, + VALUE: { type: Scratch.ArgumentType.NUMBER, defaultValue: 0 }, + }, + }, + { + opcode: "effectValue", + blockType: Scratch.BlockType.REPORTER, + extensions: ["colours_looks"], + text: Scratch.translate("[EFFECT] effect of [TARGET]"), + arguments: { + EFFECT: { + type: Scratch.ArgumentType.STRING, + menu: "EFFECT_MENU", + }, + TARGET: { type: Scratch.ArgumentType.STRING, menu: "TARGETS" }, + }, + }, + { + opcode: "tintSprite", + blockType: Scratch.BlockType.COMMAND, + extensions: ["colours_looks"], + text: Scratch.translate("set tint of [TARGET] to [COLOR]"), + arguments: { + TARGET: { type: Scratch.ArgumentType.STRING, menu: "TARGETS" }, + COLOR: { type: Scratch.ArgumentType.COLOR }, + }, + }, + "---", + { + opcode: "replaceColor", + blockType: Scratch.BlockType.COMMAND, + extensions: ["colours_looks"], + text: Scratch.translate( + "replace [COLOR1] with [COLOR2] in [TARGET] softness [VALUE]" + ), + arguments: { + COLOR1: { type: Scratch.ArgumentType.COLOR }, + COLOR2: { type: Scratch.ArgumentType.COLOR }, + TARGET: { type: Scratch.ArgumentType.STRING, menu: "TARGETS" }, + VALUE: { type: Scratch.ArgumentType.NUMBER, defaultValue: 10 }, + }, + }, + { + opcode: "resetColor", + blockType: Scratch.BlockType.COMMAND, + extensions: ["colours_looks"], + text: Scratch.translate("reset [COLOR1] replacer in [TARGET]"), + arguments: { + COLOR1: { type: Scratch.ArgumentType.COLOR }, + TARGET: { type: Scratch.ArgumentType.STRING, menu: "TARGETS" }, + }, + }, + { + opcode: "resetReplacers", + blockType: Scratch.BlockType.COMMAND, + extensions: ["colours_looks"], + text: Scratch.translate("reset color replacers in [TARGET]"), + arguments: { + TARGET: { type: Scratch.ArgumentType.STRING, menu: "TARGETS" }, + }, + }, + { + blockType: Scratch.BlockType.XML, + xml: `