From bd0adfe32b839383b9ae67f5b30569825349d987 Mon Sep 17 00:00:00 2001 From: hernia_0721 <22e1407@andrew.ac.jp> Date: Sun, 1 Feb 2026 08:58:42 +0900 Subject: [PATCH] =?UTF-8?q?"/"=20=E3=81=AB=E3=83=95=E3=82=A1=E3=82=A4?= =?UTF-8?q?=E3=83=AB=E3=82=92=E3=82=A2=E3=83=83=E3=83=97=E3=83=AD=E3=83=BC?= =?UTF-8?q?=E3=83=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.html | 127 +++ lang.js | 72 ++ libs.js | 2773 ++++++++++++++++++++++++++++++++++++++++++++++++ tyrano.base.js | 252 +++++ tyrano.css | 903 ++++++++++++++++ 5 files changed, 4127 insertions(+) create mode 100644 index.html create mode 100644 lang.js create mode 100644 libs.js create mode 100644 tyrano.base.js create mode 100644 tyrano.css diff --git a/index.html b/index.html new file mode 100644 index 0000000..8b591ac --- /dev/null +++ b/index.html @@ -0,0 +1,127 @@ + + + + Loading TyranoScript + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + +
+
+

+

+
+ + +
+ + + + + + diff --git a/lang.js b/lang.js new file mode 100644 index 0000000..3e44f3f --- /dev/null +++ b/lang.js @@ -0,0 +1,72 @@ +window.tyrano_lang = { + word: { + go_title: "タイトルに戻ります。よろしいですか?", + exit_game: "ウィンドウを閉じて終了します。よろしいですか?", + not_saved: "まだ、保存されているデータがありません。", + confirm: "確認", + confirm_beforeunload: "保存していない内容があります。ゲームを終了してもよろしいですか?", + tag: "タグ", + not_exists: "は存在しません。", + error: "エラーが発生しました。スクリプトを確認して下さい。", + label: "ラベル", + label_double: "は同一シナリオファイル内に重複しています。", + error_occurred: "エラーが発生しました。", + save_file_violation_1: "セーブデータの移動を検知しました。信頼できるセーブデータでない場合、絶対に読み込まないでください。", + save_file_violation_2: "セーブデータを読み込んでゲームを起動してもよろしいですか?", + save_file_violation_3: "起動を中止しました。セーブデータを削除してもう一度、起動してください。", + double_start: "すでにゲームは起動済みです。二重起動はできません。", + reload: "リロード", + line: "{ line }行目", + initialized_saved_data: "セーブデータを初期化しました。", + initializing_saved_data: "セーブデータを初期化します。よろしいですか?", + saved_data_is_corrupted: "セーブデータが壊れている可能性があります。セーブデータを初期化しますか?", + save_does_not_work: "この環境ではセーブ機能が動作しません。ローカルで実行している場合などに発生します。", + undefined_tag: "タグ「{ name }」は存在しません。", + undefined_label: "ラベル「{ name }」は存在しません。", + undefined_character: "指定されたキャラクター「{ name }」は定義されていません。[chara_new]で定義してください。", + undefined_face: "指定されたキャラクター「{ name }」の表情「{ face }」は定義されていません。[chara_face]で定義してください。", + undefined_character_parts: "指定されたキャラクター「{ name }」の差分パーツは定義されていません。[chara_layer]で定義してください。", + undefined_keyframe: "指定されたキーフレームアニメーション「{ keyframe }」は定義されていません。[keyframe]で定義してください。", + undefined_3d_model: "指定された3Dモデル「{ name }」は定義されていません。", + preload_failure_sound: + "音声ファイル「{ src }」が見つかりません。場所はフルパスで指定されていますか?\n\n(適切な例) ./data/bgm/my_bgm.mp3", + preload_failure_image: + "画像ファイル「{ src }」が見つかりません。場所はフルパスで指定されていますか?\n\n(適切な例) ./data/image/my_image.png", + preload_failure_video: + "動画ファイル「{ src }」が見つかりません。場所はフルパスで指定されていますか?\n\n(適切な例) ./data/video/my_video.mp4", + invalid_keyframe: "キーフレームが無効な値です。", + invalid_keyframe_percentage: + 'キーフレームに設定されているパーセンテージ「{ percentage }」は無効な値です。"0%"、"30%"のように指定してください。', + error_in_iscript: "[iscript] の内部でエラーが発生しました。", + missing_endif: "[if] のあとに [elsif] [else] [endif] のいずれかが見つかりません。または、[if] 内のタグの数が多すぎます。", + missing_endmacro: "[macro] のあとに [endmacro] が見つかりません。または、[macro] 内のタグの数が多すぎます。", + missing_endignore: "[ignore] のあとに [endignore] が見つかりません。または、[ignore] 内の数が多すぎます。", + missing_parameter: "タグ「{ tag }」にパラメーター「{ param }」は必須です。", + if_and_endif_do_not_match: "[if] と [endif] の数が一致しません。", + unsupported_extensions: "「{ ext }」はサポートしていないファイル形式です。", + undefined_keyconfig: + 'キーコンフィグが定義されていません。KeyConfig.js が存在しないか、KeyConfig.js 内で構文エラーが発生している可能性があります。カンマ "," やブラケット "]" "}" が不足していないかどうか、確認してください。', + compensate_missing_quart: + '予期しない "]" を検知しました。パラメータのクォートの数が足りていない可能性があるため、自動的に修正して解釈します。\n\n修正前: { before }\n修正後: { after }', + duplicate_label: + "ラベル「{ name }」は同一シナリオファイル内に重複しています。ラベル名は同一シナリオファイル内で重複しないようにしてください。", + file_not_found: "ファイルが見つかりませんでした。\n\n{ path }", + patch_not_found: "パッチファイルが見つかりませんでした。\n\n{ path }", + new_patch_found: "新しいアップデートが見つかりました。\n\nVer: { version }\n{ message }\n\n アップデートを行いますか?", + apply_web_patch: "アップデートを行います。完了後、自動的にゲームは終了します。", + apply_patch_complete: "アップデートを適用しました。ゲームを再起動してください。", + }, + + novel: { + file_menu_button_save: "menu_button_save.gif", + file_menu_button_load: "menu_button_load.gif", + file_menu_button_message_close: "menu_message_close.gif", + file_menu_button_skip: "menu_button_skip.gif", + file_menu_button_title: "menu_button_title.gif", + file_menu_button_close: "menu_button_close.png", + file_menu_bg: "menu_bg.jpg", + file_save_bg: "menu_save_bg.jpg", + file_load_bg: "menu_load_bg.jpg", + file_button_menu: "button_menu.png", + }, +}; diff --git a/libs.js b/libs.js new file mode 100644 index 0000000..d313764 --- /dev/null +++ b/libs.js @@ -0,0 +1,2773 @@ +(function ($) { + //jquery 拡張 + + $.getBaseURL = function () { + var str = location.pathname; + var i = str.lastIndexOf("/"); + return str.substring(0, i + 1); + }; + + $.getDirPath = function (str) { + var i = str.lastIndexOf("/"); + return str.substring(0, i + 1); + }; + + $.isHTTP = function (str) { + + try { + if ($.isBase64(str)) { + return true; + } + + if (str.substring(0, 4) === "http" || str.substring(0, 4) === "file" || str.substring(0, 6) === "./data" || str.substring(0, 1) === "/" || str.substring(1, 2) === ":") { + return true; + } else { + return false; + } + } catch (e) { + console.log(e) + } + + return false; + }; + + + $.play_audio = function (audio_obj) { + audio_obj.play(); + }; + + $.toBoolean = function (str) { + if (str == "true") { + return true; + } else { + return false; + } + }; + + $.getAngle = function () { + let angle = screen && screen.orientation && screen.orientation.angle; + if (angle === undefined) { + angle = window.orientation; // iOS用 + } + + return angle; + }; + + //横幅の方が大きければtrue; + $.getLargeScreenWidth = function () { + let w = parseInt(window.innerWidth); + let h = parseInt(window.innerHeight); + + if (w > h) { + return true; + } else { + return false; + } + }; + + $.localFilePath = function () { + + var path = ""; + //Mac os Sierra 対応 + if (process.execPath.indexOf("var/folders") != -1) { + path = process.env.HOME + "/_TyranoGameData"; + } else { + path = $.getExePath(); + } + + return path; + }; + + $.getViewPort = function () { + let width; + let height; + + if (self.innerHeight) { + // all except Explorer + width = self.innerWidth; + height = self.innerHeight; + } else if (document.documentElement && document.documentElement.clientHeight) { + // Explorer 6 Strict Mode + width = document.documentElement.clientWidth; + height = document.documentElement.clientHeight; + } else if (document.body) { + // other Explorers + width = document.body.clientWidth; + height = document.body.clientHeight; + } + + return { + width: width, + height: height, + }; + }; + + $.escapeHTML = function (val, replace_str) { + val = val || ""; + var t = $("
").text(val).html(); + + if (replace_str) { + if (t === "") { + t = replace_str; + } + } + return t; + }; + + $.br = function (txtVal) { + txtVal = txtVal.replace(/\r\n/g, "
"); + txtVal = txtVal.replace(/(\n|\r)/g, "
"); + return txtVal; + }; + + const dateFormatter = { + _fmt: { + hh: function (date) { + return ("0" + date.getHours()).slice(-2); + }, + h: function (date) { + return date.getHours(); + }, + mm: function (date) { + return ("0" + date.getMinutes()).slice(-2); + }, + m: function (date) { + return date.getMinutes(); + }, + ss: function (date) { + return ("0" + date.getSeconds()).slice(-2); + }, + dd: function (date) { + return ("0" + date.getDate()).slice(-2); + }, + d: function (date) { + return date.getDate(); + }, + s: function (date) { + return date.getSeconds(); + }, + yyyy: function (date) { + return date.getFullYear() + ""; + }, + yy: function (date) { + return date.getYear() + ""; + }, + t: function (date) { + return date.getDate() <= 3 ? ["st", "nd", "rd"][date.getDate() - 1] : "th"; + }, + w: function (date) { + return ["Sun", "$on", "Tue", "Wed", "Thu", "Fri", "Sat"][date.getDay()]; + }, + MMMM: function (date) { + return [ + "January", + "February", + "$arch", + "April", + "$ay", + "June", + "July", + "August", + "September", + "October", + "November", + "December", + ][date.getMonth()]; + }, + MMM: function (date) { + return ["Jan", "Feb", "$ar", "Apr", "$ay", "Jun", "Jly", "Aug", "Spt", "Oct", "Nov", "Dec"][date.getMonth()]; + }, + MM: function (date) { + return ("0" + (date.getMonth() + 1)).slice(-2); + }, + M: function (date) { + return date.getMonth() + 1; + }, + $: function (date) { + return "M"; + }, + }, + _priority: ["hh", "h", "mm", "m", "ss", "dd", "d", "s", "yyyy", "yy", "t", "w", "MMMM", "MMM", "MM", "M", "$"], + format: function (date, format) { + return this._priority.reduce((res, fmt) => res.replace(fmt, this._fmt[fmt](date)), format); + }, + }; + + /** + * Dateをフォーマットする + * @param {Date} date 日付 + * @param {string} format フォーマット (例) "yyyy/M/d hh:mm:ss" + * @returns {string} + */ + $.formatDate = function (date, format) { + return dateFormatter.format(date, format); + }; + + //現在時刻を取得 + //現在の日 + $.getNowDate = function (format = "yyyy/M/d") { + return $.formatDate(new Date(), format); + }; + + //現在の時刻 + $.getNowTime = function (format = "hh:mm:ss") { + return $.formatDate(new Date(), format); + }; + + $.convertRem = function (px_val) { + function getRootElementFontSize() { + // Returns a number + return parseFloat( + // of the computed font-size, so in px + getComputedStyle( + // for the root element + document.documentElement, + ).fontSize, + ); + } + + return px_val * getRootElementFontSize(); + }; + + //複数のスクリプトを一括して読み込み + $.getMultiScripts = function (arr, cb) { + var cnt_script = arr.length; + var load_cnt = 0; + + if (cnt_script == 0) { + cb(); + return; + } + + function getScript(src) { + $.getScript(arr[load_cnt], function (e) { + load_cnt++; + + if (cnt_script == load_cnt) { + if (typeof cb == "function") { + cb(); + } + } else { + getScript(arr[load_cnt]); + } + }); + } + + getScript(arr[0]); + }; + + $.convertSecToString = function (val) { + if (val == 0) { + return "-"; + } + var day = Math.floor(val / (24 * 60 * 60)); + var hour = Math.floor((val % (24 * 60 * 60)) / (60 * 60)); + var minute = Math.floor(((val % (24 * 60 * 60)) % (60 * 60)) / 60); + var second = Math.floor(((val % (24 * 60 * 60)) % (60 * 60)) % 60); + + var str = ""; + if (day !== 0) { + str += day + "日"; + } + if (hour !== 0) { + str += hour + "時間"; + } + if (minute !== 0) { + str += minute + "分"; + } + if (second !== 0) { + str += second + "秒"; + } + + return str; + }; + + $.secToMinute = function (val) { + if (val === 0) { + return "-"; + } + + var m = Math.floor(val / 60); + var s = Math.floor(val % 60); + var str = ""; + + if (m !== 0) { + str += m + "分"; + } + str += s + "秒"; + + return str; + }; + + $.trim = function (str) { + if (str) { + } else { + return ""; + } + + return str.replace(/^\s+|\s+$/g, ""); + }; + + $.tag = function (tag_name, pm) { + var pm_str = ""; + for (key in pm) { + pm_str += " " + key + '="' + pm[key] + '" '; + } + return "[" + tag_name + " " + pm_str + " ]"; + }; + + $.rmspace = function (str) { + str = str.replace(/ /g, ""); + str = str.replace(/ /g, ""); + str = str.replace(/\r\n?/g, ""); + + return str; + }; + + $.replaceAll = function (text, searchString, replacement) { + if (typeof text != "string") { + return text; + } + + //置換のコード変えてみた + var result = text.split(searchString).join(replacement); + + return result; + }; + + //確証しを取得 + $.getExt = function (str) { + + var questionMarkIndex = str.indexOf('?'); + var str = questionMarkIndex !== -1 ? str.substring(0, questionMarkIndex) : str; + + return str.split(".").pop(); + }; + + //指定した拡張子を付ける。拡張子がなければ + $.setExt = function (name, ext_str) { + var tmp = name.split("."); + if (tmp.length == 1) { + name = name + "." + ext_str; + } + + return name; + }; + + //要素をクローンします + $.cloneObject = function (source) { + return $.extend(true, {}, source); + }; + + //透明度を適切な値に変更 + $.convertOpacity = function (val) { + //255をマックスとして計算する + + var p = val / 255; + + return p; + }; + + //パスにfgimage bgimage image が含まれていた場合、それを適応する + $.convertStorage = function (path) { }; + + $.convertColor = function (val) { + if (val.indexOf("0x") != -1) { + return val.replace("0x", "#"); + } + + return val; + }; + + $.convertBold = function (flag) { + if (flag == "true") { + return "bold"; + } + + return ""; + }; + + $.convertItalic = function (flag) { + if (flag == "true") { + return "italic"; + } + + return ""; + }; + + $.send = function (url, obj, call_back) { + //game.current_story_file = story_file; + $.ajax({ + type: "POST", + url: url, + data: obj, + dataType: "json", + complete: function () { + //通信終了時の処理 + $.hideLoading(); + }, + success: function (data, status) { + $.hideLoading(); + + var data_obj = data; + if (call_back) { + call_back(data_obj); + } + }, + }); + }; + + $.loadText = function (file_path, callback) { + if (window.TYRANO) window.TYRANO.kag.showLoadingLog(); + + let dataType = "text"; + + if ($.getExt(file_path) == "json") { + dataType = "json"; + } + + $.ajax({ + url: file_path + "?" + Math.floor(Math.random() * 1000000), + dataType: dataType, + cache: false, + success: function (text) { + if (window.TYRANO) window.TYRANO.kag.hideLoadingLog(); + const order_str = text; + callback(order_str); + }, + error: function () { + if (window.TYRANO) window.TYRANO.kag.hideLoadingLog(); + alert($.lang("file_not_found", { path: file_path })); + callback(""); + }, + }); + }; + + $.loadTextSync = function (file_path) { + + let dataType = "text"; + + if ($.getExt(file_path) == "json") { + dataType = "json"; + } + + return new Promise((resolve, reject) => { + $.ajax({ + url: file_path + "?" + Math.floor(Math.random() * 1000000), + dataType: dataType, + cache: false, + success: function (text) { + if (window.TYRANO) window.TYRANO.kag.hideLoadingLog(); + const order_str = text; + resolve(order_str); + }, + + error: function () { + if (window.TYRANO) window.TYRANO.kag.hideLoadingLog(); + alert($.lang("file_not_found", { path: file_path })); + reject(); + }, + }); + }); + }; + + //クッキーを取得 + $.getCookie = function (key) { + var tmp = document.cookie + ";"; + var index1 = tmp.indexOf(key, 0); + if (index1 != -1) { + tmp = tmp.substring(index1, tmp.length); + var index2 = tmp.indexOf("=", 0) + 1; + var index3 = tmp.indexOf(";", index2); + return tmp.substring(index2, index3); + } + return null; + }; + + $.isNull = function (str) { + if (str == null) { + return ""; + } else { + } + + return str; + }; + + $.dstop = function () { + console.log("dstop"); + }; + + //ユーザ環境を取得 + $.userenv = function () { + var ua = navigator.userAgent; + if (ua.indexOf("iPhone") > -1) { + return "iphone"; + } else if (ua.indexOf("iPad") > -1) { + return "iphone"; + } else if (ua.indexOf("Android") > -1) { + return "android"; + } else if (ua.indexOf("Chrome") > -1 && navigator.platform.indexOf("Linux") > -1) { + return "android"; + } else { + return "pc"; + } + }; + + $.isTyranoPlayer = function () { + if (typeof _tyrano_player != "undefined") { + return true; + } else { + return false; + } + }; + + $.lang = function (key, replace_map, target = "word") { + if (typeof replace_map === "string") target = replace_map; + let string_defined = tyrano_lang[target][key]; + if (string_defined) { + if (replace_map) { + for (const replace_key in replace_map) { + const pattern = new RegExp(`\\{\\s*${replace_key}\\s*\\}`, "g"); + string_defined = string_defined.replace(pattern, replace_map[replace_key]); + } + } + return string_defined; + } else { + return "NOT_DEFINED"; + } + }; + + $.novel = function (key) { + if (tyrano_lang["novel"][key]) { + return tyrano_lang["novel"][key]; + } else { + return "NOT_DEFINED"; + } + }; + + //ユーザのブラウザ情報を取得 + $.getBrowser = function () { + var userAgent = window.navigator.userAgent.toLowerCase(); + + if (userAgent.indexOf("msie") >= 0 || userAgent.indexOf("trident") >= 0) { + return "msie"; + } else if (userAgent.indexOf("edge") > -1) { + return "edge"; + } else if (userAgent.indexOf("firefox") > -1) { + return "firefox"; + } else if (userAgent.indexOf("opera") > -1) { + return "opera"; + } else if (userAgent.indexOf("chrome") > -1) { + return "chrome"; + } else if (userAgent.indexOf("safari") > -1) { + return "safari"; + } else if (userAgent.indexOf("applewebkit") > -1) { + return "safari"; + } else { + return "unknown"; + } + }; + + $.isNWJS = function () { + //Electronならfalse + if ($.isElectron()) { + return false; + } + + // Node.js で動作しているか + var isNode = typeof process !== "undefined" && typeof require !== "undefined"; + // ブラウザ上(非Node.js)で動作しているか + var isBrowser = !isNode; + // node-webkitで動作しているか + var isNodeWebkit; + try { + isNodeWebkit = isNode ? typeof require("nw.gui") !== "undefined" : false; + } catch (e) { + isNodeWebkit = false; + } + + if (isNodeWebkit) { + // node-webkitで動作 + return true; + } else if (isNode) { + // Node.js上で動作している + return true; + } else { + // 通常のWebページとして動作している + return false; + } + }; + + $.isNeedClickAudio = function () { + //プレイヤーはクリックの必要なし + if ($.isTyranoPlayer()) { + return false; + } + + //ブラウザやスマホアプリは必要 + if ($.isElectron() || $.isNWJS()) { + return false; + } + + return true; + }; + + $.isElectron = function () { + if (navigator.userAgent.indexOf("TyranoErectron") != -1) { + return true; + } else { + return false; + } + }; + + //オブジェクトを引き継ぐ。 + $.extendParam = function (pm, target) { + var tmp = target; + + for (let key in target) { + if (pm[key]) { + if (pm[key] != "") { + target[key] = pm[key]; + } + } + } + + return target; + }; + + $.insertRule = function (css_str) { + var sheet = (function () { + var style = document.createElement("style"); + document.getElementsByTagName("head")[0].appendChild(style); + return style.sheet; + })(); + sheet.insertRule(css_str, 0); + }; + + $.insertRuleToTyranoCSS = function (css_str) { + const sheet = $('link[href*="tyrano/tyrano.css"]').get(0).sheet; + sheet.insertRule(css_str, sheet.cssRules.length); + }; + + $.swfName = function (str) { + if (navigator.appName.indexOf("Microsoft") != -1) { + return window[str]; + } else { + return document[str]; + } + }; + + //古いトランス。 + $.trans_old = function (method, j_obj, time, mode, callback) { + if (method == "crossfade" || mode == "show") { + if (time == 0) { + if (mode == "show") { + j_obj.show(); + } else { + j_obj.hide(); + } + if (callback) { + callback(); + } + } else { + var ta = {}; + + if (mode == "show") { + ta = { + opacity: "show", + }; + } else { + ta = { + opacity: "hide", + }; + } + + j_obj.animate(ta, { + duration: time, + easing: "linear", + complete: function () { + if (callback) { + callback(); + } + }, //end complerte + }); + } + + return false; + } else { + if (mode == "hide") { + j_obj.hide(method, time, function () { + if (callback) callback(); + }); + } else if (mode == "show") { + j_obj.show(method, time, function () { + if (callback) callback(); + }); + } + } + }; + + //コンバート v450rc5以前 + var _map_conv_method = { + corssfade: "fadeIn", + explode: "zoomIn", + slide: "slideInLeft", + blind: "bounceIn", + bounce: "bounceIn", + clip: "flipInX", + drop: "slideInLeft", + fold: "fadeIn", + puff: "fadeIn", + scale: "zoomIn", + shake: "fadeIn", + size: "zoomIn", + }; + + $.trans = function (method, j_obj, time, mode, callback) { + if (method == "crossfade") { + method = "fadeIn"; + } else if (_map_conv_method[method]) { + method = _map_conv_method[method]; + } + + j_obj.css("animation-duration", parseInt(time) + "ms"); + + if (mode == "hide") { + j_obj.show(); + method = $.replaceAll(method, "In", "Out"); + const animationEnd = "webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend"; + j_obj.addClass("animated " + method).one(animationEnd, function () { + j_obj.off(animationEnd); + j_obj.css("animation-duration", ""); + $(this).remove(); + if (callback) { + //callback(); + } + }); + } else if (mode == "show") { + j_obj.show(); + const animationEnd = "webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend"; + j_obj.addClass("animated " + method).one(animationEnd, function () { + j_obj.off(animationEnd); + j_obj.css("animation-duration", ""); + $(this).removeClass("animated " + method); + if (callback) { + callback(); + } + }); + } + }; + + //要素から空白のオブジェクトを削除して返却する + $.minifyObject = function (obj) { + for (let key in obj) { + if (obj[key] == null || obj[key] == "") { + delete obj[key]; + } + } + + return obj; + }; + + $.preloadImgCallback = function (j_menu, cb, that) { + var img_storage = []; + + j_menu.find("img").each(function () { + img_storage.push($(this).attr("src")); + }); + + //ロードが全て完了したら、ふわっと出す + var sum = 0; + for (var i = 0; i < img_storage.length; i++) { + that.kag.preload(img_storage[i], function () { + sum++; + if (img_storage.length == sum) { + cb(); + } + }); + } + + if (img_storage.length == 0) { + cb(); + } + }; + + $.removeStorage = function (key, type) { + if (type == "file") { + $.removeStorageFile(key); + } else { + $.removeStorageWeb(key); + } + }; + + $.setStorage = function (key, val, type) { + if (type == "webstorage_compress") { + $.setStorageCompress(key, val); + } else if (type == "file") { + $.setStorageFile(key, val); + } else { + $.setStorageWeb(key, val); + } + }; + + //PC版のみ。実行フォルダを取得 + /* + $.getProcessPath = function(){ + var path = process.execPath; + var tmp_index = path.indexOf(".app"); + var os = "mac"; + if(tmp_index == -1){ + tmp_index = path.indexOf(".exe"); + os="win"; + } + var tmp_path =  path.substr(0,tmp_index); + var path_index =0; + if(os=="mac"){ + path_index = tmp_path.lastIndexOf("/"); + }else{ + path_index = tmp_path.lastIndexOf("\\"); + } + + var out_path = path.substr(0,path_index); + return out_path; + + }; + */ + + $.getOS = function () { + if ($.isElectron()) { + if (process.platform == "darwin") { + return "mac"; + } else { + return "win"; + } + } else { + const ua = window.navigator.userAgent.toLowerCase(); + if (ua.includes("windows nt")) { + return "win"; + } else if (ua.includes("android")) { + return "android"; + } else if (ua.includes("iphone") || ua.includes("ipad")) { + return "ios"; + } else if (ua.includes("mac os x")) { + return "mac"; + } else { + return ""; + } + } + }; + + $.makeSaveKey = function () { + var S = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + var N = 16; + let key = Array.from(Array(N)) + .map(() => S[Math.floor(Math.random() * S.length)]) + .join(""); + return key; + }; + + $.getStorage = function (key, type) { + var gv = "null"; + + if (type == "webstorage_compress") { + gv = $.getStorageCompress(key); + } else if (type == "file") { + gv = $.getStorageFile(key); + } else { + gv = $.getStorageWeb(key); + } + + return gv; + }; + + $.removeStorageWeb = function (key) { + localStorage.removeItem(key); + }; + + $.setStorageWeb = function (key, val) { + val = JSON.stringify(val); + //localStorage.setItem(key, LZString.compress(escape(val))); + try { + localStorage.setItem(key, escape(val)); + } catch (e) { + console.error("セーブデータ。localstorageが利用できません。"); + return; + } + }; + + $.getStorageWeb = function (key) { + try { + var gv = "null"; + + if (localStorage.getItem(key)) { + //gv = unescape(LZString.decompress(localStorage.getItem(key))); + gv = unescape(localStorage.getItem(key)); + } + + if (gv == "null") return null; + } catch (e) { + //alert("この環境はセーブ機能を利用できません。ローカルで実行している場合などに発生します"); + //$.confirmSaveClear(); + + console.error("セーブデータ。localstorageが利用できません。"); + return null; + } + + return gv; + }; + + $.playerHtmlPath = function (html) { + if ("appJsInterface" in window) { + //Android + } else { + if (typeof TyranoPlayer == "function") { + //playerの場合HTMLを修正する必要がある + var result_html = ""; + while (1) { + var index = html.indexOf("file:///"); + if (index == -1) { + result_html += html; + break; + } else { + result_html += html.substring(0, index); + html = html.substring(index, html.length); + + var replace_index = html.indexOf("/game/data"); + const tmp_html = html.substring(replace_index + "/game/data".length, html.length); + html = "./data" + tmp_html; + } + } + + if (result_html != "") { + html = result_html; + } + } + } + + return html; + }; + + $.confirmSaveClear = function () { + if (confirm($.lang("saved_data_is_corrupted"))) { + alert($.lang("initialized_saved_data")); + TYRANO.kag.removeSaveData(); + } + }; + + $.setStorageCompress = function (key, val) { + val = JSON.stringify(val); + localStorage.setItem(key, LZString.compress(escape(val))); + //localStorage.setItem(key, escape(val)); + }; + + $.getStorageCompress = function (key) { + try { + var gv = "null"; + + if (localStorage.getItem(key)) { + gv = unescape(LZString.decompress(localStorage.getItem(key))); + + if (gv == "null") { + gv = unescape(localStorage.getItem(key)); + } + } + + if (gv == "null") return null; + } catch (e) { + console.log("=============="); + console.log(e); + alert($.lang("save_does_not_work")); + $.confirmSaveClear(); + } + + return gv; + }; + + $.getExtWithFile = function (str) { + var filename = ""; + if (str.indexOf("/") != -1) { + filename = str.split("/").pop(); + } else { + filename = str; + } + + var dir_name = $.replaceAll(str, filename, ""); + + var ext = ""; + if (filename.indexOf(".") != -1) { + ext = str.split(".").pop(); + } else { + ext = ""; + //拡張子がない場合はディレクトリ名とする。 + dir_name = str; + } + var name = $.replaceAll(filename, "." + ext, ""); + + return { filename: filename, ext: ext, name: name, dir_name: dir_name }; + }; + + //getExePathのキャッシュ + $.cacheExePath = ""; + + //PC用の実行パスを取得 + $.getExePath = function () { + + if ($.cacheExePath != "") { + return $.cacheExePath; + } + + //TyranoStudio.app/Contents/Resources/app + let path = window.studio_api.ipcRenderer.sendSync("getAppPath", {}); + + let platform = ""; + //alert(process.platform); + //console.log(process.platform) + //console.log(path); + + if (process.platform == "darwin") { + const platrofm = "mac"; + //TyranoStudio-darwin-x64.asar + if (path.indexOf(".asar") != -1) { + path = $.replaceAll(path, "/Contents/Resources/app.asar", ""); + } else { + path = $.replaceAll(path, "/Contents/Resources/app", ""); + } + + path = $.getExtWithFile(path).dir_name; + } else if (process.platform == "win32") { + if (path.indexOf(".asar") != -1) { + path = $.replaceAll(path, "\\resources\\app.asar", ""); + } else { + path = $.replaceAll(path, "\\resources\\app", ""); + } + } + + $.cacheExePath = path; + + return path; + }; + + //展開先のパスを返す。 + $.getUnzipPath = function () { + let path = process.__dirname; + + if (path.indexOf(".asar") != -1) { + return "asar"; + } + + return path; + }; + + $.removeStorageFile = function (key) { + try { + const fs = window.studio_api.fs; + let out_path; + if (process.execPath.indexOf("var/folders") != -1) { + out_path = process.env.HOME + "/_TyranoGameData"; + if (!fs.existsSync(out_path)) { + fs.mkdirSync(out_path); + } + } else { + out_path = $.getExePath(); + } + const file_path = out_path + "/" + key + ".sav"; + fs.unlinkSync(file_path); + } catch (e) { } + }; + + $.setStorageFile = function (key, val) { + val = JSON.stringify(val); + var fs = window.studio_api.fs; + + var out_path = $.getExePath(); + + //mac os Sierra 対応 + if (process.execPath.indexOf("var/folders") != -1) { + out_path = process.env.HOME + "/_TyranoGameData"; + if (!fs.existsSync(out_path)) { + fs.mkdirSync(out_path); + } + } else { + out_path = $.getExePath(); + } + + fs.writeFileSync(out_path + "/" + key + ".sav", escape(val)); + }; + + $.getStorageFile = function (key) { + try { + var gv = "null"; + var fs = window.studio_api.fs; + var out_path = $.getExePath(); + + if (process.execPath.indexOf("var/folders") != -1) { + out_path = process.env.HOME + "/_TyranoGameData"; + if (!fs.existsSync(out_path)) { + fs.mkdirSync(out_path); + } + } else { + out_path = $.getExePath(); + } + + if (fs.existsSync(out_path + "/" + key + ".sav")) { + var str = fs.readFileSync(out_path + "/" + key + ".sav", "utf8"); + gv = unescape(str); + } else { + //Fileが存在しない場合にローカルストレージから読み取る使用は破棄。 + //gv = unescape(localStorage.getItem(key)); + } + + if (gv == "null") { + return null; + } + } catch (e) { + console.log(e); + alert($.lang("save_does_not_work")); + $.confirmSaveClear(); + } + + return gv; + }; + + /** + * remodal のイベントをすべて消去 + */ + $.removeRemodalEvents = function (includes_closed) { + $(document).off("opening", ".remodal"); + $(document).off("opened", ".remodal"); + $(document).off("closing", ".remodal"); + $(document).off("confirmation", ".remodal"); + $(document).off("cancellation", ".remodal"); + if (includes_closed) $(document).off("closed", ".remodal"); + }; + + /** + * remodal のイベント汎用処理 + * @param {Object} options + * @param {"alert" | "confirm"} options.type + * @param {string} options.title + * @param {function} on_ok + * @param {function} on_cancel + */ + $.remodalCommon = function (options = {}) { + const j_box = $("[data-remodal-id=modal]"); + const j_ok = $(".remodal").find("#remodal-confirm"); + const j_ng = $(".remodal").find("#remodal-cancel"); + const j_wrapper = $(".remodal-wrapper"); + const j_button = $([j_ok[0], j_ng[0]]); + const j_event = $([j_ok[0], j_ng[0], j_wrapper[0], j_box[0]]); + const j_anim = $(".remodal-base").add(j_box); + + //

の更新 + $(".remodal_title").html(options.title); + + // OK 表示 + j_ok.show().focusable(); + j_ok.trigger("init"); + + // Cancel 表示 + if (options.type === "confirm") { + j_ng.show().focusable(); + j_ng.trigger("init"); + } else { + j_ng.hide(); + } + + // ポイント不可 + j_event.setStyle("pointer-events", "none"); + + // remodal 初期化 + j_box.css("font-family", TYRANO.kag.config.userFace); + const inst = j_box.remodal(); + + // 汎用クローズ処理 + const close_common = (e) => { + e.stopPropagation(); + j_event.setStyle("pointer-events", "none"); + TYRANO.kag.key_mouse.vmouse.hide(); + const effect = TYRANO.kag.tmp.remodal_closing_effect; + if (effect && effect !== "none") { + j_box.setStyleMap({ "animation-name": effect }, "webkit"); + $(document).on("closed", ".remodal", () => { + j_box.setStyleMap({ "animation-name": "" }, "webkit"); + }); + } + $.removeRemodalEvents(false); + }; + + // + // イベントリスナを設定 + // https://github.com/VodkaBears/Remodal#events + // + + // 旧イベントを消去 + $.removeRemodalEvents(true); + + let mousedown_elm = null; + + // ラッパーのクリックでウィンドウを閉じられるようにする + j_wrapper + .off("mousedown.outerclose click.outerclose") + .on("click.outerclose", (e) => { + e.stopPropagation(); + if (mousedown_elm !== j_wrapper[0]) return; + j_box.off("mousedown.outerclose"); + j_wrapper.off("mousedown.outerclose click.outerclose"); + if (options.type === "confirm") j_ng.trigger("click"); + }) + .on("mousedown.outerclose", () => { + mousedown_elm = j_wrapper[0]; + }); + + // メッセージボックスのクリックがラッパーに突き抜けないようにする + j_box.off("mousedown.outerclose").on("mousedown.outerclose", (e) => { + mousedown_elm = j_box[0]; + e.stopPropagation(); + }); + + j_button.off("click.outerclose").on("click.outerclose", () => { + j_box.off("click.outerclose"); + }); + + // 表示完了時 + $(document).on("opened", ".remodal", () => { + // ポイント可 + j_event.setStyle("pointer-events", "auto"); + }); + + // + // ボタンのクリックイベント + // + + if (options.type === "alert") { + // アラート: クローズ時の処理 + $(document).on("closed", ".remodal", (e) => { + close_common(e); + $.removeRemodalEvents(false); + if (typeof options.on_ok === "function") { + options.on_ok(); + } + }); + } + + if (options.type === "confirm") { + // コンファーム: OK 時の処理 + $(document).on("confirmation", ".remodal", (e) => { + close_common(e); + if (typeof options.on_ok === "function") { + options.on_ok(); + } + }); + + // コンファーム: Cancel 時の処理 + $(document).on("cancellation", ".remodal", (e) => { + close_common(e); + if (typeof options.on_cancel === "function") { + options.on_cancel(); + } + }); + } + + // + // オープンアニメーション + // + + if (TYRANO.kag.tmp.remodal_opening_effect_time !== undefined) { + j_anim.setStyleMap({ "animation-duration": TYRANO.kag.tmp.remodal_opening_effect_time }, "webkit"); + } + + // オープン開始時にアニメクラスを付ける, オープン完了時に外す + const opening_effect = TYRANO.kag.tmp.remodal_opening_effect; + if (opening_effect && opening_effect !== "none") { + $(document).on("opening", ".remodal", () => { + j_box.setStyleMap({ "animation-name": opening_effect }, "webkit"); + }); + $(document).on("opened", ".remodal", () => { + j_box.setStyleMap({ "animation-name": "" }, "webkit"); + if (TYRANO.kag.tmp.remodal_closing_effect_time !== undefined) { + j_anim.setStyleMap({ "animation-duration": TYRANO.kag.tmp.remodal_closing_effect_time }, "webkit"); + } + }); + } + + // + // 開く + // + + inst.open(); + }; + + /** + * モーダルウィンドウで remodal + * @param {string} title + * @param {function} on_ok + */ + $.alert = (title, on_ok) => { + $.remodalCommon({ type: "alert", title, on_ok }); + }; + + /** + * モーダルウィンドウでコンファーム, remodal + * @param {string} title + * @param {function} on_ok + * @param {function} on_cancel + */ + $.confirm = function (title, on_ok, on_cancel) { + $.remodalCommon({ type: "confirm", title, on_ok, on_cancel }); + }; + + /** + * 画面右下にトースト通知, alertify + * ゲーム画面(tyrano_base)よりも外側に出る + * @param {*} str + * @param {*} type + */ + $.inform = (str, type) => { + alertify.log(str, type); + }; + + $.prompt = function (str, cb) { + alertify.prompt(str, function (flag, text) { + if (typeof cb == "function") { + cb(flag, text); + } + }); + }; + + $.isBase64 = function (str) { + if (!str) return false; + + if (str.substr(0, 10) == "data:image") { + return true; + } else { + return false; + } + }; + + //オブジェクトの個数をもってきます。1 + $.countObj = function (obj) { + var num = 0; + for (let key in obj) { + num++; + } + return num; + }; + + $.getUrlQuery = function (url) { + var hash = url.slice(1).split("&"); + var max = hash.length; + var vars = {}; + var array = ""; + + for (var i = 0; i < max; i++) { + array = hash[i].split("="); + vars[array[0]] = array[1]; + } + + return vars; + }; + + //アトリビュートの中で保存するリストを取得する + $.makeSaveJSON = function (el, array_white_attr) { + var j_el = $($.playerHtmlPath($(el).outerHTML())); + + var root = { + tag: el.tagName, + style: j_el.attr("style"), + class: j_el.attr("class"), + text: "", + attr: {}, + children: [], + }; + + //属性を設置 + for (var k = 0; k < array_white_attr.length; k++) { + if (j_el.attr(array_white_attr[k])) { + root["attr"][array_white_attr[k]] = j_el.attr(array_white_attr[k]); + } + } + + loop(el, root); + + function loop(node, _root) { + var nodes = node.childNodes; + for (var i = 0; i < nodes.length; i++) { + //console.log(nodes[i]); + var j_node = $(nodes[i]); + + var obj = { + tag: nodes[i].tagName, + style: j_node.attr("style"), + class: j_node.attr("class"), + text: j_node.text(), + attr: {}, + children: [], + }; + + //属性を設置 + for (var k = 0; k < array_white_attr.length; k++) { + if (j_node.attr(array_white_attr[k])) { + obj["attr"][array_white_attr[k]] = j_node.attr(array_white_attr[k]); + } + } + + if (!nodes[i]) { + console.error("セーブデータ作成エラー"); + console.error(nodes[i]); + continue; + } + + if (nodes[i].childNodes.length > 0) { + loop(nodes[i], obj); + } + + _root.children.push(obj); + } + } + + return root; + }; + + //レイヤーをhtmlエレメントとして復元する + $.makeElementFromSave = function (root, array_white_attr) { + if (root.tag.toLowerCase() == "script") return false; + + var j_root = $(document.createElement(root.tag)); + + j_root.attr("style", root["style"]); + j_root.attr("class", root["class"]); + + for (var i = 0; i < array_white_attr.length; i++) { + if (typeof root["attr"][array_white_attr[i]] != "undefined") { + j_root.attr(array_white_attr[i], root["attr"][array_white_attr[i]]); + } + } + + loop(root, j_root); + + function loop(_root, _j_obj) { + var nodes = _root.children; + for (var i = 0; i < nodes.length; i++) { + if (typeof nodes[i].tag != "undefined" && nodes[i].tag.toLowerCase() == "script") { + break; + } + + if (typeof nodes[i].tag == "undefined") { + _j_obj.append(nodes[i].text); + continue; + } + + var j_node = $(document.createElement(nodes[i].tag)); + + j_node.attr("style", nodes[i]["style"]); + j_node.attr("class", nodes[i]["class"]); + + for (var k = 0; k < array_white_attr.length; k++) { + if (typeof nodes[i]["attr"][array_white_attr[k]] != "undefined") { + j_node.attr(array_white_attr[k], nodes[i]["attr"][array_white_attr[k]]); + } + } + + if (!nodes[i]) { + console.error("セーブデータ作成エラー"); + console.error(nodes[i]); + continue; + } + + if (nodes[i].children.length > 0) { + loop(nodes[i], j_node); + } + + _j_obj.append(j_node); + } + } + + return j_root; + }; + + //渡されたJqueryオブジェクトにクラスをセットします + $.setName = function (jobj, str) { + str = $.trim(str); + + if (str == "") return; + + var array = str.split(","); + for (var i = 0; i < array.length; i++) { + jobj.addClass(array[i]); + } + }; + + //フラッシュのインストール判定 + $.isFlashInstalled = function () { + if (navigator.plugins["Shockwave Flash"]) { + return true; + } + try { + new ActiveXObject("ShockwaveFlash.ShockwaveFlash"); + return true; + } catch (e) { + return false; + } + }; + + /*スマホの場合は、タッチでクリックを置き換える*/ + /*タッチ系、一応出来たけど、動作確認よくしなければならなk,問題なければR9にも適応*/ + if ($.userenv() != "pc") { + $.event.tap = function (o) { + o.bind("touchstart", onTouchStart_); + function onTouchStart_(e) { + e.preventDefault(); + o.data("event.tap.moved", false).one("touchmove", onTouchMove_).one("touchend", onTouchEnd_); + e.stopPropagation(); + } + + function onTouchMove_(e) { + //o.data('event.tap.moved', true); + e.stopPropagation(); + } + + function onTouchEnd_(e) { + if (!o.data("event.tap.moved")) { + o.unbind("touchmove", onTouchMove_); + o.trigger("click").click(); + e.stopPropagation(); + } + } + }; + + if ("ontouchend" in document) { + $.fn.tap = function (data, fn) { + //alert("tap!"); + + if (fn == null) { + fn = data; + data = null; + } + + if (arguments.length > 0) { + this.bind("tap", data, fn); + $.event.tap(this); + } else { + this.trigger("tap"); + } + return this; + }; + + if ($.attrFn) { + $.attrFn["tap"] = true; + } + + //クリック上書き + $.fn.click = $.fn.tap; + } else { + //$.fn.tap = $.fn.click; + } + } + + ////////////////////////////// + + $.error_message = function (str) { + alert(str); + }; + + //クッキー設定 + $.setCookie = function (key, val) { + document.cookie = key + "=" + escape(val) + ";expires=Fri, 31-Dec-2030 23:59:59;path=/;"; + }; + + // window.setTimeoutのラッパー関数 + // timeoutが0より大きい数値ならwindow.setTimeoutに投げて非同期実行(戻り値はtimerId:正の整数) + // そうでないならcallbackを同期実行(戻り値は0) + // ※window.setTimeout(callback, 0)は非同期実行になってしまう + $.setTimeout = function (callback, timeout) { + if (typeof timeout === "number" && timeout > 0) { + return setTimeout(callback, timeout); + } + callback(); + return 0; + }; + + /** + * @typedef {Object} EdgeOption + * @property {string} color + * @property {number} width + * @property {number} total_width + */ + /** + * 縁取りの太さと幅を指定した文字列を解析してEdgeの配列を返す + * たとえば $.parseEdgeOptions("4px rgb(255,0,0), 2px 0xFFFFFF, blue") は次の配列を返す + * [ + * {color: "rgb(255,0,0)", width: 4, total_width: 4}, + * {color: "#FFFFFF", width: 2, total_width: 6}, + * {color: "blue", width: 1, total_width: 7} + * ] + * @param {string} edge_str 縁取りの太さと幅 (例) "4px rgb(255,0,0), 2px 0xFFFFFF" + * @returns {EdgeOption[]} + */ + $.parseEdgeOptions = function (edge_str) { + // キャッシュを活用 + const cache_map = $.parseEdgeOptions.cache; + if (edge_str in cache_map) { + return cache_map[edge_str]; + } + + // 戻り値となるEdgeの配列 + const edges = []; + + // 文字列を「カッコの外にあるカンマ」で刻む + // 色指定自体にカンマが含まれるケース("rgb(255,255,255)"のような)を考慮しなければならない + const edge_str_hash = edge_str.split(/,(?![^(]*\))/); + + // 内側から加算していった合計の縁取り太さ(複数縁取りを行う場合に意味を持つ) + // filter: drop-shadow()方式では不要、text-shadow方式や-webkit-text-stroke方式では必要 + let total_width = 0; + + for (const this_edge_str of edge_str_hash) { + // 例) "6px Black" + const this_edge_str_trim = $.trim(this_edge_str); + + // 先頭の〇〇pxをチェック + const width_match = this_edge_str_trim.match(/^[0-9.]+px /); + + let width; + let width_str; + if (width_match) { + // 先頭の〇〇pxが見つかればそれを縁取りの太さとして解釈し、〇〇pxよりもあとの文字列を色解析に回す + width = parseFloat(width_match[0]); + width_str = this_edge_str_trim.substring(width_match[0].length); + } else { + // 先頭の〇〇pxが見つからなければ太さは1とし、文字列をまるごと色解析に回す + width = 1; + width_str = this_edge_str_trim; + } + + const color = $.convertColor($.trim(width_str)); + total_width += width; + if (color) { + edges.push({ color, width, total_width }); + } + } + cache_map[edge_str] = edges; + return edges; + }; + $.parseEdgeOptions.cache = {}; + + /** + * 縁取りしたいDOM要素のスタイルのfilterプロパティにセットするべき値を生成する + * @param {string} edge_str 縁取りの太さと幅 (例) "4px red, 2px white" + * @returns {string} (例) "drop-shadow(0 0 4px red) drop-shadow(0 0 red) ..." + */ + $.generateDropShadowStrokeCSS = function (edge_str) { + // 毎回計算するのは意外と重いのでキャッシュを活用 + const cache_map = $.generateDropShadowStrokeCSS.cache; + if (edge_str in cache_map) { + return cache_map[edge_str]; + } + + // "drop-shadow(...)" を格納していく配列 + const css_arr = []; + + const edges = $.parseEdgeOptions(edge_str); + for (const edge of edges) { + css_arr.push($.generateDropShadowStrokeCSSOne(edge.color, edge.width)); + } + + const css_value = css_arr.join(" "); + cache_map[edge_str] = css_value; + return css_value; + }; + $.generateDropShadowStrokeCSS.cache = {}; + + /** + * 縁取りしたいDOM要素のスタイルのfilterプロパティにセットするべき値を生成する + * @param {string} color 縁取りの色 (例) "red" + * @param {number} width 縁取りの幅 (例) 3 + * @returns {string} 例) "drop-shadow(0 0 3px red) drop-shadow(0 0 red) ..." + */ + $.generateDropShadowStrokeCSSOne = function (color = "black", width = 1) { + // drop-shadow(...)を重ねる + // 試行錯誤の結果これが良い感じと思われた + const shadow_width = (width - 1) * 0.4; + const css_array = []; + if (shadow_width > 0) { + css_array.push(`drop-shadow(0 0 ${shadow_width.toFixed(2)}px ${color})`); + for (let i = 0; i < 8; i++) { + css_array.push(`drop-shadow(0 0 ${color})`); + } + } + // 最後にうっすらとぼかした細い影を落とすことでアンチエイリアス効果を与える + // これがないと縁取りがガビガビに見えてしまう + css_array.push(`drop-shadow(0 0 0.4px ${color})`); + css_array.push(`drop-shadow(0 0 0.4px ${color})`); + css_array.push(`drop-shadow(0 0 0.2px ${color})`); + return css_array.join(" "); + }; + + if ($.getOS() === "ios" && $.getBrowser() === "safari") { + $.generateDropShadowStrokeCSSOne = function (color = "black", width = 1) { + const shadow_width = Math.max(1, parseInt(width * 0.5)); + console.warn(shadow_width); + const css_array = []; + if (shadow_width > 0) { + css_array.push(`drop-shadow(0 0 ${shadow_width}px ${color})`); + for (let i = 0; i < 8; i++) { + css_array.push(`drop-shadow(0 0 ${color})`); + } + } + return css_array.join(" "); + }; + } + + /** + * 縁取りしたいDOM要素のスタイルのtext-shadowプロパティにセットするべき値を生成する + * @param {string} edge_str 縁取りの太さと幅 (例) "4px red, 2px white" + * @returns {string} 例) "1px 1px 0px red, -1px 1px 0px red, ..." + */ + $.generateTextShadowStrokeCSS = function (edge_str) { + // 毎回計算するのは意外と重いのでキャッシュを活用 + const cache_map = $.generateTextShadowStrokeCSS.cache; + if (edge_str in cache_map) { + return cache_map[edge_str]; + } + + // "1px 1px 0px black" のような文字列を格納していく配列 + const css_arr = []; + + const edges = $.parseEdgeOptions(edge_str); + for (const edge of edges) { + css_arr.push($.generateTextShadowStrokeCSSOne(edge.color, edge.total_width)); + } + + const css_value = css_arr.join(","); + cache_map[edge_str] = css_value; + return css_value; + }; + $.generateTextShadowStrokeCSS.cache = {}; + + /** + * 縁取りしたいDOM要素のスタイルのtext-shadowプロパティにセットするべき値を生成する + * @param {string} color 縁取りの色 例) "black" + * @param {number} width 縁取りの幅 例) 3 + * @returns {string} 例) "1px 1px 0px black, -1px 1px 0px black, ..." + */ + $.generateTextShadowStrokeCSSOne = function (color = "black", width = 1) { + // 円周上の頂点を取得 + const points = $.calcTextShadowStrokePoints(width); + + // 座標の小数点以下の桁数 + const position_digits = 2; + + // text-shadowを重ねる + const css_array = []; + for (let p of points) { + const x = p.x.toFixed(position_digits); + const y = p.y.toFixed(position_digits); + const css = `${x}px ${y}px 0px ${color}`; + css_array.push(css); + } + + return css_array.join(","); + }; + + /** + * text-shadowで文字の縁取りを行うときの、陰をずらす先の頂点の座標配列を計算する + * 太い縁取りを行う場合ほど大量の頂点を生み出す必要がある + * @param {number} width 縁取りの幅 (例) 3 + * @returns {{x: number; y: number;}[]} + */ + $.calcTextShadowStrokePoints = function (width) { + // 太さが1以下の場合は固定値を返す + if (width <= 1) { + return [ + { x: 1, y: -1 }, + { x: 1, y: 1 }, + { x: -1, y: 1 }, + { x: -1, y: -1 }, + ]; + } + + // 円周の長さ + const circumference = 2 * width * Math.PI; + + // 円周をこの長さで分割する + const hash_length = 1; + + // 頂点の数の最低値 + const point_num_min = 8; + + // 頂点の数(円周の長さ÷分割する長さ) + const point_num = Math.max(point_num_min, circumference / hash_length); + + // 1周(2πラジアン=360°) + const round = 2 * Math.PI; + + // 1周をこの角度で分割する + const hash_angle = round / point_num; + + // 円周上の点の座標を格納する配列 + const points = []; + + for (let angle = 0; angle < round; angle += hash_angle) { + points.push({ + x: width * Math.cos(angle), + y: width * Math.sin(angle), + }); + } + + return points; + }; + + /** + * CSSのfilterプロパティに値をセット + * prefixを考慮 + * @param {string} str filter プロパティにセットする値 + * @param {boolean} [add_z_0=true] z-0 クラスを付けるかどうか + * @return {jQuery} + */ + $.fn.setFilterCSS = function (str, add_z_0 = true) { + // プレフィックスを考慮して filter プロパティをセット + this.setStyle("filter", str, ["webkit", "moz", "ms"]); + + // z-0 クラスを付与して transform: translateZ(0) でGPUレイヤー作成を促す + // Safari on iOS においてfilterプロパティだけではGPUレイヤーが作成されず + // filterが崩れる可能性がある + if (add_z_0) this.addClass("z-0"); + + return this; + }; + + /** + * CSSグラデーションのプリセット + */ + $.gradientPresetMap = { + dark: "linear-gradient(0deg, rgba(0,0,0,1) 0%, rgba(0,0,0,1) 41%, rgba(126,126,126,1) 100%)", + light: "linear-gradient(0deg, rgba(193,245,239,1) 0%, rgba(255,255,255,1) 34%, rgba(255,255,255,1) 100%)", + fire: "linear-gradient(0deg, rgba(255,0,0,1) 0%, rgba(255,239,0,1) 100%)", + sky: "linear-gradient(0deg, rgba(0,255,235,1) 0%, rgba(0,18,255,1) 100%)", + leaf: "linear-gradient(0deg, rgba(234,240,0,1) 0%, rgba(0,226,49,1) 100%)", + gold: "repeating-linear-gradient(0deg, #B67B03 0.1em, #DAAF08 0.2em, #FEE9A0 0.3em, #DAAF08 0.4em, #B67B03 0.5em)", + gold2: "linear-gradient(0deg, #b8751e 0%, #ffce08 37%, #fefeb2 47%, #fafad6 50%, #fefeb2 53%, #e1ce08 63%, #b8751e 100%)", + silver: "repeating-linear-gradient(0deg, #a8c7c3 0.1em, #b6c9d1 0.2em, #e7fbff 0.3em, #c7d5d6 0.4em, #a6b2b6 0.5em)", + silver2: "linear-gradient(0deg, #acb4b8 0%, #e6f8ff 37%, #e6f7f8 47%, #e2f3fd 50%, #eff9ff 53%, #d8e4e7 63%, #b5bbbd 100%)", + }; + + /** + * グラデーションテキストを設定する + * @param {string} gradient CSSのグラデーション関数文字列 linear-gradient(...) + * @return {jQuery} + */ + $.fn.setGradientText = function (gradient) { + if (this.length === 0) { + return this; + } + if (gradient in $.gradientPresetMap) { + gradient = $.gradientPresetMap[gradient]; + } + this.each(function () { + $(this) + .setStyleMap({ + "background-image": gradient, + "-webkit-background-clip": "text", + "background-clip": "text", + "color": "transparent", + }) + .addClass("gradient-text"); + }); + return this; + }; + + /** + * グラデーションテキストを復元する + * @return {jQuery} + */ + $.fn.restoreGradientText = function () { + if (this.length === 0) { + return this; + } + this.each(function () { + const j_this = $(this); + const style = j_this.attr("style"); + if (style && !style.includes("-webkit-background-clip") && style.includes("background-clip")) { + const new_style = style.replace("background-clip", "-webkit-background-clip: text; background-clip"); + j_this.attr("style", new_style); + } + }); + return this; + }; + + /** + * -webkit-text-strokeで縁取りされている可能性のある + * [ptext]のテキスト内容を書き換える + * @param {string} str 新しいテキスト + * @return {jQuery} + */ + $.fn.updatePText = function (str) { + if (this.length === 0) { + return this; + } + this.each(function () { + if (typeof this.updateText === "function") { + this.updateText(str); + } else { + $(this).html(str); + } + }); + return this; + }; + + /** + * 引数が"空でない文字列"であるかどうかを返す + * @param {any} val + * @returns {boolean} + */ + $.isNonEmptyStr = function (val) { + if (typeof val === "string" && val !== "") { + return true; + } + return false; + }; + + /** + * CSSのオブジェクトを渡してセットする + * 本家jQueryの.css()メソッドは汎用性が高い分処理が遅い + * こちらのメソッドであれば処理時間が40-50%ほどで済む + * @param {{[key: string]: string;}} map CSSのプロパティと値が対になっているオブジェクト + * @param {string | string[]} [prefixes] ベンダープレフィックス対応 (例) [ "webkit", "mz" ] + * @return {jQuery} + */ + $.fn.setStyleMap = function (map, prefixes) { + const len = this.length; + if (len === 0) { + return this; + } + if (typeof prefixes === "string") { + prefixes = [prefixes]; + } + for (let i = 0; i < len; i++) { + const elm = this[i]; + for (const plain_key in map) { + const value = map[plain_key]; + if (prefixes) { + for (const prefix of prefixes) { + const prefix_key = `-${prefix}-${plain_key}`; + elm.style.setProperty(prefix_key, value); + } + } + elm.style.setProperty(plain_key, value); + } + } + return this; + }; + + /** + * CSSのキーとバリューを渡してセットする + * 本家jQueryの.css()メソッドは汎用性が高い分処理が遅い + * こちらのメソッドであれば処理時間が40-50%ほどで済む + * @param {string} key + * @param {string} value + * @param {string | string[]} [prefixes] ベンダープレフィックス対応 (例) [ "webkit", "mz" ] + * @return {jQuery} + */ + $.fn.setStyle = function (key, value, prefixes) { + const len = this.length; + if (len === 0) { + return this; + } + if (typeof prefixes === "string") { + prefixes = [prefixes]; + } + for (let i = 0; i < len; i++) { + const elm = this[i]; + if (prefixes) { + for (const prefix of prefixes) { + const prefix_key = `-${prefix}-${key}`; + elm.style.setProperty(prefix_key, value); + } + } + elm.style.setProperty(key, value); + } + return this; + }; + + /** + * 要素に直接指定されているスタイルの値を取得する + * なにも指定されていない場合は空の文字列が帰る + * ※ .css()とは違う。.css()は.getComputedStyle()がベースになっている。 + *   .css()は、要素自体には何のスタイルも指定されていない場合でも、読み込まれているCSSを考慮して + *   最終的にどのようなスタイルが当たるのかを判断し、さらに長さのプロパティをpxに変換して返す仕様がある。 + * ※ ここで定義している.getStyle()は単純に『この要素に直接』指定されているスタイルを返す。 + * @param {string | string[]} prop + * @return {string} + */ + $.fn.getStyle = function (prop) { + if (this[0]) { + if (typeof prop === "string") { + return this[0].style.getPropertyValue(prop); + } else { + const style_map = {}; + prop.forEach((this_prop) => { + style_map[this_prop] = this[0].style.getPropertyValue(this_prop); + }); + return style_map; + } + } + return ""; + }; + + /** + * 要素が表示されていれば true を返す + * (display: none; でなければ true を返す) + * @return {boolean} + */ + $.fn.isDisplayed = function () { + if (!this[0]) return false; + return this.css("display") !== "none"; + }; + + /** + * 渡されたjQueryコレクション内のすべての要素について横幅を調査し、 + * その調査で得られたもっとも大きい横幅をすべての要素のwidthプロパティにpx単位で適用する + * box-sizingも考慮する + * @returns {jQuery} + */ + $.fn.alignMaxWidth = function () { + return this.alignMaxWidthOrHeight("width", "left", "right"); + }; + $.fn.alignMaxHeight = function () { + return this.alignMaxWidthOrHeight("height", "top", "bottom"); + }; + $.fn.alignMaxWidthOrHeight = function (_width, _left, _right) { + const len = this.length; + if (len === 0) { + return this; + } + let max_width = -1; + let j_max_elm; + this.each((i, elm) => { + const j_elm = $(elm); + // border-box にしておく → 横幅を統一的に解釈するため + // display: block にしておく → 表示状態でないと横幅が取得できないため + j_elm.setStyle("box-sizing", "border-box").show(); + const computed_style = j_elm.css([ + "box-sizing", + `padding-${_left}`, + `padding-${_right}`, + `border-${_left}-width`, + `border-${_right}-width`, + ]); + const padding_sum = + parseFloat(computed_style[`padding-${_left}`]) + + parseFloat(computed_style[`padding-${_right}`]) + + parseFloat(computed_style[`border-${_left}-width`]) + + parseFloat(computed_style[`border-${_right}-width`]); + const client_width = j_elm[_width]() + padding_sum; + if (client_width > max_width) { + max_width = client_width; + j_max_elm = j_elm; + } + }); + const width = j_max_elm.getStyle("width"); + this.setStyle(_width, `${max_width}px`); + return this; + }; + + /** + * ティラノスクリプトの[kanim]に渡されたパラメータを用いて + * 任意のDOM要素にWeb Animation APIによるキーフレームアニメーションを適用する + * @param {Object} pm + * @return {jQuery} + */ + $.fn.animateWithTyranoKeyframes = function (pm) { + const len = this.length; + if (len === 0) { + return this; + } + const keyframes = TYRANO.kag.parseKeyframesForWebAnimationAPI(pm.keyframe); + if (!keyframes) { + return this; + } + for (let i = 0; i < len; i++) { + const anim = this[i].animate(keyframes, { + delay: parseInt(pm.delay) || 0, + direction: pm.direction || "normal", + duration: parseInt(pm.time) || 1000, + easing: pm.easing || "linear", + iterations: pm.count === "infinite" ? Infinity : parseInt(pm.count) || Infinity, + fill: pm.mode || "forwards", + }); + anim.onfinish = () => { + if (pm.onend) { + pm.onend(anim); + } + }; + } + return this; + }; + + /** + * dataフォルダに入っていることが想定されるフォルダ名("scenario", "image"など)を格納した配列 + */ + const data_folder_names = ["scenario", "image", "fgimage", "bgimage", "video", "sound", "bgm", "others"]; + + /** + * タグのstorageパラメータに指定された値をフルパスに直す + * - "http" から始まる場合はそのまま返す。 + * - そうでない場合、戻り値が "./" で始まることを保証する。 + * - 同等のパスが一意に定まるパスで表されることを保証する。 + * - そのために "../" を除去する。 + * - "../" を許可してしまうと、同等のパスを無限のパターンで表せてしまうため、 + * たとえばパスをキーにした連想配列でキャッシュ管理をしている場合に + * キャッシュが機能しないケースが出てきてしまう。 + * @param {string} storage "foo.png" + * @param {string} folder "image" + * @returns {string} + * @example + * $.parseStorage("foo.png", "image"); + * // "./data/image/foo.png" + * $.parseStorage("https://tyrano.jp/foo.png", "image"); + * // "https://tyrano.jp/foo.png" + * $.parseStorage("nextpage.gif", "tyrano/images/system"); + * // "./tyrano/images/system/nextpage.gif" + * $.parseStorage("foo.png", "data/image"); + * // "./data/image/foo.png" + * $.parseStorage("../fgimage/foo.png", "image"); + * // "./data/fgimage/foo.png" + */ + $.parseStorage = function (storage, folder = "") { + if (!storage) return ""; + + // "http"で始まっているならそのまま返す + if ($.isHTTP(storage)) { + return storage; + } + + // フォルダパスを特定 + if (folder && data_folder_names.includes(folder.split("/").shift())) { + // dataフォルダに入っているフォルダ名が指定されている場合は自動的に"data/"を足す + // たとえば"scenario"を"data/scenario"に変換する + folder = `data/${folder}`; + } + + // / フォルダパス / ファイル名 + let full_path = `/${folder}/${storage}`; + + // "//" や "/./" は "/" に直す + full_path = full_path.replace(/\/\.?\/+/g, "/"); + + const path_hash = []; + + full_path.split("/").forEach((item) => { + if (item === "" || item === ".") { + return; + } + if (item === "..") { + path_hash.pop(); + return; + } + path_hash.push(item); + }); + + full_path = "./" + path_hash.join("/"); + + return full_path; + }; + + /** + * "300", "0.3s", "300ms" などでありうる文字列を + * animation-duration にセットできる値に変換する + * @param {string} str + * @returns + */ + $.convertDuration = function (str, default_value = "0s") { + if (typeof str !== "string" || str === "") { + return default_value; + } + if (str.includes("s")) { + return str; + } + return str + "ms"; + }; + + /** + * スネークケース(ハイフン区切り)のCSSのプロパティ名を + * キャメルケースに変換して返す + * @param {string} str + * @returns {string} + * @example + * $.parseCamelCaseCSS("-webkit-text-stroke"); + * // "webkitTextStroke" + */ + $.parseCamelCaseCSS = function (str) { + if (typeof str !== "string") { + return ""; + } + // 先頭のハイフンはただ消去するだけでいい + if (str.charAt(0) === "-") { + str = str.substring(1); + } + // ハイフン+なんらかの小文字アルファベットのマッチ + const match = str.match(/-[a-z]/); + // マッチしなくなったら完成 + if (!match) { + return str; + } + // マッチし続ける限り再帰する + return $.parseCamelCaseCSS(str.replace(match[0], match[0].charAt(1).toUpperCase())); + }; + + $.findAnimTargets = function (pm = {}) { + // アニメーション対象 + let j_target = null; + + if (pm.name) { + // nameパラメータが指定されている場合 + j_target = $("." + pm.name); + } else if (pm.layer) { + // nameパラメータは指定されていないがlayerパラメータが指定されている場合 + // 対象レイヤのクラス名を取得 (例) "layer_free", "0_fore", "1_fore" + const layer_name = pm.layer == "free" ? "layer_free" : pm.layer + "_fore"; + // レイヤ内の子要素をすべて対象に取る + j_target = $("." + layer_name).children(); + } + + return j_target || $(); + }; + + /** + * volumeパラメータの値を実際にhowler.jsで利用可能な値に直す + * "0"~"100" の文字列を 0.0~1.0 の数値に変換する + * @param {string} vol_str parseInt()で数値に変換可能な文字列 (例) "0", "50", "100" + * @returns {number} 0以上1以下の数値 + */ + $.parseVolume = function (vol_str) { + const vol_int = typeof vol_str === "string" ? parseInt(vol_str) : vol_str; + if (isNaN(vol_int)) { + return 1; + } + return Math.max(0, Math.min(1, vol_str / 100)); + }; + + /** + * フォーカス可能にする + * @param {number} tabindex + * @return {jQuery} + */ + $.fn.focusable = function (tabindex = 0) { + TYRANO.kag.makeFocusable(this, tabindex); + return this; + }; + + /** + * フォーカス不可能にする + * @param {number} tabindex + * @return {jQuery} + */ + $.fn.unfocusable = function (tabindex = 0) { + TYRANO.kag.makeUnfocusable(this, tabindex); + return this; + }; + + /** + * rule のセレクタが :hover や :active であるなら + * セレクタを .hover や .active に書き変えたものを複製して stylesheet に追加する + * https://developer.mozilla.org/ja/docs/Web/API/CSSStyleSheet + * @param {CSSRule} rule + * @param {CSSStyleSheet} stylesheet + */ + $.copyHoverRuleToFocusRule = (rule, stylesheet) => { + if (rule.selectorText) { + const new_selector_texts = []; + const hash = rule.selectorText.split(","); + for (const selector of hash) { + if (selector.includes(":hover")) { + new_selector_texts.push(selector.replace(":hover", ".hover")); + new_selector_texts.push(selector.replace(":hover", ".focus")); + } + if (selector.includes(":active")) { + new_selector_texts.push(selector.replace(":active", ".active")); + } + } + if (new_selector_texts.length) { + const selector_text = new_selector_texts.join(","); + const bracket_index = rule.cssText.indexOf("{"); + const style_text = rule.cssText.substring(bracket_index); + const css_text = selector_text + style_text; + stylesheet.insertRule(css_text, stylesheet.cssRules.length); + } + } + }; + + /** + * ボタンのホバー時の CSS をキーボードによるフォーカス時や仮想マウスカーソルによるホバー時にも適用するために、 + * 渡された