diff --git a/src/lib/extensions.js b/src/lib/extensions.js
index d1e8f0ab9..288136a19 100644
--- a/src/lib/extensions.js
+++ b/src/lib/extensions.js
@@ -669,4 +669,16 @@ export default [
tags: ["customtype","data","utility","new","large"],
creatorAlias: "AndrewGaming587"
},
+ {
+ name: "boolvariales",
+ description: "Add bool variables and additional tools.",
+ code: "utakeuchigames/boolvariable.js",
+ banner: "utakeuchigames/boolvariables.avif",
+ creator: "utakeuchigames",
+ tags: ["variables"],
+ creatorAlias: "utakeuchigames",
+ unstable: false,
+ unstableReason: "May break sometimes, Use at your own risk.",
+ isGitHub: false,
+ },
];
diff --git a/static/extensions/utakeuchigames/boolvariable.js b/static/extensions/utakeuchigames/boolvariable.js
new file mode 100644
index 000000000..ab4f5fcd0
--- /dev/null
+++ b/static/extensions/utakeuchigames/boolvariable.js
@@ -0,0 +1,552 @@
+((Scratch) => {
+ 'use strict';
+
+ const icon = 'https://utakeuchigames.github.io/boolvariable/favicon.svg';
+
+ const vm = Scratch.vm;
+
+ let deltaTime = 0;
+ let previousTime = 0;
+
+
+
+ class utakeuchigamesBoolvariable {
+ constructor() {
+ this.boolVariables = {};
+ this.boolVariablesinfo = {};
+ this.isUIOpen = false;
+ this.isDelUIOpen = false;
+ this.frameCount = 0;
+ }
+
+ // セーブデータの書き出し
+ customSave() {
+ const saveData = {
+ boolVariables: this.boolVariables,
+ boolVariablesinfo: this.boolVariablesinfo
+ };
+ return JSON.stringify(saveData);
+ }
+
+ // セーブデータの読み込み
+ customLoad(data) {
+ if (!data) return;
+ try {
+ const parsed = (typeof data === 'string') ? JSON.parse(data) : data;
+ if (parsed) {
+ this.boolVariables = parsed.boolVariables ?? {};
+ this.boolVariablesinfo = parsed.boolVariablesinfo ?? {};
+
+ // 読み込み時にもし管理情報(info)が欠落している変数があれば自動救出
+ for (const key of Object.keys(this.boolVariables)) {
+ if (!this.boolVariablesinfo[key]) {
+ this.ensureVariableExists(key);
+ }
+ }
+ }
+
+ this.refreshBlocks();
+ } catch (e) {
+ console.error("❌ データの復元に失敗したよ:", e);
+ }
+ }
+
+ // ⚡️ ブロックの表示を最新状態に更新するヘルパー
+ refreshBlocks() {
+ setTimeout(() => {
+ if (Scratch.vm && Scratch.vm.runtime) {
+ Scratch.vm.runtime.requestBlocksDisplayUpdate();
+ }
+ }, 5);
+ }
+
+ // ⚡️【核心】内部キーの形からグローバル/ローカルを賢く見極めて、その場で復元を試みる関数
+ ensureVariableExists(internalKey) {
+ // すでに存在していれば何もしない
+ if (Object.prototype.hasOwnProperty.call(this.boolVariables, internalKey)) {
+ return;
+ }
+
+ console.log(`💡 未知のデータ「${internalKey}」を検知!自動復元を試みます。`);
+
+ let displayName = internalKey;
+ let isLocal = false;
+ let targetId = 'stage';
+
+ // 文字列の中に "_" が含まれているかチェック
+ if (internalKey.includes('_')) {
+ isLocal = true; // "_" があるならローカル変数として復元を試みる
+
+ // スプライトID自体の "_" 混入を考慮して、一番最後の "_" の位置で綺麗に分割
+ const lastIndex = internalKey.lastIndexOf('_');
+ targetId = internalKey.substring(0, lastIndex); // 前半:元の所有スプライトID
+ displayName = internalKey.substring(lastIndex + 1); // 後半:表示名
+ } else {
+ // "_" がない場合は、全体が内部キーであり表示名(グローバル変数)
+ displayName = internalKey;
+ isLocal = false;
+ targetId = 'stage';
+ }
+
+ // データ全体(internalKey)をそのままキーにして実体を生成!
+ this.boolVariables[internalKey] = false;
+ this.boolVariablesinfo[internalKey] = {
+ isLocal: isLocal,
+ targetId: targetId,
+ displayName: displayName
+ };
+
+ this.refreshBlocks();
+ }
+
+ getInfo() {
+ return {
+ id: 'utakeuchigamesBV',
+ name: 'Bool変数拡張',
+ menuIconURI: icon,
+ color1: "#ff8c1a",
+ color2: "#ff8000",
+ color3: "#db6d00",
+ blocks: [
+ {
+ blockType: Scratch.BlockType.LABEL,
+ text: '真偽値変数'
+ },
+ {
+ func: 'createUI',
+ blockType: Scratch.BlockType.BUTTON,
+ text: '変数作成フォームを開く'
+ },
+ {
+ opcode: 'setBool',
+ blockType: Scratch.BlockType.COMMAND,
+ text: 'bool値[variable]を[bool]にする',
+ arguments: {
+ variable: { type: Scratch.ArgumentType.STRING, menu: 'boolVariableMenu' },
+ bool: { type: Scratch.ArgumentType.STRING, menu: 'staticBoolMenu' }
+ }
+ },
+ {
+ opcode: 'getBool',
+ blockType: Scratch.BlockType.BOOLEAN,
+ text: 'bool値[variable]',
+ arguments: {
+ variable: { type: Scratch.ArgumentType.STRING, menu: 'boolVariableMenu' }
+ }
+ },
+ {
+ opcode: 'ifBool',
+ blockType: Scratch.BlockType.EVENT,
+ text: 'bool値[variable]が[bool]になった時',
+ isEdgeActivated: false, // startHats連動のイベント型
+ arguments: {
+ variable: { type: Scratch.ArgumentType.STRING, menu: 'boolVariableHatMenu' },
+ bool: { type: Scratch.ArgumentType.STRING, menu: 'staticBoolMenu' }
+ }
+ },
+ {
+ opcode: 'getallBool',
+ blockType: Scratch.BlockType.REPORTER,
+ text: '全部のbool値を見る',
+ },
+ {
+ opcode: 'getallboolinfo',
+ blockType: Scratch.BlockType.REPORTER,
+ text: '全部のbool値の情報を見る',
+ },
+ '---',
+ {
+ blockType: Scratch.BlockType.LABEL,
+ text: 'その他のキット'
+ },
+ {
+ opcode: 'reversebool',
+ blockType: Scratch.BlockType.BOOLEAN,
+ text: '![bool]',
+ arguments: {
+ bool: { type: Scratch.ArgumentType.BOOLEAN }
+ }
+ },
+ {
+ opcode: 'andbool',
+ blockType: Scratch.BlockType.BOOLEAN,
+ text: '[bool1] && [bool2]',
+ arguments: {
+ bool1: { type: Scratch.ArgumentType.BOOLEAN },
+ bool2: { type: Scratch.ArgumentType.BOOLEAN }
+ }
+ },
+ {
+ opcode: 'orbool',
+ blockType: Scratch.BlockType.BOOLEAN,
+ text: '[bool1] || [bool2]',
+ arguments: {
+ bool1: { type: Scratch.ArgumentType.BOOLEAN },
+ bool2: { type: Scratch.ArgumentType.BOOLEAN }
+ }
+ },
+ {
+ opcode: 'xorbool',
+ blockType: Scratch.BlockType.BOOLEAN,
+ text: '[bool1] !== [bool2]',
+ arguments: {
+ bool1: { type: Scratch.ArgumentType.BOOLEAN },
+ bool2: { type: Scratch.ArgumentType.BOOLEAN }
+ }
+ },
+ {
+ opcode: 'waitFrames',
+ blockType: Scratch.BlockType.COMMAND,
+ text: '[frames] フレーム待つ',
+ arguments: {
+ frames: { type: Scratch.ArgumentType.NUMBER, defaultValue: 1 }
+ }
+ }
+ ],
+ menus: {
+ boolVariableMenu: { acceptReporters: false, items: 'getVariableMenuItems' },
+ boolVariableHatMenu: { acceptReporters: false, items: 'getVariableMenuItems' },
+ staticBoolMenu: {
+ acceptReporters: false,
+ items: [{text: 'true', value: 'true'}, {text: 'false', value: 'false'}]
+ }
+ }
+ };
+ }
+
+ // 変数作成UIのレンダリング
+ createUI() {
+ if (this.isUIOpen) return;
+ this.isUIOpen = true;
+
+ const editingTarget = Scratch.vm.runtime.getEditingTarget();
+ const isStage = editingTarget ? !!editingTarget.isStage : false;
+ const currentTargetId = editingTarget ? (editingTarget.id ?? 'stage') : 'stage';
+
+ const overlay = document.createElement('div');
+ overlay.style.cssText = `position:fixed;top:0;left:0;right:0;bottom:0;z-index:99999;background-color:var(--ui-modal-overlay,rgba(0,0,0,0.55));color:var(--ui-modal-foreground,#333333);display:flex;justify-content:center;align-items:center;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;`;
+
+ const dialog = document.createElement('div');
+ dialog.style.cssText = `background-color:var(--ui-modal-background,#ffffff);width:360px;outline:none;border:4px solid #ff8787;padding:0;border-radius:0.5rem;user-select:none;overflow:hidden;display:flex;flex-direction:column;box-shadow:var(--shadow,0px 4px 15px rgba(0,0,0,0.3));`;
+
+ dialog.innerHTML = `
+
+
+ `;
+
+ overlay.appendChild(dialog);
+ document.body.appendChild(overlay);
+
+ setTimeout(() => {
+ const inputField = document.getElementById('varInput');
+ if (inputField) inputField.focus();
+ }, 50);
+
+ const close = () => {
+ overlay.remove();
+ this.isUIOpen = false;
+ };
+
+ overlay.onclick = (e) => {
+ if (e.target === overlay) close();
+ };
+
+ document.getElementById('ceoCloseXBtn').onclick = close;
+ document.getElementById('cancelBtn').onclick = close;
+
+ document.getElementById('okBtn').onclick = () => {
+ const name = document.getElementById('varInput').value;
+ if (name && name.trim() !== "") {
+ const trimmedName = name.trim();
+ const scopeValue = (document.querySelector('input[name="variableScopeOption"]:checked') ?? { value: 'global' }).value;
+
+ const isLocal = isStage ? false : (scopeValue === 'local');
+ const targetId = isLocal ? currentTargetId : 'stage';
+
+ let isDuplicate = false;
+
+ for (const existingKey of Object.keys(this.boolVariables)) {
+ const info = this.boolVariablesinfo[existingKey];
+ const existingDisplayName = info ? (info.displayName ?? existingKey) : existingKey;
+
+ if (existingDisplayName === trimmedName) {
+ if (!isLocal) {
+ isDuplicate = true;
+ break;
+ } else {
+ if (!info || !info.isLocal || info.targetId === targetId) {
+ isDuplicate = true;
+ break;
+ }
+ }
+ }
+ }
+
+ if (isDuplicate) {
+ alert(`❌ エラー: 「${trimmedName}」という名前の変数はすでに存在するか、競合するため作成できません!`);
+ return;
+ }
+
+ const internalKey = isLocal ? `${targetId}_${trimmedName}` : trimmedName;
+
+ this.boolVariables[internalKey] = false;
+ this.boolVariablesinfo[internalKey] = {
+ isLocal: isLocal,
+ targetId: targetId,
+ displayName: trimmedName
+ };
+
+ this.refreshBlocks();
+ }
+ close();
+ };
+
+ document.getElementById('varInput').onkeypress = (e) => {
+ if (e.key === 'Enter') {
+ document.getElementById('okBtn').click();
+ }
+ };
+ }
+
+ // 変数メニュー(リスト)の生成
+ getVariableMenuItems(currentlySelectedValue) {
+ const menuItems = [];
+ const currentTarget = Scratch.vm.runtime.getEditingTarget();
+ const currentTargetId = currentTarget ? (currentTarget.id ?? 'stage') : 'stage';
+
+ for (const key of Object.keys(this.boolVariables)) {
+ const info = this.boolVariablesinfo[key];
+ const dispName = info ? (info.displayName ?? key) : key;
+
+ if (info) {
+ // グローバル変数、または「いま編集中のスプライト」に属するローカル変数のみメニューに出す
+ if (!info.isLocal || info.targetId === currentTargetId) {
+ menuItems.push({ text: dispName, value: key });
+ }
+ } else {
+ menuItems.push({ text: dispName, value: key });
+ }
+ }
+
+ const isValidUserVar = Object.prototype.hasOwnProperty.call(this.boolVariables, currentlySelectedValue);
+
+ if (!isValidUserVar || !currentlySelectedValue || currentlySelectedValue === '(空)') {
+ menuItems.unshift({ text: '(空)', value: '(空)' });
+ } else {
+ menuItems.push({ text: '(空)', value: '( Stock )' });
+ }
+
+ if (menuItems.length > 0) {
+ menuItems.push({ text: '────────────────', value: 'IGNORE_CLICK' });
+ }
+ menuItems.push({ text: '🔥 変数を削除するフォームを開く', value: 'OPEN_DELETE_UI' });
+
+ return menuItems;
+ }
+
+ // 変数削除UIのレンダリング
+ // 変数削除UIのレンダリング
+ createDeleteUI() {
+ if (this.isDelUIOpen) return;
+ this.isDelUIOpen = true;
+
+ setTimeout(() => {
+ const currentTarget = Scratch.vm.runtime.getEditingTarget();
+ const currentTargetId = currentTarget ? (currentTarget.id ?? 'stage') : 'stage';
+
+ // 今開いているスプライトで削除できる変数(グローバル or 自分のローカル)を絞り込む
+ const deleteableKeys = Object.keys(this.boolVariables).filter(internalKey => {
+ const info = this.boolVariablesinfo[internalKey];
+ if (!info) return true;
+ return !info.isLocal || info.targetId === currentTargetId;
+ });
+
+ if (deleteableKeys.length === 0) {
+ alert("❌ 削除できる変数がありません!");
+ this.isDelUIOpen = false;
+ return;
+ }
+
+ const overlay = document.createElement('div');
+ overlay.style.cssText = `position:fixed;top:0;left:0;right:0;bottom:0;z-index:99999;background-color:rgba(0,0,0,0.6);display:flex;justify-content:center;align-items:center;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;`;
+
+ const dialog = document.createElement('div');
+ dialog.style.cssText = `background-color:#ffffff;width:340px;border:4px solid #ff4c4c;border-radius:0.5rem;overflow:hidden;display:flex;flex-direction:column;box-shadow:0px 4px 15px rgba(0,0,0,0.3);`;
+
+ // 選択肢のHTMLをループで組み立てる
+ let optionsHtml = '';
+ for (const key of deleteableKeys) {
+ const info = this.boolVariablesinfo[key];
+ const disp = info ? (info.displayName ?? key) : key;
+ const typeText = info ? (info.isLocal ? '[ローカル]' : '[グローバル]') : '[不明]';
+ optionsHtml += ``;
+ }
+
+ // ⚡️【修正箇所】 ${optionsHtml} の前の「\\」を消去したよ!
+ dialog.innerHTML = `
+
+ 変数の削除
+
+
+
削除する変数を選択してください:
+
+
+
+
+
+
+ `;
+
+ overlay.appendChild(dialog);
+ document.body.appendChild(overlay);
+
+ const closeDel = () => {
+ overlay.remove();
+ this.isDelUIOpen = false;
+ };
+
+ document.getElementById('cancelDelBtn').onclick = closeDel;
+ overlay.onclick = (e) => { if (e.target === overlay) closeDel(); };
+
+ document.getElementById('executeDelBtn').onclick = () => {
+ const targetKey = document.getElementById('deleteSelect').value;
+ const info = this.boolVariablesinfo[targetKey];
+ const dispName = info ? (info.displayName ?? targetKey) : targetKey;
+
+ if (confirm(`本当に bool値「${dispName}」を完全に削除しますか?\n(この変数を使用している他のブロックは初期状態に戻ります)`)) {
+ delete this.boolVariables[targetKey];
+ delete this.boolVariablesinfo[targetKey];
+
+ closeDel();
+
+ setTimeout(() => {
+ alert(`🎉 bool値「${dispName}」を完全に削除しました!`);
+ if (Scratch.vm && Scratch.vm.runtime) {
+ Scratch.vm.runtime.requestBlocksDisplayUpdate();
+ }
+ }, 100);
+ return;
+ }
+ closeDel();
+ };
+ }, 100);
+ }
+
+ // ⚡️ セットブロック
+ setBool(args, util) {
+ if (args.variable === 'OPEN_DELETE_UI') {
+ this.createDeleteUI();
+ return;
+ }
+ if (args.variable === 'IGNORE_CLICK' || args.variable === '(空)') return;
+
+ // 🔥 処理が走る手前で、未知のキーであれば安全に復元する
+ this.ensureVariableExists(args.variable);
+
+ const prevalue = this.boolVariables[args.variable];
+ this.boolVariables[args.variable] = (args.bool === 'true');
+ const data = {
+ "variable": args.variable.toString(),
+ bool: String(args.bool)
+ };
+
+ if (prevalue != (args.bool === 'true')) {
+ // 変数が確実に作られてから startHats を呼ぶので、イベント処理もバグらない
+ Scratch.vm.runtime.startHats("BV_ifBool", data, false);
+ }
+ }
+
+ // ⚡️ 値取得ブロック
+ getBool(args, util) {
+ if (args.variable === 'OPEN_DELETE_UI') {
+ this.createDeleteUI();
+ return false;
+ }
+ if (args.variable === 'IGNORE_CLICK' || args.variable === '(空)') return false;
+
+ // 🔥 値を読み込む手前でも、未知のキーであれば安全に復元する
+ this.ensureVariableExists(args.variable);
+
+ return !!this.boolVariables[args.variable];
+ }
+
+ // ⚡️ ハットブロック(isEdgeActivated: falseのため、startHatsのフィルタ判定として一瞬だけ動く)
+ ifBool(args, util) {
+ if (args.variable === 'IGNORE_CLICK' || args.variable === '(空)') return false;
+
+ // setBool側ですでに実体が保証されているため、ここでは純粋な一致判定だけを行う
+ return args.variable === util.currentBackgroundData.variable && args.bool === util.currentBackgroundData.bool;
+ }
+
+ getallBool(args) { return JSON.stringify(this.boolVariables); }
+ getallboolinfo(args) { return JSON.stringify(this.boolVariablesinfo); }
+
+ reversebool(args,util){ return !args.bool; }
+ andbool(args,util){ return !!(args.bool1 && args.bool2); }
+ orbool(args,util){ return !!(args.bool1 || args.bool2); }
+ xorbool(args,util){ return (args.bool1 !== args.bool2); }
+ async waitFrames(args, util) {
+ // frames分 * deltaTime(秒) で待機時間(ミリ秒)を算出
+ // ※deltaTimeはBEFORE_EXECUTEで秒単位で計算されている前提
+ //const waitMs = (args.frames * deltaTime) * 1000;
+
+ // PromiseでsetTimeoutをラップしてawaitできるようにする
+ //await new Promise(resolve => setTimeout(resolve, waitMs));
+
+ // イメージ:内部カウンターを使う方法
+ const targetFrame = this.frameCount + args.frames - 1;
+ while (this.frameCount < targetFrame) {
+ await new Promise(resolve => requestAnimationFrame(resolve));
+ }
+ }
+ }
+
+ const utakeuchigamesBoolvariableextension = new utakeuchigamesBoolvariable();
+
+ vm.runtime.on("BEFORE_EXECUTE", () => {
+ utakeuchigamesBoolvariableextension.frameCount++;
+ const now = performance.now();
+
+ if (previousTime === 0) {
+ // First frame. We used to always return 0 here, but that can break projects that
+ // expect delta time to always be non-zero. Instead we'll make our best guess.
+ deltaTime = 1 / vm.runtime.frameLoop.framerate;
+ } else {
+ deltaTime = (now - previousTime) / 1000;
+ }
+
+ previousTime = now;
+ });
+
+ Scratch.extensions.register(Boolvariableextension);
+
+})(Scratch);
diff --git a/static/images/utakeuchigames/boolvariables.avif b/static/images/utakeuchigames/boolvariables.avif
new file mode 100644
index 000000000..acb825c4a
Binary files /dev/null and b/static/images/utakeuchigames/boolvariables.avif differ