<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Mobile Ready TODO List v3.7 + Ext Save</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Sortable/1.15.0/Sortable.min.js"></script>
<style>
/* --- 既存スタイル (基本維持) --- */
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background-color: #f0f2f5;
display: flex;
justify-content: center;
padding: 20px 10px;
margin: 0;
-webkit-tap-highlight-color: transparent;
overflow: hidden;
touch-action: manipulation;
}
button, input, textarea { touch-action: manipulation; }
.container {
width: 100%;
max-width: 500px;
background: #fff;
padding: 20px;
border-radius: 12px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
position: relative;
z-index: 10;
height: 85vh;
display: flex;
flex-direction: column;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
flex-shrink: 0;
}
.header-left { display: flex; align-items: center; gap: 10px; }
h2 { margin: 0; color: #333; font-size: 1.5rem; }
.header-btn {
border: none;
padding: 6px 12px;
border-radius: 6px;
font-size: 16px;
cursor: pointer;
color: #333;
transition: opacity 0.2s;
display: flex;
align-items: center;
justify-content: center;
}
.header-btn:active { opacity: 0.8; }
#edit-toggle-btn { background-color: #6c757d; color: white; font-size: 14px; }
#edit-toggle-btn.active { background-color: #28a745; }
#random-btn { background-color: #ffc107; }
#history-btn { background-color: #6f42c1; color: white; }
#action-btn { background-color: #e83e8c; color: white; display: none; }
.input-group {
display: flex; margin-bottom: 20px; box-shadow: 0 2px 5px rgba(0,0,0,0.05);
border-radius: 8px; overflow: hidden; flex-shrink: 0;
}
#task-input {
flex-grow: 1; padding: 12px 15px; border: 1px solid #ddd; border-right: none;
outline: none; font-size: 16px; border-radius: 8px 0 0 8px;
}
#add-btn {
padding: 0 20px; background-color: #007bff; color: white; border: none;
cursor: pointer; font-weight: bold; font-size: 16px; border-radius: 0 8px 8px 0; min-width: 60px;
}
.todo-list { list-style: none; padding: 0; margin: 0; flex-grow: 1; overflow-y: auto; }
.todo-item {
background-color: #fff; border-bottom: 1px solid #eee; padding: 8px 10px;
display: flex; justify-content: space-between; align-items: center;
user-select: none; touch-action: pan-y; min-height: 30px;
}
.todo-text { flex-grow: 1; font-size: 16px; padding-right: 10px; word-break: break-all; line-height: 1.3; }
.edit-input {
flex-grow: 1; font-size: 16px; padding: 4px 8px; margin-right: 10px;
border: 2px solid #17a2b8; border-radius: 4px; outline: none; width: 100px;
}
.handle, .delete-btn, .modify-btn { display: none; }
.edit-mode .handle { display: inline-block; color: #ccc; margin-right: 10px; cursor: grab; font-size: 20px; padding: 0 5px; }
.btn-group { display: flex; gap: 5px; align-items: center; }
.edit-mode .modify-btn, .edit-mode .delete-btn {
display: block; color: white; border: none; padding: 4px 8px;
border-radius: 4px; cursor: pointer; font-size: 12px; flex-shrink: 0;
}
.edit-mode .modify-btn { background-color: #17a2b8; }
.edit-mode .modify-btn.saving { background-color: #007bff; }
.edit-mode .delete-btn { background-color: #ff4d4d; }
/* モーダル共通 */
.modal {
display: none; position: fixed; z-index: 2000; left: 0; top: 0; width: 100%; height: 100%;
background-color: rgba(0,0,0,0.5); justify-content: center; align-items: center; backdrop-filter: blur(2px);
}
.modal-content {
background-color: #fff; padding: 20px; border-radius: 12px; width: 90%;
max-width: 450px; max-height: 90vh; display: flex; flex-direction: column;
box-shadow: 0 5px 20px rgba(0,0,0,0.25); animation: popIn 0.2s ease-out;
overflow-y: auto;
}
@keyframes popIn { from { transform: scale(0.9); opacity: 0; } to { transform: scale(1); opacity: 1; } }
.modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; border-bottom: 1px solid #eee; padding-bottom: 10px; }
.modal-header h3 { margin: 0; font-size: 1.2rem; color: #333; }
.close-btn { background: none; border: none; font-size: 24px; cursor: pointer; color: #666; padding: 0 5px; }
#help-modal { z-index: 3100; }
/* 履歴リスト */
#history-list { list-style: none; padding: 0; margin: 0; overflow-y: auto; flex-grow: 1; }
.history-item { padding: 8px 0; border-bottom: 1px solid #f0f0f0; font-size: 14px; }
.history-header { display: flex; justify-content: space-between; margin-bottom: 2px; }
.history-date { font-size: 11px; color: #888; }
.history-symbol { font-weight: bold; color: #6f42c1; margin-left: 10px; }
.history-text { color: #333; font-weight: 500; word-break: break-all; margin-bottom: 2px; }
.history-memo { font-size: 12px; color: #666; background: #f8f9fa; padding: 2px 6px; border-radius: 4px; display: inline-block; }
/* 削除確認 */
.confirm-task-preview { font-weight: bold; color: #007bff; display: block; margin-bottom: 15px; background: #f8f9fa; padding: 10px; border-radius: 6px; word-break: break-all; text-align: center; }
.assessment-group { display: flex; justify-content: center; gap: 15px; margin-bottom: 15px; }
.assess-btn { width: 50px; height: 50px; border-radius: 50%; border: 2px solid #ddd; background: #fff; font-size: 20px; cursor: pointer; display: flex; align-items: center; justify-content: center; color: #ccc; }
.assess-btn.selected { border-color: #6f42c1; color: #6f42c1; background-color: #f3eefc; transform: scale(1.1); font-weight: bold; }
#confirm-memo-input { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; margin-bottom: 20px; box-sizing: border-box; }
.confirm-main-buttons { display: flex; gap: 10px; margin-bottom: 10px; }
.confirm-btn { padding: 12px; border: none; border-radius: 8px; font-size: 14px; font-weight: bold; cursor: pointer; flex: 1; }
#btn-record-delete { background-color: #6f42c1; color: white; }
#btn-just-delete { background-color: #ff4d4d; color: white; }
#btn-cancel-delete { background-color: #e9ecef; color: #333; width: 100%; }
/* --- アクションエディタ用スタイル --- */
#action-editor-overlay {
display: none;
position: fixed;
top: 0; left: 0; width: 100%; height: 100%;
background: #f8f9fa;
z-index: 3000;
flex-direction: column;
}
.editor-section {
border-bottom: 2px solid #ddd;
padding: 10px;
box-sizing: border-box;
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
}
.editor-label {
font-size: 12px; color: #666; font-weight: bold; margin-bottom: 5px;
background: rgba(255,255,255,0.9); padding: 2px 5px; border-radius: 4px;
position: absolute; top: 5px; left: 5px; z-index: 5;
}
.char-view-section {
height: 35%; display: flex; flex-direction: column;
}
#char-view-container {
display: flex; align-items: center; justify-content: center;
flex-grow: 1; background: #e9ecef; border-radius: 8px; position: relative; gap: 20px;
}
/* 左側の列(ヘルプと挿入ボタン) */
.char-left-col {
display: flex; flex-direction: column; gap: 15px; align-items: center;
}
.char-center-col {
display: flex; flex-direction: column; align-items: center; justify-content: center;
}
#preview-img { width: auto; height: 96px; image-rendering: pixelated; }
#char-filename {
font-size: 12px; color: #333; margin-top: 5px;
background: #fff; padding: 2px 6px; border-radius: 4px; font-family: monospace;
}
.char-right-col { display: flex; flex-direction: column; gap: 15px; }
.circle-btn {
width: 45px; height: 45px; border-radius: 50%; border: 1px solid #999;
background: white; font-size: 20px; cursor: pointer;
display: flex; align-items: center; justify-content: center;
box-shadow: 0 2px 5px rgba(0,0,0,0.1); user-select: none;
}
.circle-btn:active { background: #ddd; }
.insert-btn { background: #fd7e14; color: white; border: none; font-weight: bold; }
.help-btn { background: #17a2b8; color: white; border: none; font-weight: bold; font-size: 18px; }
.play-btn { background: #28a745; color: white; border: none; font-weight: bold; font-size: 18px; padding-left: 5px; } /* Play Icon centering */
.delete-act-btn { background: #dc3545; color: white; border: none; font-weight: bold; font-size: 16px; }
.editor-row-container {
display: flex; flex-direction: row; flex: 1; overflow: hidden; width: 100%;
}
.editor-split-pane {
flex: 1; width: 50%; min-width: 0;
border-right: 2px solid #ddd;
display: flex; flex-direction: column;
position: relative;
}
.editor-split-pane:last-child { border-right: none; padding: 10px; }
/* 左パネル内部の分割 */
.sub-pane-top {
flex: 2; /* 3分の2 */
display: flex; flex-direction: column;
overflow: hidden; border-bottom: 2px solid #ddd;
padding: 10px; position: relative;
}
.sub-pane-bottom {
flex: 1; /* 3分の1 */
display: flex; flex-direction: column;
justify-content: center; align-items: center;
padding: 10px; position: relative; background: #fafafa;
}
.action-header-row { display: flex; gap: 5px; margin-bottom: 5px; margin-top: 20px; }
#act-name-input { flex: 1; padding: 5px; font-size: 14px; border: 1px solid #ccc; border-radius: 4px; min-width: 0; }
.mini-circle-btn {
width: 30px; height: 30px; font-size: 14px;
border-radius: 50%; border: 1px solid #999; background: white; cursor: pointer; flex-shrink: 0;
}
#act-content-area {
flex: 1; resize: none; border: 1px solid #ccc; border-radius: 4px;
padding: 5px; font-family: monospace; font-size: 12px; white-space: pre; overflow-x: auto;
}
.action-footer {
display: flex; align-items: center; justify-content: center; gap: 10px; margin-top: 5px;
}
/* 定型入力エリアのスタイル */
#tmpl-display {
font-family: monospace; font-size: 16px; margin-bottom: 10px; font-weight: bold;
background: #fff; padding: 5px 10px; border-radius: 4px; border: 1px solid #ccc;
width: 80%; text-align: center;
}
.tmpl-controls {
display: flex; gap: 15px; align-items: center;
}
#loop-script-area {
flex: 1; resize: none; border: 1px solid #ccc; border-radius: 4px;
padding: 5px; font-family: monospace; font-size: 11px; white-space: pre;
margin-top: 20px; background: #2d2d2d; color: #50fa7b; overflow-x: auto;
}
.editor-footer { display: flex; gap: 5px; margin-top: 5px; flex-wrap: wrap; }
.editor-cmd-btn {
padding: 6px 8px; border: none; border-radius: 4px;
cursor: pointer; font-weight: bold; font-size: 11px; flex: 1; white-space: nowrap;
}
#btn-save-script { background: #28a745; color: white; }
#btn-close-editor { background: #6c757d; color: white; }
#btn-external-menu { background: #6f42c1; color: white; } /* 外部ボタン */
.help-content-text { font-size: 13px; color: #444; line-height: 1.6; }
.help-code {
background: #eee; padding: 4px; border-radius: 4px; font-family: monospace;
display: block; margin: 5px 0; font-size: 12px;
}
h4 { margin: 10px 0 5px 0; color: #007bff; border-bottom: 1px solid #ddd; padding-bottom: 2px;}
#neko-container {
position: fixed; z-index: 100; pointer-events: auto;
transition: top 0.1s linear, left 0.1s linear;
}
#neko-img { width: 100%; height: auto; display: block; image-rendering: pixelated; }
/* ミニプレビューオーバーレイ */
#mini-preview-overlay {
display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.6); z-index: 4000;
justify-content: center; align-items: center;
}
.mini-preview-box {
background: #fff; padding: 20px; border-radius: 12px;
display: flex; flex-direction: column; align-items: center; gap: 10px;
box-shadow: 0 5px 20px rgba(0,0,0,0.4);
animation: popIn 0.1s;
}
#mini-preview-img {
width: auto; height: 128px; image-rendering: pixelated;
}
.close-mini-btn {
padding: 5px 20px; background: #666; color: white; border: none;
border-radius: 4px; cursor: pointer; font-size: 14px;
}
</style>
</head>
<body>
<div class="container" id="main-container">
<div class="header">
<div class="header-left">
<h2>My Tasks</h2>
<button id="random-btn" class="header-btn" title="ランダム">🎲</button>
<button id="history-btn" class="header-btn" title="履歴">📖</button>
<button id="action-btn" class="header-btn" title="動作設定">⚙️ 動作</button>
</div>
<button id="edit-toggle-btn" class="header-btn">編集</button>
</div>
<div class="input-group">
<input type="text" id="task-input" placeholder="タスクを入力..." autocomplete="off">
<button id="add-btn">追加</button>
</div>
<ul id="todo-list" class="todo-list"></ul>
</div>
<div id="history-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>完了・削除履歴</h3>
<button class="close-btn" id="close-history">×</button>
</div>
<ul id="history-list"></ul>
</div>
</div>
<div id="confirm-modal" class="modal">
<div class="modal-content" style="max-width: 380px;">
<div class="modal-header" style="border:none; padding-bottom:0; margin-bottom:10px;">
<h3 style="font-size:1rem; color:#666;">タスクを削除・記録</h3>
</div>
<span id="confirm-task-name" class="confirm-task-preview"></span>
<div class="assessment-group">
<button class="assess-btn" data-val="○">○</button>
<button class="assess-btn" data-val="△">△</button>
<button class="assess-btn" data-val="✕">✕</button>
</div>
<input type="text" id="confirm-memo-input" placeholder="ひとことメモ(任意)" autocomplete="off">
<div class="confirm-actions">
<div class="confirm-main-buttons">
<button id="btn-record-delete" class="confirm-btn">📖 記録して削除</button>
<button id="btn-just-delete" class="confirm-btn">🗑️ 記録せず削除</button>
</div>
<button id="btn-cancel-delete" class="confirm-btn">キャンセル</button>
</div>
</div>
</div>
<div id="help-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>スクリプトの書き方</h3>
<button class="close-btn" id="close-help">×</button>
</div>
<div class="help-content-text">
<h4>アクション定義 (左側)</h4>
画像IDと表示時間(1/100秒)を記述します。<br>
<span class="help-code">stop1 20<br>stop2 20</span>
上の例:stop1を0.2秒、stop2を0.2秒表示。
<h4>メインループ (右側)</h4>
猫の行動ロジックを記述します。<br><br>
<b>コマンド</b><br>
<span class="help-code">neko-[アクション名] [秒数]</span>
指定したアクションを秒数分繰り返す。<br>
<span class="help-code">GOTO [ラベル]</span>
指定したラベル(:LABEL名)へジャンプ。<br>
<span class="help-code">RANDOM [数]</span>
1~[数]の乱数を変数Rに入れる。<br>
<span class="help-code">TC</span>
タップフラグ(T)を0にリセット。<br><br>
<b>移動の定義 (重要)</b><br>
ラベル名が <b>MOVE</b> で始まる時だけ、そのラベルに入った瞬間に目的地が決定し移動します。<br>
<span class="help-code">:MOVE_RUN<br>neko-run 4</span>
例: このラベルに入ると目的地が決定し、neko-runアクションを使って4秒かけてそこへ移動します。<br><br>
<b>条件分岐</b><br>
<span class="help-code">R=1 AND S>0<br>...処理...</span>
条件が真なら次の行へ。偽なら次の条件/ラベルまでスキップ。<br><br>
<b>変数</b><br>
R (乱数), S (タスク数), M (モード:1=通常/2=編集/3=履歴), T (タップフラグ:0/1)
<div style="margin-top:5px; border-left:3px solid #ccc; padding-left:8px; color:#555;"><br>
<b>※重要:</b> 画面をタップしても即座にアクションは中断されず、フラグ(T=1)が立ちます。<br>
スクリプトの条件判定で T=1 を検知し、反応させたい処理を行った後にコマンド <b>TC</b> でフラグを0に戻してください。
</div>
</div>
</div>
</div>
<div id="external-modal" class="modal" style="z-index: 5000;">
<div class="modal-content" style="max-width: 300px; text-align: center;">
<div class="modal-header" style="justify-content: center; border: none;">
<h3>外部データ操作</h3>
</div>
<p style="font-size: 13px; color: #666; margin-bottom: 20px;">
サーバー上のファイル(todo-action-data.txt)と<br>同期します。
</p>
<div style="display: flex; flex-direction: column; gap: 10px;">
<button id="btn-ext-save" class="confirm-btn" style="background-color: #dc3545; color: white;">外部 SAVE (上書き)</button>
<button id="btn-ext-load" class="confirm-btn" style="background-color: #28a745; color: white;">外部 LOAD (読込)</button>
<button id="btn-ext-cancel" class="confirm-btn" style="background-color: #6c757d; color: white; margin-top: 10px;">キャンセル</button>
</div>
</div>
</div>
<div id="mini-preview-overlay">
<div class="mini-preview-box">
<div style="font-weight:bold; color:#555; font-size:12px;">Action Preview</div>
<img id="mini-preview-img" src="">
<button id="close-mini-preview" class="close-mini-btn">閉じる</button>
</div>
</div>
<div id="action-editor-overlay">
<div class="editor-section char-view-section">
<span class="editor-label">キャラクタ表示</span>
<div id="char-view-container">
<div class="char-left-col">
<button class="circle-btn help-btn" id="btn-show-help" title="ヘルプ">?</button>
<button class="circle-btn insert-btn" id="btn-insert-char" title="アクション定義へ挿入">↓</button>
</div>
<div class="char-center-col">
<img id="preview-img" src="./pic3/neko-rstop4.png" alt="preview">
<span id="char-filename">neko-rstop4.png</span>
</div>
<div class="char-right-col">
<button class="circle-btn" id="btn-char-up">▲</button>
<button class="circle-btn" id="btn-char-down">▼</button>
</div>
</div>
</div>
<div class="editor-row-container">
<div class="editor-split-pane">
<div class="sub-pane-top">
<span class="editor-label">アクション定義</span>
<div class="action-header-row">
<input type="text" id="act-name-input" placeholder="name">
<button class="mini-circle-btn" id="btn-act-prev">◀</button>
<button class="mini-circle-btn" id="btn-act-next">▶</button>
</div>
<textarea id="act-content-area" placeholder="例:
stop1 10
stop2 10"></textarea>
<div class="action-footer">
<button class="circle-btn delete-act-btn" id="btn-delete-act" title="アクション削除">🗑️</button>
<button class="circle-btn play-btn" id="btn-play-act" title="アクション再生">▶</button>
<button class="circle-btn insert-btn" id="btn-insert-act" title="メインループへ挿入">→</button>
</div>
</div>
<div class="sub-pane-bottom">
<span class="editor-label">定型入力</span>
<div id="tmpl-display">:</div>
<div class="tmpl-controls">
<button class="mini-circle-btn" id="btn-tmpl-prev">◀</button>
<button class="mini-circle-btn" id="btn-tmpl-next">▶</button>
<button class="circle-btn insert-btn" id="btn-tmpl-insert">→</button>
</div>
</div>
</div>
<div class="editor-split-pane">
<span class="editor-label">メインループ</span>
<textarea id="loop-script-area" spellcheck="false"></textarea>
<div class="editor-footer">
<button id="btn-external-menu" class="editor-cmd-btn">外部</button>
<button id="btn-save-script" class="editor-cmd-btn">保存&適用</button>
<button id="btn-close-editor" class="editor-cmd-btn">閉じる</button>
</div>
</div>
</div>
</div>
<div id="neko-container">
<img id="neko-img" src="./pic3/neko-rstop4.png" alt="neko">
</div>
<script>
// --- 既存のTODOアプリロジック ---
const input = document.getElementById('task-input');
const addBtn = document.getElementById('add-btn');
const list = document.getElementById('todo-list');
const editToggleBtn = document.getElementById('edit-toggle-btn');
const randomBtn = document.getElementById('random-btn');
const historyBtn = document.getElementById('history-btn');
const actionBtn = document.getElementById('action-btn');
const container = document.getElementById('main-container');
const historyModal = document.getElementById('history-modal');
const closeHistoryBtn = document.getElementById('close-history');
const historyList = document.getElementById('history-list');
const confirmModal = document.getElementById('confirm-modal');
const confirmTaskName = document.getElementById('confirm-task-name');
const confirmMemoInput = document.getElementById('confirm-memo-input');
const assessBtns = document.querySelectorAll('.assess-btn');
const btnRecordDelete = document.getElementById('btn-record-delete');
const btnJustDelete = document.getElementById('btn-just-delete');
const btnCancelDelete = document.getElementById('btn-cancel-delete');
let pendingDeleteTarget = null;
let selectedSymbol = '';
let appMode = 1;
function getCurrentTime() {
const now = new Date();
return `${now.getFullYear()}/${(now.getMonth()+1).toString().padStart(2,'0')}/${now.getDate().toString().padStart(2,'0')} ${now.getHours().toString().padStart(2,'0')}:${now.getMinutes().toString().padStart(2,'0')}`;
}
function saveToHistory(text, symbol, memo) {
const historyData = JSON.parse(localStorage.getItem('myTodoHistory') || '[]');
historyData.unshift({ text, date: getCurrentTime(), symbol: symbol||'', memo: memo||'' });
localStorage.setItem('myTodoHistory', JSON.stringify(historyData));
}
function renderHistory() {
const historyData = JSON.parse(localStorage.getItem('myTodoHistory') || '[]');
historyList.innerHTML = historyData.length ? '' : '<li style="text-align:center;padding:20px;color:#999">履歴なし</li>';
historyData.forEach(item => {
const li = document.createElement('li');
li.className = 'history-item';
const sym = item.symbol ? `<span class="history-symbol">${item.symbol}</span>` : '';
const mem = item.memo ? `<br><span class="history-memo">${item.memo}</span>` : '';
li.innerHTML = `<div class="history-header"><span class="history-date">${item.date}</span>${sym}</div><div class="history-text">${item.text}</div>${mem}`;
historyList.appendChild(li);
});
}
historyBtn.addEventListener('click', () => { appMode = 3; renderHistory(); historyModal.style.display = 'flex'; });
closeHistoryBtn.addEventListener('click', () => { appMode = 1; historyModal.style.display = 'none'; });
assessBtns.forEach(btn => btn.addEventListener('click', () => {
assessBtns.forEach(b => b.classList.remove('selected'));
btn.classList.add('selected');
selectedSymbol = btn.dataset.val;
}));
function executeDelete(record) {
if(!pendingDeleteTarget) return;
if(record) saveToHistory(confirmTaskName.textContent, selectedSymbol, confirmMemoInput.value);
pendingDeleteTarget.remove();
saveTodos();
confirmModal.style.display = 'none';
pendingDeleteTarget = null;
}
btnRecordDelete.addEventListener('click', () => executeDelete(true));
btnJustDelete.addEventListener('click', () => executeDelete(false));
btnCancelDelete.addEventListener('click', () => { confirmModal.style.display='none'; pendingDeleteTarget=null; });
function saveTodos() {
const todos = [];
document.querySelectorAll('.todo-item').forEach(li => {
const val = li.querySelector('.edit-input')?.value || li.querySelector('.todo-text').textContent;
todos.push(val);
});
localStorage.setItem('myTodoList', JSON.stringify(todos));
}
function loadTodos() {
const saved = localStorage.getItem('myTodoList');
const todos = saved ? JSON.parse(saved) : ['タスク1', 'タスク2'];
list.innerHTML = '';
todos.forEach(t => addTodo(t, false));
}
let sortable = new Sortable(list, { animation: 150, handle: '.handle', disabled: true, onEnd: saveTodos });
editToggleBtn.addEventListener('click', () => {
const isEdit = container.classList.toggle('edit-mode');
appMode = isEdit ? 2 : 1;
if (isEdit) {
editToggleBtn.textContent = '完了';
editToggleBtn.classList.add('active');
sortable.option('disabled', false);
randomBtn.style.display = 'none';
historyBtn.style.display = 'none';
actionBtn.style.display = 'flex';
} else {
document.querySelectorAll('.modify-btn.saving').forEach(b => b.click());
editToggleBtn.textContent = '編集';
editToggleBtn.classList.remove('active');
sortable.option('disabled', true);
randomBtn.style.display = 'flex';
historyBtn.style.display = 'flex';
actionBtn.style.display = 'none';
}
});
addBtn.addEventListener('click', () => { if(input.value.trim()){ addTodo(input.value.trim()); input.value=''; } });
input.addEventListener('keypress', (e) => { if(e.key==='Enter') addBtn.click(); });
randomBtn.addEventListener('click', () => {
const items = Array.from(list.children);
if(items.length < 2) return;
for(let i=items.length-1; i>0; i--){
const j=Math.floor(Math.random()*(i+1));
list.appendChild(items[j]);
}
saveTodos();
});
function addTodo(text, save=true) {
const li = document.createElement('li');
li.className = 'todo-item';
li.innerHTML = `
<span class="handle">≡</span><span class="todo-text">${text}</span>
<div class="btn-group"><button class="modify-btn">修正</button><button class="delete-btn">削除</button></div>`;
const textSpan = li.querySelector('.todo-text');
const modBtn = li.querySelector('.modify-btn');
modBtn.addEventListener('click', () => {
if(modBtn.classList.contains('saving')) {
const inp = li.querySelector('.edit-input');
textSpan.textContent = inp.value;
li.replaceChild(textSpan, inp);
modBtn.textContent = '修正';
modBtn.classList.remove('saving');
saveTodos();
} else {
const inp = document.createElement('input');
inp.className = 'edit-input';
inp.value = textSpan.textContent;
li.replaceChild(inp, textSpan);
modBtn.textContent = '保存';
modBtn.classList.add('saving');
}
});
li.querySelector('.delete-btn').addEventListener('click', (e) => {
e.stopPropagation();
pendingDeleteTarget = li;
confirmTaskName.textContent = li.querySelector('.edit-input')?.value || textSpan.textContent;
confirmMemoInput.value = '';
assessBtns.forEach(b => b.classList.remove('selected'));
selectedSymbol='';
confirmModal.style.display = 'flex';
});
list.appendChild(li);
if(save) saveTodos();
}
loadTodos();
/* =================================================================
★★★ Neko Script Engine & Editor Logic ★★★
================================================================= */
const NEKO_PATH = './pic3/';
const DEFAULT_ACTIONS = {
"stop": "stop1 20\nstop2 20\nstop3 20\nstop4 400",
"run": "run1 10\nrun2 10\nrun3 10\nrun4 10",
"sit": "sit1 30\nsit2 30",
"sleep": "sleep1 50\nsleep2 50",
"jump": "jump1 10\njump2 10"
};
const DEFAULT_SCRIPT = `
:MAIN
RANDOM 3
R=1
neko-stop 5
GOTO MOVE_R
R=2
neko-sit 4
GOTO MAIN
R=3
neko-sleep 5
GOTO MAIN
:MOVE_R
neko-run 4
GOTO CHECK
:CHECK
R=1 AND T=1
neko-jump 2
TC
GOTO MAIN
S>0
neko-run 2
GOTO MAIN
GOTO MAIN
`.trim();
let actions = JSON.parse(localStorage.getItem('neko_actions')) || JSON.parse(JSON.stringify(DEFAULT_ACTIONS));
let mainScript = localStorage.getItem('neko_script') || DEFAULT_SCRIPT;
let nekoSprites = [];
const editorOverlay = document.getElementById('action-editor-overlay');
const previewImg = document.getElementById('preview-img');
const charFilename = document.getElementById('char-filename');
const actNameInput = document.getElementById('act-name-input');
const actContentArea = document.getElementById('act-content-area');
const loopScriptArea = document.getElementById('loop-script-area');
const helpModal = document.getElementById('help-modal');
// 定型入力用変数
const tmplList = [':', 'RANDOM', 'R=', 'GOTO', 'GOTO MAIN', '=', '<', '>'];
let tmplIdx = 0;
const tmplDisplay = document.getElementById('tmpl-display');
let spriteIdx = 0;
async function loadSpriteList() {
try {
const response = await fetch(`${NEKO_PATH}list.txt`);
const text = await response.text();
const lines = text.split(/\r?\n/);
const uniqueSprites = new Set();
lines.forEach(line => {
const l = line.trim();
const match = l.match(/^neko-[lr](.+)\.png$/);
if (match) { uniqueSprites.add(match[1]); }
});
if (uniqueSprites.size > 0) {
nekoSprites = Array.from(uniqueSprites).sort();
spriteIdx = 0;
updatePreview();
}
} catch (e) {
console.error(e);
nekoSprites = ['stop1', 'stop2', 'run1', 'run2'];
updatePreview();
}
}
loadSpriteList();
actionBtn.addEventListener('click', () => {
editorOverlay.style.display = 'flex';
actions = JSON.parse(localStorage.getItem('neko_actions')) || actions;
mainScript = localStorage.getItem('neko_script') || mainScript;
updatePreview();
const firstAct = Object.keys(actions)[0] || "new_action";
actNameInput.value = firstAct;
actContentArea.value = actions[firstAct] || "";
loopScriptArea.value = mainScript;
// 定型入力初期化
tmplIdx = 0;
updateTmplDisplay();
});
document.getElementById('btn-close-editor').addEventListener('click', () => {
if (document.activeElement) { document.activeElement.blur(); }
editorOverlay.style.display = 'none';
window.scrollTo(0, 0);
});
// ヘルプ表示(ボタン位置変更に伴いIDバインド確認)
document.getElementById('btn-show-help').addEventListener('click', () => {
helpModal.style.display = 'flex';
});
document.getElementById('close-help').addEventListener('click', () => {
helpModal.style.display = 'none';
});
function updatePreview() {
if(nekoSprites.length === 0) return;
const name = nekoSprites[spriteIdx];
const filename = `neko-r${name}.png`;
previewImg.src = `${NEKO_PATH}${filename}`;
charFilename.textContent = filename;
}
document.getElementById('btn-char-up').addEventListener('click', () => {
if(nekoSprites.length === 0) return;
spriteIdx = (spriteIdx - 1 + nekoSprites.length) % nekoSprites.length;
updatePreview();
});
document.getElementById('btn-char-down').addEventListener('click', () => {
if(nekoSprites.length === 0) return;
spriteIdx = (spriteIdx + 1) % nekoSprites.length;
updatePreview();
});
document.getElementById('btn-insert-char').addEventListener('click', () => {
if(nekoSprites.length === 0) return;
const txt = nekoSprites[spriteIdx] + " 10";
insertAtCursor(actContentArea, txt + "\n");
});
document.getElementById('btn-act-prev').addEventListener('click', () => cycleAction(-1));
document.getElementById('btn-act-next').addEventListener('click', () => cycleAction(1));
function cycleAction(dir) {
const keys = Object.keys(actions);
if(keys.length === 0) return;
let curr = keys.indexOf(actNameInput.value);
if(curr === -1) curr = 0;
else curr = (curr + dir + keys.length) % keys.length;
actNameInput.value = keys[curr];
actContentArea.value = actions[keys[curr]];
}
document.getElementById('btn-delete-act').addEventListener('click', () => {
const currentAct = actNameInput.value;
if(!currentAct || !actions[currentAct]) return;
if(!confirm(`アクション "${currentAct}" を削除しますか?`)) return;
delete actions[currentAct];
localStorage.setItem('neko_actions', JSON.stringify(actions));
const keys = Object.keys(actions);
if(keys.length > 0) {
actNameInput.value = keys[0];
actContentArea.value = actions[keys[0]];
} else {
actNameInput.value = "new_action";
actContentArea.value = "";
}
});
// --- 新機能:アクションプレビュー ---
let miniPreviewInterval = null;
const miniPreviewOverlay = document.getElementById('mini-preview-overlay');
const miniPreviewImg = document.getElementById('mini-preview-img');
const closeMiniPreviewBtn = document.getElementById('close-mini-preview');
document.getElementById('btn-play-act').addEventListener('click', () => {
const scriptText = actContentArea.value;
if(!scriptText.trim()) return;
// パース
const frames = scriptText.split('\n').map(l => {
const p = l.trim().split(/\s+/);
return { sprite: p[0], time: parseInt(p[1]) || 10 };
}).filter(f => f.sprite);
if(frames.length === 0) return;
// プレビュー開始
miniPreviewOverlay.style.display = 'flex';
let fIdx = 0;
let nextTime = Date.now();
// 簡易ループ関数
const playLoop = () => {
if(miniPreviewOverlay.style.display === 'none') return;
const now = Date.now();
if(now >= nextTime) {
const frame = frames[fIdx];
// 右向き画像を使用(デフォルト)
miniPreviewImg.src = `${NEKO_PATH}neko-r${frame.sprite}.png`;
nextTime = now + (frame.time * 10);
fIdx = (fIdx + 1) % frames.length;
}
miniPreviewInterval = requestAnimationFrame(playLoop);
};
playLoop();
});
closeMiniPreviewBtn.addEventListener('click', () => {
if(miniPreviewInterval) cancelAnimationFrame(miniPreviewInterval);
miniPreviewOverlay.style.display = 'none';
});
document.getElementById('btn-insert-act').addEventListener('click', () => {
const name = "neko-" + actNameInput.value.trim();
insertAtCursor(loopScriptArea, name + " 3\n");
});
document.getElementById('btn-save-script').addEventListener('click', () => {
if(actNameInput.value) { actions[actNameInput.value] = actContentArea.value; }
mainScript = loopScriptArea.value;
localStorage.setItem('neko_actions', JSON.stringify(actions));
localStorage.setItem('neko_script', mainScript);
alert('保存しました。リロードします。');
nekoVM.restart();
});
/* --- 定型入力ロジック --- */
function updateTmplDisplay() {
tmplDisplay.textContent = tmplList[tmplIdx];
}
document.getElementById('btn-tmpl-prev').addEventListener('click', () => {
tmplIdx = (tmplIdx - 1 + tmplList.length) % tmplList.length;
updateTmplDisplay();
});
document.getElementById('btn-tmpl-next').addEventListener('click', () => {
tmplIdx = (tmplIdx + 1) % tmplList.length;
updateTmplDisplay();
});
document.getElementById('btn-tmpl-insert').addEventListener('click', () => {
const txt = tmplList[tmplIdx];
insertAtCursor(loopScriptArea, txt + " ");
});
function insertAtCursor(field, text) {
const start = field.selectionStart || field.value.length;
const end = field.selectionEnd || field.value.length;
field.value = field.value.substring(0, start) + text + field.value.substring(end);
field.focus();
field.selectionStart = field.selectionEnd = start + text.length;
}
/* =================================================================
★★★ External Save/Load Logic (Perl CGI) ★★★
================================================================= */
const externalModal = document.getElementById('external-modal');
const btnExternalMenu = document.getElementById('btn-external-menu');
const btnExtSave = document.getElementById('btn-ext-save');
const btnExtLoad = document.getElementById('btn-ext-load');
const btnExtCancel = document.getElementById('btn-ext-cancel');
const CGI_URL = './todo-neko-data.cgi'; // ★CGIファイル名指定
// メニューを開く
btnExternalMenu.addEventListener('click', () => {
externalModal.style.display = 'flex';
});
// キャンセル
btnExtCancel.addEventListener('click', () => {
externalModal.style.display = 'none';
});
// --- データ形式の定義 ---
// Readableなテキスト形式に変換する
// <<<ACTION:name>>> ... <<<END_ACTION>>>
// <<<MAINSCRIPT>>> ... <<<END_MAINSCRIPT>>>
function serializeData() {
let textData = "";
// アクションデータの変換
const actKeys = Object.keys(actions);
for(let key of actKeys) {
textData += `<<<ACTION:${key}>>>\n`;
textData += actions[key].trim() + "\n";
textData += `<<<END_ACTION>>>\n\n`;
}
// メインスクリプトの変換 (エディタの内容を優先)
const currentScript = loopScriptArea.value;
textData += `<<<MAINSCRIPT>>>\n`;
textData += currentScript.trim() + "\n";
textData += `<<<END_MAINSCRIPT>>>\n`;
return textData;
}
function parseAndLoadData(textData) {
if(!textData || textData === 'EMPTY') return false;
const lines = textData.split(/\r?\n/);
const newActions = {};
let newScript = "";
let state = "NONE"; // NONE, ACTION, SCRIPT
let currentActName = "";
let buffer = [];
for(let line of lines) {
const trimLine = line.trim();
if (trimLine.startsWith('<<<ACTION:')) {
state = "ACTION";
currentActName = trimLine.replace('<<<ACTION:', '').replace('>>>', '');
buffer = [];
} else if (trimLine === '<<<END_ACTION>>>') {
if(currentActName) {
newActions[currentActName] = buffer.join('\n');
}
state = "NONE";
} else if (trimLine === '<<<MAINSCRIPT>>>') {
state = "SCRIPT";
buffer = [];
} else if (trimLine === '<<<END_MAINSCRIPT>>>') {
newScript = buffer.join('\n');
state = "NONE";
} else {
// 中身の行
if(state === "ACTION" || state === "SCRIPT") {
buffer.push(line);
}
}
}
// データ適用
if(Object.keys(newActions).length > 0) {
actions = newActions;
mainScript = newScript;
// ローカルストレージも更新しておく
localStorage.setItem('neko_actions', JSON.stringify(actions));
localStorage.setItem('neko_script', mainScript);
// エディタ画面への反映
loopScriptArea.value = mainScript;
// アクションエディタの表示リセット
const firstAct = Object.keys(actions)[0] || "new_action";
actNameInput.value = firstAct;
actContentArea.value = actions[firstAct] || "";
alert('外部データを読み込みました。');
nekoVM.restart(); // VM再起動
return true;
} else {
alert('有効なデータが見つかりませんでした。');
return false;
}
}
// --- CGI通信 ---
// SAVE処理
btnExtSave.addEventListener('click', () => {
if(!confirm('サーバー上のファイルを上書きします。よろしいですか?')) return;
// 現在のエディタの内容を反映させてから保存データを作る
if(actNameInput.value) { actions[actNameInput.value] = actContentArea.value; }
mainScript = loopScriptArea.value;
const dataStr = serializeData();
const formData = new URLSearchParams();
formData.append('mode', 'save');
formData.append('data', dataStr);
fetch(CGI_URL, {
method: 'POST',
body: formData
})
.then(response => response.text())
.then(text => {
if(text.includes('SUCCESS')) {
alert('保存完了しました。\n(todo-action-data.txt)');
externalModal.style.display = 'none';
} else {
alert('保存エラー: ' + text);
}
})
.catch(err => {
alert('通信エラーが発生しました。\n' + err);
});
});
// LOAD処理
btnExtLoad.addEventListener('click', () => {
if(!confirm('現在の編集内容は失われます。サーバーから読み込みますか?')) return;
const formData = new URLSearchParams();
formData.append('mode', 'load');
fetch(CGI_URL, {
method: 'POST',
body: formData
})
.then(response => response.text())
.then(text => {
if(text.startsWith('ERROR')) {
alert(text);
} else if (text === 'EMPTY') {
alert('サーバーにデータファイルがありません。');
} else {
parseAndLoadData(text);
externalModal.style.display = 'none';
}
})
.catch(err => {
alert('通信エラーが発生しました。\n' + err);
});
});
/* --- Neko Virtual Machine (VM) --- */
class NekoVM {
constructor() {
this.nekoImg = document.getElementById('neko-img');
this.container = document.getElementById('neko-container');
this.size = 96;
this.running = false;
// VM State
this.vars = { R: 0, S: 0, M: 1, T: 0 };
this.currentLine = 0;
this.lines = [];
this.direction = 'r';
this.posX = window.innerWidth/2;
this.posY = window.innerHeight/2;
// 移動制御用
this.currentLabel = "";
this.destX = this.posX;
this.destY = this.posY;
this.container.addEventListener('click', () => {
this.vars.T = 1;
});
this.init();
}
init() {
this.container.style.width = this.size + 'px';
this.container.style.height = this.size + 'px';
this.updatePos();
this.restart();
}
updatePos() {
this.container.style.left = this.posX + 'px';
this.container.style.top = this.posY + 'px';
}
restart() {
this.running = false;
this.lines = mainScript.split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('//'));
this.labels = {};
this.lines.forEach((line, idx) => {
if(line.startsWith(':')) {
this.labels[line.substring(1)] = idx;
}
});
this.currentLine = 0;
this.vars.T = 0;
this.currentLabel = "";
setTimeout(() => {
this.running = true;
this.loop();
}, 500);
}
setRandomDestination() {
const maxX = window.innerWidth - this.size;
const maxY = window.innerHeight - this.size;
this.destX = Math.random() * maxX;
this.destY = Math.random() * maxY;
}
async loop() {
if(!this.running) return;
this.vars.S = document.querySelectorAll('.todo-item').length;
this.vars.M = appMode;
if(this.currentLine >= this.lines.length) {
this.currentLine = 0;
}
const line = this.lines[this.currentLine];
this.currentLine++;
// ラベル処理
if(line.startsWith(':')) {
const labelName = line.substring(1);
this.currentLabel = labelName;
if(labelName.startsWith('MOVE')) {
this.setRandomDestination();
}
this.loop();
return;
}
// GOTO処理
if(line.startsWith('GOTO')) {
const label = line.split(/\s+/)[1];
if(this.labels[label] !== undefined) {
this.currentLine = this.labels[label];
this.currentLabel = label;
if(label.startsWith('MOVE')) {
this.setRandomDestination();
}
}
this.loop();
return;
}
if(line.startsWith('RANDOM')) {
const max = parseInt(line.split(/\s+/)[1]) || 1;
this.vars.R = Math.floor(Math.random() * max) + 1;
this.loop();
return;
}
if(line === 'TC') {
this.vars.T = 0;
this.loop();
return;
}
if(line.includes('=') || line.includes('>') || line.includes('<')) {
if(this.evaluateCondition(line)) {
this.loop();
} else {
this.skipBlock();
this.loop();
}
return;
}
if(line.startsWith('neko-')) {
const parts = line.split(/\s+/);
const actName = parts[0].replace('neko-', '');
const durationSec = parseFloat(parts[1]) || 1;
await this.playAction(actName, durationSec);
this.loop();
return;
}
this.loop();
}
skipBlock() {
while(this.currentLine < this.lines.length) {
const next = this.lines[this.currentLine];
if(next.startsWith(':')) break;
if(next.includes('=') || next.includes('>') || next.includes('<')) break;
this.currentLine++;
}
}
evaluateCondition(condStr) {
let jsExpr = condStr
.replace(/AND/g, '&&')
.replace(/OR/g, '||')
.replace(/=/g, '===')
.replace(/<>/g, '!==');
const v = this.vars;
jsExpr = jsExpr.replace(/\bR\b/g, v.R);
jsExpr = jsExpr.replace(/\bS\b/g, v.S);
jsExpr = jsExpr.replace(/\bM\b/g, v.M);
jsExpr = jsExpr.replace(/\bT\b/g, v.T);
try {
return new Function('return ' + jsExpr)();
} catch(e) {
console.error("Cond Error:", condStr, e);
return false;
}
}
async playAction(actName, durationSec) {
const defRaw = actions[actName];
if(!defRaw) return;
const frames = defRaw.split('\n').map(l => {
const p = l.trim().split(/\s+/);
return { sprite: p[0], time: parseInt(p[1]) || 10 };
}).filter(f => f.sprite);
if(frames.length === 0) return;
// 移動判定:現在のラベルが MOVE で始まる場合のみ移動
const isMoving = this.currentLabel.startsWith('MOVE');
// 重要: 移動開始時の座標を保持 (線形補間用)
const startX = this.posX;
const startY = this.posY;
if(isMoving) {
this.direction = this.destX > this.posX ? 'r' : 'l';
}
const startTime = Date.now();
const endTime = startTime + (durationSec * 1000);
let fIdx = 0;
let nextFrameTime = 0;
return new Promise(resolve => {
const frameLoop = () => {
if(!this.running) { resolve(); return; }
const now = Date.now();
if(now >= endTime) {
// 完了時、最終座標へ確実に移動
if(isMoving) {
this.posX = this.destX;
this.posY = this.destY;
this.updatePos();
}
resolve();
return;
}
// アニメーションフレーム更新
if(now >= nextFrameTime) {
const frame = frames[fIdx];
this.nekoImg.src = `${NEKO_PATH}neko-${this.direction}${frame.sprite}.png`;
nextFrameTime = now + (frame.time * 10);
fIdx = (fIdx + 1) % frames.length;
}
// 移動ロジック (線形補間: Lerp)
// 経過時間に基づき、現在位置を計算する
if(isMoving) {
const timePassed = now - startTime;
let progress = timePassed / (durationSec * 1000);
if(progress > 1) progress = 1;
this.posX = startX + (this.destX - startX) * progress;
this.posY = startY + (this.destY - startY) * progress;
this.updatePos();
}
requestAnimationFrame(frameLoop);
};
frameLoop();
});
}
}
const nekoVM = new NekoVM();
</script>
</body>
</html>
使用変数
| ' | |
| 'R | |
| * | |
| 1 | |
| 2 | |
| 3 | |
| actContentArea | |
| actionBtn | |
| actions | |
| actKeys | |
| actName | |
| actNameInput | |
| addBtn | |
| addTodo -------( Function ) | |
| alt | |
| appMode | |
| assessBtns | |
| autocomplete | |
| b | |
| btn | |
| btnCancelDelete | |
| btnExtCancel | |
| btnExternalMenu | |
| btnExtLoad | |
| btnExtSave | |
| btnJustDelete | |
| btnRecordDelete | |
| buffer | |
| CGI_URL | |
| charFilename | |
| charset | |
| class | |
| className | |
| closeHistoryBtn | |
| confirmMemoInput | |
| confirmModal | |
| confirmTaskName | |
| container | |
| content | |
| curr | |
| currentAct | |
| currentActName | |
| currentLabel | |
| currentLine | |
| currentScript | |
| cycleAction -------( Function ) | |
| dataStr | |
| DEFAULT_ACTIONS | |
| DEFAULT_SCRIPT | |
| defRaw | |
| destX | |
| destY | |
| direction | |
| display | |
| durationSec | |
| editorOverlay | |
| editToggleBtn | |
| end | |
| endingDeleteTarget | |
| endTime | |
| err | |
| executeDelete -------( Function ) | |
| externalModal | |
| f | |
| fIdx | |
| filename | |
| firstAct | |
| formData | |
| frame | |
| frameLoop | |
| frames | |
| getCurrentTime -------( Function ) | |
| height | |
| helpModal | |
| historyBtn | |
| historyData | |
| historyList | |
| historyModal | |
| i | |
| id | |
| iniPreviewInterval | |
| innerHTML | |
| inp | |
| input | |
| insertAtCursor -------( Function ) | |
| isEdit | |
| isMoving | |
| item | |
| items | |
| j | |
| jsExpr | |
| key | |
| keys | |
| l | |
| label | |
| labelName | |
| labels | |
| lang | |
| left | |
| length | |
| li | |
| line | |
| lines | |
| list | |
| loadSpriteList -------( Function ) | |
| loadTodos -------( Function ) | |
| loopScriptArea | |
| loseMiniPreviewBtn | |
| M | |
| mainScript | |
| match | |
| max | |
| maxX | |
| maxY | |
| mem | |
| miniPreviewImg | |
| miniPreviewOverlay | |
| modBtn | |
| name | |
| nekoImg | |
| nekoSprites | |
| nekoVM | |
| NEKO_PATH | |
| newActions | |
| newScript | |
| next | |
| nextFrameTime | |
| nextTime | |
| now | |
| p | |
| parseAndLoadData -------( Function ) | |
| parts | |
| pendingDeleteTarget | |
| placeholder | |
| playLoop | |
| posX | |
| posY | |
| previewImg | |
| progress | |
| R | |
| randomBtn | |
| renderHistory -------( Function ) | |
| resolve | |
| response | |
| running | |
| S | |
| save | |
| saved | |
| saveTodos -------( Function ) | |
| saveToHistory -------( Function ) | |
| scalable | |
| scale | |
| scriptText | |
| selectedSymbol | |
| selectionEnd | |
| selectionStart | |
| serializeData -------( Function ) | |
| size | |
| sortable | |
| spellcheck | |
| spriteIdx | |
| src | |
| start | |
| startTime | |
| startX | |
| startY | |
| state | |
| style | |
| sym | |
| T | |
| t | |
| text | |
| textContent | |
| textData | |
| textSpan | |
| timePassed | |
| title | |
| tmplDisplay | |
| tmplIdx | |
| tmplList | |
| todos | |
| top | |
| trimLine | |
| txt | |
| type | |
| uniqueSprites | |
| updatePreview -------( Function ) | |
| updateTmplDisplay -------( Function ) | |
| v | |
| val | |
| value | |
| vars | |
| width |