junkerstock
 ss-test01.02 

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>猫なでアドベンチャー</title>
<style>
/* ベーススタイル */
body { margin: 0; padding: 0; font-family: sans-serif; overflow: hidden; background: #333; color: #fff; touch-action: none; }
#app { width: 100vw; height: 100vh; position: relative; display: flex; flex-direction: column; }

/* 共通コンポーネント */
button { padding: 10px; margin: 2px; border: none; border-radius: 4px; background: #ddd; cursor: pointer; }
button:active { background: #bbb; }
.hidden { display: none !important; }

/* 画面レイアウト */
.screen { flex: 1; display: flex; flex-direction: column; width: 100%; height: 100%; }

/* スタート画面 */
#start-screen { justify-content: center; align-items: center; gap: 20px; }
#start-screen h1 { margin-bottom: 20px; }

/* 編集・再生共通:画像エリア */
.image-area {
position: relative;
width: 100%;
height: 50vh;
background: #222;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
}
.image-area img { max-width: 100%; max-height: 100%; object-fit: contain; user-select: none; pointer-events: none; }

/* ヒットエリア(ナデナデポイント) */
.hit-box {
position: absolute;
width: 60px; height: 60px;
display: flex; justify-content: center; align-items: center;
font-weight: bold; color: white;
user-select: none;
touch-action: none;
}
.hit-box.red { background: rgba(255, 0, 0, 0.5); border: 2px solid red; }
.hit-box.blue { background: rgba(0, 0, 255, 0.5); border: 2px solid blue; }
.hit-box.playing { opacity: 0; } /* 再生中は見えないが判定はある */
/* デバッグ用: 再生中でも位置確認したい場合はここを opacity: 0.3 とかにする */

/* 編集モード下部 */
.edit-bottom { flex: 1; display: flex; height: 50vh; }
.edit-text-panel { flex: 1; display: flex; flex-direction: column; padding: 5px; background: #444; }
.edit-text-panel textarea { flex: 1; width: 95%; resize: none; font-size: 14px; }
.edit-control-panel { flex: 1; background: #555; padding: 5px; display: flex; flex-direction: column; gap: 5px; align-items: center; }
.palette-area { display: flex; gap: 10px; margin-bottom: 10px; }
.palette-item { width: 50px; height: 50px; display: flex; justify-content: center; align-items: center; color: white; cursor: grab; }
.img-selector { display: flex; align-items: center; gap: 5px; margin-bottom: 5px; }

/* 再生モード下部 */
.play-bottom { flex: 1; padding: 20px; background: #222; border-top: 2px solid #555; font-size: 18px; line-height: 1.6; overflow-y: auto; }

/* 戻るボタン配置 */
.back-btn-area { position: absolute; top: 10px; left: 10px; z-index: 100; }
</style>
</head>
<body>

<div id="app">
<div id="start-screen" class="screen">
<h1>猫なでADV</h1>
<button onclick="startEditMode()">編集モード</button>
<button onclick="startPlayMode()">再生モード</button>
</div>

<div id="edit-screen" class="screen hidden">
<div class="image-area" id="edit-image-container">
<img id="edit-img" src="" alt="Cat Image">
<div id="edit-overlay" style="position:absolute; top:0; left:0; width:100%; height:100%;"></div>
</div>
<div class="edit-bottom">
<div class="edit-text-panel">
<textarea id="scenario-input" placeholder="ここにシナリオを入力..."></textarea>
</div>
<div class="edit-control-panel">
<div class="img-selector">
<button onclick="changeEditImage(-1)">←</button>
<span id="current-img-name">a1.png</span>
<button onclick="changeEditImage(1)">→</button>
</div>
<div class="palette-area">
<div class="palette-item red" style="background:red;" draggable="true" ondragstart="dragStart(event, 'red')">赤</div>
<div class="palette-item blue" style="background:blue;" draggable="true" ondragstart="dragStart(event, 'blue')">青</div>
</div>
<button onclick="downloadData()">保存 (DL)</button>
<button onclick="returnToTitle()">戻る</button>
<div style="font-size:10px; color:#aaa;">赤:ドラッグで配置<br>配置後移動可能</div>
</div>
</div>
</div>

<div id="play-screen" class="screen hidden">
<div class="image-area" id="play-image-container">
<img id="play-img" src="" alt="Cat Image">
<div class="back-btn-area">
<button onclick="returnToTitle()">Exit</button>
</div>
</div>
<div class="play-bottom" id="message-area">
</div>
</div>
</div>

<script>
/* ================= 定数・状態管理 ================= */
// 画像は本来 picフォルダ ですが、デモ用にプレースホルダーを使用
// 実装時はここを実際のパスにしてください ('pic/a1.png' 等)
const MAX_IMAGES = 10;
const getImgPath = (name) => {
// デモ用: 色付きの画像を生成するAPIを使用
const num = parseInt(name.replace('a', '').replace('.png', ''));
const hue = (num * 30) % 360;
return `https://via.placeholder.com/600x400/333/fff?text=${name}`;
// 本番用: return `pic/${name}`;
};

let appState = {
mode: 'title', // title, edit, play
currentImgIndex: 1, // a1 ~ a10
// アクション情報: { "a1.png": [ {type:'red', id:1, x:10, y:20}, ... ] }
actionData: {},
scenarioText: `:start\n猫がいる。\na1.png\n赤1-3 happy\n青1-5 sleep\n\n:happy\n気持ちよさそうだ。\na2.png\n\n:sleep\n眠ってしまった。\na3.png`,
};

// 実行時の一時変数
let playState = {
currentLabel: null,
touchCounts: {}, // { "red1": 0, "blue2": 0 }
activeTriggers: [] // 現在有効なトリガーリスト
};

/* ================= 画面遷移 ================= */
function showScreen(id) {
document.querySelectorAll('.screen').forEach(el => el.classList.add('hidden'));
document.getElementById(id).classList.remove('hidden');
}

function startEditMode() {
appState.mode = 'edit';
document.getElementById('scenario-input').value = appState.scenarioText;
updateEditImageDisplay();
showScreen('edit-screen');
}

function startPlayMode() {
appState.mode = 'play';
// シナリオを読み込んで保存
appState.scenarioText = document.getElementById('scenario-input').value || appState.scenarioText;

// パースして実行開始
showScreen('play-screen');
runScenario(':start'); // 初期ラベル(指定がなければ最初から)
}

function returnToTitle() {
appState.mode = 'title';
showScreen('start-screen');
}

/* ================= 編集モードロジック ================= */
function changeEditImage(dir) {
appState.currentImgIndex += dir;
if (appState.currentImgIndex < 1) appState.currentImgIndex = MAX_IMAGES;
if (appState.currentImgIndex > MAX_IMAGES) appState.currentImgIndex = 1;
updateEditImageDisplay();
}

function updateEditImageDisplay() {
const name = `a${appState.currentImgIndex}.png`;
document.getElementById('current-img-name').innerText = name;
document.getElementById('edit-img').src = getImgPath(name);
renderEditHitBoxes(name);
}

// ヒットボックスの描画(編集用)
function renderEditHitBoxes(imgName) {
const container = document.getElementById('edit-overlay');
container.innerHTML = '';
const items = appState.actionData[imgName] || [];

items.forEach((item, index) => {
const el = document.createElement('div');
el.className = `hit-box ${item.type}`;
el.innerText = item.id;
el.style.left = item.x + '%';
el.style.top = item.y + '%';

// 編集モードでのドラッグ移動
el.draggable = true;
el.ondragend = (e) => updateBoxPosition(e, imgName, index);
// スマホ用タッチ移動ロジック(簡易版)
addTouchDrag(el, imgName, index);

container.appendChild(el);
});
}

// パレットからのドラッグ開始
let draggingType = null;
function dragStart(e, type) {
draggingType = type;
}

// ドロップエリア(オーバーレイ)
const overlay = document.getElementById('edit-overlay');
overlay.ondragover = (e) => e.preventDefault();
overlay.ondrop = (e) => {
e.preventDefault();
if (!draggingType) return;

const rect = overlay.getBoundingClientRect();
const x = ((e.clientX - rect.left) / rect.width) * 100;
const y = ((e.clientY - rect.top) / rect.height) * 100;

addHitBox(x, y, draggingType);
draggingType = null;
};

function addHitBox(x, y, type) {
const imgName = `a${appState.currentImgIndex}.png`;
if (!appState.actionData[imgName]) appState.actionData[imgName] = [];

// 番号の自動採番
const existing = appState.actionData[imgName].filter(i => i.type === type);
const nextId = existing.length > 0 ? Math.max(...existing.map(i => i.id)) + 1 : 1;

appState.actionData[imgName].push({
type: type,
id: nextId,
x: x.toFixed(1), // %
y: y.toFixed(1) // %
});
renderEditHitBoxes(imgName);
}

function updateBoxPosition(e, imgName, index) {
const rect = overlay.getBoundingClientRect();
// マウス位置から計算(簡易実装)
let clientX = e.clientX;
let clientY = e.clientY;
// 0の場合は非表示等の可能性があるので無視
if (clientX === 0 && clientY === 0) return;

const x = ((clientX - rect.left) / rect.width) * 100;
const y = ((clientY - rect.top) / rect.height) * 100;

appState.actionData[imgName][index].x = Math.max(0, Math.min(90, x)).toFixed(1);
appState.actionData[imgName][index].y = Math.max(0, Math.min(90, y)).toFixed(1);
renderEditHitBoxes(imgName);
}

// スマホタッチでの配置編集用(簡易実装)
function addTouchDrag(el, imgName, index) {
let startX, startY;
el.addEventListener('touchstart', (e) => {
startX = e.touches[0].clientX;
startY = e.touches[0].clientY;
e.stopPropagation(); // 親への伝播を止める
}, {passive: false});

el.addEventListener('touchmove', (e) => {
e.preventDefault(); // スクロール防止
const touch = e.touches[0];
const rect = overlay.getBoundingClientRect();
const x = ((touch.clientX - rect.left) / rect.width) * 100;
const y = ((touch.clientY - rect.top) / rect.height) * 100;

// 即時反映すると重いかもしれないが、簡単な実装として
el.style.left = x + '%';
el.style.top = y + '%';
}, {passive: false});

el.addEventListener('touchend', (e) => {
// 最終位置を保存
const touch = e.changedTouches[0];
const rect = overlay.getBoundingClientRect();
const x = ((touch.clientX - rect.left) / rect.width) * 100;
const y = ((touch.clientY - rect.top) / rect.height) * 100;

appState.actionData[imgName][index].x = Math.max(0, Math.min(90, x)).toFixed(1);
appState.actionData[imgName][index].y = Math.max(0, Math.min(90, y)).toFixed(1);
renderEditHitBoxes(imgName);
});
}

function downloadData() {
appState.scenarioText = document.getElementById('scenario-input').value;
const data = {
scenario: appState.scenarioText,
actions: appState.actionData
};
const blob = new Blob([JSON.stringify(data, null, 2)], {type: 'application/json'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'cat_game_data.json';
a.click();
}

/* ================= 再生エンジンのコアロジック ================= */

// シナリオ解析と実行
function runScenario(startLabel) {
const lines = appState.scenarioText.split('\n').map(l => l.trim());
let startIndex = 0;

// ラベル検索(:abc または startLabelが指定なしなら0行目から)
if (startLabel) {
const labelKey = startLabel.startsWith(':') ? startLabel : ':' + startLabel;
startIndex = lines.findIndex(l => l === labelKey);
if (startIndex === -1) startIndex = 0; // 見つからなければ最初から
}

// 実行状態のリセット
playState.activeTriggers = [];
playState.touchCounts = {};

const messageArea = document.getElementById('message-area');
messageArea.innerHTML = ''; // 画面切り替え時にクリアするかは仕様次第(ここではクリア)

let currentImg = null;

// 1行ずつ処理(非同期処理なしでここまで一気に読む)
// ループで回し、次のラベル(:xxx)が来たら「待ち」状態に入る
for (let i = startIndex + 1; i < lines.length; i++) {
const line = lines[i];
if (!line) continue;

// 次のラベルが来たら、ここまでの設定で「待機」に入る
if (line.startsWith(':')) {
break;
}

// 画像指定 (xxx.png)
if (line.match(/^[a-zA-Z0-9]+\.png$/)) {
currentImg = line;
document.getElementById('play-img').src = getImgPath(currentImg);
setupPlayHitBoxes(currentImg); // 画像が変わったらヒットエリアも再配置
}
// トリガー指定 (赤1-5 ccc)
else if (line.match(/^(赤|青)(\d+)-(\d+)\s+(.+)$/)) {
const m = line.match(/^(赤|青)(\d+)-(\d+)\s+(.+)$/);
const type = m[1] === '赤' ? 'red' : 'blue';
const id = parseInt(m[2]);
const count = parseInt(m[3]);
const jumpTo = m[4];

// トリガー登録
playState.activeTriggers.push({ type, id, count, jumpTo });
// カウンター初期化
playState.touchCounts[`${type}${id}`] = 0;
}
// テキスト表示(それ以外)
else {
const p = document.createElement('p');
p.innerText = line;
messageArea.appendChild(p);
messageArea.scrollTop = messageArea.scrollHeight;
}
}

// ループを抜けたら「ユーザー入力待ち」状態になっている
console.log("Wait for input...", playState.activeTriggers);
}

// 再生用ヒットボックス配置
function setupPlayHitBoxes(imgName) {
const container = document.getElementById('play-image-container');
// 画像と戻るボタン以外(=古いヒットボックス)を削除
Array.from(container.children).forEach(c => {
if (c.tagName === 'DIV' && c.classList.contains('hit-box')) {
container.removeChild(c);
}
});

const items = appState.actionData[imgName] || [];
items.forEach(item => {
const el = document.createElement('div');
el.className = `hit-box ${item.type} playing`; // 透明クラス付与
el.style.left = item.x + '%';
el.style.top = item.y + '%';

// インタラクション登録
if (item.type === 'red') {
// 赤=タップ(クリック)
el.addEventListener('pointerdown', () => handleInteraction('red', item.id));
} else {
// 青=スライド(なでなで)
// 連続した動きを検知する必要がある
let slideCount = 0;
let lastX = 0;
el.addEventListener('touchmove', (e) => {
// 簡単な実装:イベント発火回数、または移動量でカウント
// ここでは「撫でられた」感を出すため、一定のイベント発火ごとにカウントアップ
slideCount++;
if (slideCount % 5 === 0) { // 感度調整
handleInteraction('blue', item.id);
}
}, {passive: true});

// PCのマウス用
el.addEventListener('mousemove', (e) => {
if (e.buttons === 1) { // クリックしながら
slideCount++;
if (slideCount % 5 === 0) {
handleInteraction('blue', item.id);
}
}
});
}

container.appendChild(el);
});
}

// 判定処理
function handleInteraction(type, id) {
const key = `${type}${id}`;
if (playState.touchCounts[key] !== undefined) {
playState.touchCounts[key]++;
console.log(`Hit: ${key}, Count: ${playState.touchCounts[key]}`);

// 条件を満たすトリガーがあるかチェック
const trigger = playState.activeTriggers.find(t => t.type === type && t.id === id);

if (trigger) {
if (playState.touchCounts[key] >= trigger.count) {
// 条件達成!ジャンプ実行
console.log(`Trigger fired! Jump to ${trigger.jumpTo}`);
runScenario(trigger.jumpTo);
}
}
}
}


/* ================= Android/スマホ用 パレットドラッグ対応 ================= */
// ページ読み込み完了時、またはスクリプトの最後で実行
(function setupPaletteTouch() {
const paletteItems = document.querySelectorAll('.palette-item');
const overlay = document.getElementById('edit-overlay');

paletteItems.forEach(item => {
// タッチ開始時:スクロール防止など
item.addEventListener('touchstart', (e) => {
// ドラッグ操作とみなすためデフォルト動作(スクロール等)を止める
if(e.cancelable) e.preventDefault();
}, { passive: false });

// タッチ終了時:場所を判定して配置
item.addEventListener('touchend', (e) => {
// 指を離した位置を取得
const touch = e.changedTouches[0];
const clientX = touch.clientX;
const clientY = touch.clientY;

// 画像エリア(オーバーレイ)の範囲内かチェック
const rect = overlay.getBoundingClientRect();
const isInX = clientX >= rect.left && clientX <= rect.right;
const isInY = clientY >= rect.top && clientY <= rect.bottom;

if (isInX && isInY) {
// 画像内の相対座標(%)に変換
const x = ((clientX - rect.left) / rect.width) * 100;
const y = ((clientY - rect.top) / rect.height) * 100;

// 赤か青か判定
let type = 'blue';
if (item.classList.contains('red') || item.innerText.includes('赤')) {
type = 'red';
}

// ヒットボックス追加関数を呼び出し
addHitBox(x, y, type);
}
});
});
})();

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


使用変数

*
5
a
activeTriggers
addHitBox -------( Function )
addTouchDrag -------( Function )
alt
appState
blob
buttons
c
changeEditImage -------( Function )
charset
class
className
clientX
clientY
container
content
count
currentImg
currentImgIndex
data
download
downloadData -------( Function )
draggable
draggingType
dragStart -------( Function )
el
existing
getImgPath
handleInteraction -------( Function )
href
hue
i
id
imgName
innerHTML
innerText
isInX
isInY
item
items
jumpTo
key
l
labelKey
lang
lastX
left
line
lines
m
MAX_IMAGES
messageArea
mode
name
nextId
num
onclick
ondragend
ondragover
ondragstart
ondrop
overlay
p
paletteItems
placeholder
playState
rect
renderEditHitBoxes -------( Function )
returnToTitle -------( Function )
runScenario -------( Function )
scalable
scale
scenarioText
scrollTop
setupPaletteTouch -------( Function )
setupPlayHitBoxes -------( Function )
showScreen -------( Function )
slideCount
src
startEditMode -------( Function )
startIndex
startPlayMode -------( Function )
startX
startY
style
t
tagName
text
top
touch
touchCounts
trigger
type
updateBoxPosition -------( Function )
updateEditImageDisplay -------( Function )
url
value
width
x
y
パレットドラッグ対応
再生エンジンのコアロジック
定数・状態管理
画面遷移
編集モードロジック