junkerstock
 todo-test13 

<!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 + Neko Script</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;
}

.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;
}
@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; }

/* 履歴リスト */
#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%; /* 画面の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-center-col {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
#preview-img {
width: 96px; 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);
}
.circle-btn:active { background: #ddd; }
.insert-btn { background: #fd7e14; color: white; border: none; font-weight: bold; }

/* 中段・下段を横並びにするコンテナ */
.editor-row-container {
display: flex;
flex-direction: row;
flex: 1; /* 残りの高さを占有 */
overflow: hidden;
}

.editor-split-pane {
flex: 1; /* 1:1で分割 */
border-right: 2px solid #ddd;
display: flex;
flex-direction: column;
padding: 10px;
position: relative;
}
.editor-split-pane:last-child { border-right: none; }

/* アクション入力 (左側) */
.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;
}
.mini-circle-btn {
width: 30px; height: 30px; font-size: 14px;
border-radius: 50%; border: 1px solid #999; background: white;
cursor: pointer;
}
#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;
}

/* メインループ (右側) */
#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;
}
.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;
}
#btn-save-script { background: #28a745; color: white; }
#btn-close-editor { background: #6c757d; color: white; }
#btn-add-goto { background: #17a2b8; color: white; }

/* Neko Styling */
#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; }

</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="action-editor-overlay">

<div class="editor-section char-view-section">
<span class="editor-label">キャラクタ表示</span>
<div id="char-view-container">
<button class="circle-btn insert-btn" id="btn-insert-char" title="現在のアクションへ挿入">↓</button>

<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">
<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>
<button class="mini-circle-btn insert-btn" id="btn-insert-act" style="color:white; border:none;">↓</button>
</div>
<textarea id="act-content-area" placeholder="例:
stop1 10
stop2 10"></textarea>
</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-add-goto" class="editor-cmd-btn">GOTO</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=2
neko-sit 4
GOTO MAIN
R=3
neko-sleep 5
GOTO MAIN

:MOVE
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 = [];

// --- エディタUI制御 ---
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');

let spriteIdx = 0;

// list.txtの読み込みとスプライトリストの構築
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();
// neko-lxxx.png または neko-rxxx.png のみを対象とする
// l/rを除いた "xxx" をアクションIDとする
const match = l.match(/^neko-[lr](.+)\.png$/);
if (match) {
uniqueSprites.add(match[1]); // stop1, run1 など
}
});

if (uniqueSprites.size > 0) {
nekoSprites = Array.from(uniqueSprites).sort();
spriteIdx = 0;
updatePreview();
} else {
console.warn("No valid sprites found in list.txt");
}
} catch (e) {
console.error("Failed to load list.txt:", 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;
});

document.getElementById('btn-close-editor').addEventListener('click', () => {
editorOverlay.style.display = 'none';
});

// キャラクタ表示部
function updatePreview() {
if(nekoSprites.length === 0) return;
const name = nekoSprites[spriteIdx];
const filename = `neko-r${name}.png`; // プレビューは右向き(r)を基本とする
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;
// 現在の画像をアクション入力へ挿入 (IDのみ)
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-insert-act').addEventListener('click', () => {
const name = "neko-" + actNameInput.value.trim();
insertAtCursor(loopScriptArea, name + " 3\n");
});

// メインループ部
document.getElementById('btn-add-goto').addEventListener('click', () => {
insertAtCursor(loopScriptArea, "GOTO LABEL\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 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;
}

/* --- 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.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;

setTimeout(() => {
this.running = true;
this.loop();
}, 500);
}

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(':')) { this.loop(); return; }

if(line.startsWith('GOTO')) {
const label = line.split(/\s+/)[1];
if(this.labels[label] !== undefined) {
this.currentLine = this.labels[label];
}
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;

let isMoving = actName.includes('run') || actName.includes('walk');
let moveStepX = 0;
let moveStepY = 0;

if(isMoving) {
const maxX = window.innerWidth - this.size;
const maxY = window.innerHeight - this.size;
const destX = Math.random() * maxX;
const destY = Math.random() * maxY;

this.direction = destX > this.posX ? 'r' : 'l';

const totalTime = durationSec * 1000;
moveStepX = (destX - this.posX) / (totalTime / 100);
moveStepY = (destY - this.posY) / (totalTime / 100);
}

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) {
resolve();
return;
}

if(now >= nextFrameTime) {
const frame = frames[fIdx];
// 画像更新:方向(l/r) + ID
this.nekoImg.src = `${NEKO_PATH}neko-${this.direction}${frame.sprite}.png`;

nextFrameTime = now + (frame.time * 10);
fIdx = (fIdx + 1) % frames.length;
}

if(isMoving) {
this.posX += moveStepX * 1.5;
this.posY += moveStepY * 1.5;

this.posX = Math.max(0, Math.min(window.innerWidth - this.size, this.posX));
this.posY = Math.max(0, Math.min(window.innerHeight - this.size, this.posY));
this.updatePos();
}

requestAnimationFrame(frameLoop);
};
frameLoop();
});
}
}

const nekoVM = new NekoVM();

</script>
</body>
</html>


使用変数

'
*
actContentArea
actionBtn
actions
actName
actNameInput
addBtn
addTodo -------( Function )
alt
appMode
assessBtns
autocomplete
b
btn
btnCancelDelete
btnJustDelete
btnRecordDelete
charFilename
charset
class
className
closeHistoryBtn
confirmMemoInput
confirmModal
confirmTaskName
container
content
curr
currentLine
cycleAction -------( Function )
DEFAULT_ACTIONS
DEFAULT_SCRIPT
defRaw
destX
destY
direction
display
durationSec
editorOverlay
editToggleBtn
end
endingDeleteTarget
endTime
executeDelete -------( Function )
f
fIdx
filename
firstAct
frame
frameLoop
frames
getCurrentTime -------( Function )
height
historyBtn
historyData
historyList
historyModal
i
id
innerHTML
inp
input
insertAtCursor -------( Function )
isEdit
isMoving
item
items
j
jsExpr
key
keys
l
label
labels
lang
left
length
li
line
lines
list
loadSpriteList -------( Function )
loadTodos -------( Function )
loopScriptArea
M
mainScript
match
max
maxX
maxY
mem
modBtn
moveStepX
moveStepY
name
nekoImg
nekoSprites
nekoVM
NEKO_PATH
next
nextFrameTime
now
p
parts
pendingDeleteTarget
placeholder
posX
posY
previewImg
R
randomBtn
renderHistory -------( Function )
resolve
response
running
S
save
saved
saveTodos -------( Function )
saveToHistory -------( Function )
scalable
scale
selectedSymbol
selectionEnd
selectionStart
size
sortable
spellcheck
spriteIdx
src
start
startTime
style
sym
t
T
text
textContent
textSpan
title
todos
top
totalTime
txt
type
uniqueSprites
updatePreview -------( Function )
v
val
value
vars
width