#include <iostream>
#include <string>
using namespace std;
int main() {
string s;
s = "This is a"; // 最初の文字列
s.append(" pen."); // 文字列の追加
cout << s << endl;
cout << "文字列の長さ:" << s.length() << endl;
// printfで表示
printf("char*:%s\n", s.c_str());
return 0;
}
タグ: programming
C++ コンソールから入力
#include <iostream>
using namespace std;
int main() {
int a;
cin >> a;
cout << "a=" << a << endl;
return 0;
}
C++ HelloWorld
#include <iostream>
using namespace std;
int main() {
cout << "HelloWorld." << endl;
return 0;
}
Aran Red Fantasy.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Aran Red Fantasy - Ultimate</title>
<style>
/* ===============================
基本CSSスタイル
=============================== */
body {
font-family: Arial, sans-serif;
background-color: #f0f0f0;
margin: 0;
padding: 0;
}
header, footer, nav {
background-color: #005ce6;
color: #fff;
text-align: center;
padding: 10px;
}
header h1, footer .container { margin: 0; }
nav a {
color: #fff;
text-decoration: none;
margin: 0 8px;
padding: 5px 8px;
display: inline-block;
}
nav a:hover { background-color: #004bb5; border-radius: 4px; }
nav a.active { background-color: #003a8c; border-radius: 4px; }
main { padding: 20px; }
.container { max-width: 1400px; margin: 0 auto; }
.button {
background-color: #4CAF50;
border: none;
color: white;
padding: 8px 16px;
text-align: center;
text-decoration: none;
font-size: 14px;
margin: 4px 2px;
cursor: pointer;
border-radius: 5px;
}
.button:hover { background-color: #45a049; }
.disabled { opacity: 0.6; cursor: default; }
.muted { color:#667; font-size: 13px; }
/* カード風 */
.card {
background-color: #fff;
border: 1px solid #ddd;
border-radius: 5px;
padding: 16px;
margin-bottom: 20px;
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1);
}
.card h3 { margin-top: 0; }
/* プログレスバー */
.progress-bar {
background-color: #ddd;
border-radius: 5px;
height: 20px;
width: 100%;
margin-bottom: 10px;
}
.progress {
background-color: #4CAF50;
height: 100%;
border-radius: 5px;
width: 0%;
}
/* インベントリアイテム表示 */
.inventory-item {
background-color: #fff;
border: 1px solid #ddd;
border-radius: 5px;
display: inline-block;
margin: 5px;
padding: 10px;
min-width: 120px;
text-align: center;
cursor: pointer;
transition: background-color 0.2s;
user-select: none;
}
.inventory-item:hover { background-color: #eef; }
/* メッセージ表示 */
.message {
background-color: #fff8dd;
border: 1px solid #f5c666;
padding: 10px;
margin-bottom: 10px;
border-radius: 5px;
white-space: pre-wrap;
}
/* モーダル */
.modal-bg {
position: fixed;
top: 0; left: 0;
width:100%; height:100%;
background: rgba(0,0,0,.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 999;
}
.modal {
background: #fff;
padding: 20px;
border-radius: 5px;
text-align: center;
max-width: 520px;
width: 92%;
}
.modal h2 { margin-top: 0; }
.modal img { max-width: 100%; height: auto; border-radius: 6px; }
/* キャラクター表示 */
#character-image {
max-width: 420px;
width: 100%;
height: auto;
margin: 20px auto;
display: block;
border-radius: 8px;
border: 1px solid #ddd;
background: #fff;
}
/* バトル用スタイル */
.battle-container {
display: flex;
flex-wrap: wrap;
gap: 20px;
}
.enemy-card {
background-color: #fff;
border: 1px solid #ddd;
border-radius: 5px;
width: 250px;
padding: 16px;
text-align: center;
}
/* メッセージログ */
.log {
background-color: #eef;
border: 1px solid #bbe;
border-radius: 5px;
padding: 10px;
max-height: 300px;
overflow-y: auto;
margin: 10px 0;
white-space: pre-wrap;
}
/* ロケーションボタン */
#location-buttons button { margin-right: 10px; }
/* スキル一覧 */
.skill-list { list-style: none; padding: 0; }
.skill-list li { margin: 5px 0; }
/* クエストログ */
#quest-log-list { list-style: none; padding: 0; }
#quest-log-list li { margin: 4px 0; }
/* 実績一覧 */
#achievement-list { list-style: none; padding: 0; }
#achievement-list li { margin: 5px 0; }
/* ===============================
アートギャラリー
=============================== */
.gallery-grid{
display:grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 14px;
}
.gallery-item{
background:#fff;
border:1px solid #ddd;
border-radius:8px;
overflow:hidden;
box-shadow: 0px 2px 4px rgba(0,0,0,0.08);
display:flex;
flex-direction:column;
}
.gallery-item img{
width:100%;
height:auto;
display:block;
background:#fff;
}
.gallery-meta{
padding:10px;
display:flex;
align-items:center;
justify-content:space-between;
gap:8px;
flex-wrap:wrap;
}
.badge{
display:inline-block;
padding:4px 8px;
border-radius:999px;
font-size:12px;
background:#eef;
border:1px solid #bbe;
color:#223;
}
.badge.owned{
background:#e9ffe9;
border-color:#9fd49f;
color:#1c5a1c;
}
.badge.rarity-ur{
background:#fff2cc;
border-color:#f3d27a;
color:#6b4b00;
}
.badge.rarity-ssr{
background:#e8f0ff;
border-color:#9fb7ff;
color:#133a7a;
}
.gallery-actions{
display:flex;
gap:8px;
flex-wrap:wrap;
padding: 0 10px 12px;
}
/* ガチャUI */
.gacha-row{
display:flex;
align-items:center;
justify-content:space-between;
gap:10px;
flex-wrap:wrap;
}
.gacha-result{
margin-top:10px;
display:flex;
gap:10px;
flex-wrap:wrap;
align-items:flex-start;
}
.gacha-card{
width: 220px;
background:#fff;
border:1px solid #ddd;
border-radius:10px;
overflow:hidden;
box-shadow: 0px 2px 4px rgba(0,0,0,0.08);
}
.gacha-card img{ width:100%; display:block; }
.gacha-card .p{ padding:10px; }
</style>
</head>
<body>
<!-- ===============================
ヘッダー
=============================== -->
<header>
<h1>Aran Red Fantasy - Ultimate</h1>
</header>
<!-- ===============================
ナビゲーション
=============================== -->
<nav>
<div class="container">
<a href="#" id="home-link" onclick="showPage('home')">Home</a>
<a href="#" id="quests-link" onclick="showPage('quests')">Quests</a>
<a href="#" id="items-link" onclick="showPage('items')">Items</a>
<a href="#" id="friends-link" onclick="showPage('friends')">Companions</a>
<a href="#" id="character-link" onclick="showPage('character')">Character</a>
<a href="#" id="art-link" onclick="showPage('art')">Art Gallery</a>
<a href="#" id="battle-link" onclick="showPage('battle')">Battle</a>
<a href="#" id="store-link" onclick="showPage('store')">Store</a>
<a href="#" id="craft-link" onclick="showPage('craft')">Craft</a>
<a href="#" id="skills-link" onclick="showPage('skills')">Skills</a>
<a href="#" id="questlog-link" onclick="showPage('questlog')">QuestLog</a>
<a href="#" id="achievements-link" onclick="showPage('achievements')">Achievements</a>
</div>
</nav>
<!-- ===============================
メインコンテンツ
=============================== -->
<main>
<div class="container" id="content">
<!-- ===============================
Home
=============================== -->
<div id="home">
<h2>Welcome to Aran Red Fantasy!</h2>
<p>Explore the world, complete quests, craft items, recruit companions, and unlock achievements!</p>
<div id="home-message"></div>
<button class="button" onclick="showLoginModal()">Log In / Change User</button>
<button class="button" onclick="logout()">Logout (Reset All Data)</button>
<br/><br/>
<!-- BGM(継続再生対応:audioはページ外に置く) -->
<div class="card">
<h3>BGM</h3>
<p class="muted">※ 最初の1回だけ「Enable BGM」を押してください(ブラウザの自動再生ブロック対策)。以後はページ切替しても継続します。</p>
<button class="button" id="bgm-enable-btn" onclick="enableBGM()">Enable BGM (First Click)</button>
<button class="button" onclick="toggleMusic()">Toggle Music</button>
<div class="muted" id="bgm-status">Status: Off</div>
</div>
<!-- ★ガチャ(SSR/UR追加) -->
<div class="card">
<h3>Art Gacha</h3>
<div class="gacha-row">
<div class="muted">
Cost: <strong>10 Gold</strong> / pull<br/>
SSR/URが出ます。引いたアートは自動で所持になり、アートギャラリーに追加されます。
</div>
<div>
<button class="button" onclick="pullArtGacha(1)">Pull x1</button>
<button class="button" onclick="pullArtGacha(10)">Pull x10</button>
</div>
</div>
<div class="muted" id="gacha-status">—</div>
<div class="gacha-result" id="gacha-result"></div>
</div>
<!-- ランダムイベント/天候表示 -->
<div id="weather-display"></div>
<button class="button" onclick="triggerRandomEvent()">Check Random Event</button>
<!-- 昼夜サイクル -->
<div id="day-night-display"></div>
<button class="button" onclick="advanceTime()">Pass Time (+6h)</button>
<!-- 宿屋で休息 -->
<h3>Inn</h3>
<button class="button" onclick="restAtInn()">Rest at Inn (10 Gold)</button>
<!-- ロケーション移動 -->
<div id="location-section" class="card">
<h3>Locations</h3>
<div id="location-buttons">
<button class="button" onclick="moveLocation('Town')">Move to Town</button>
<button class="button" onclick="moveLocation('Forest')">Move to Forest</button>
<button class="button" onclick="moveLocation('Dungeon')">Move to Dungeon</button>
<button class="button" onclick="moveLocation('Mountain')">Move to Mountain</button>
</div>
<p>Current Location: <span id="current-location">Town</span></p>
<div class="log" id="location-log"></div>
</div>
</div>
<!-- ===============================
Quests
=============================== -->
<div id="quests" style="display: none;">
<h2>Quests</h2>
<h3>Main Quests</h3>
<div class="card" id="dragon-quest">
<h4>Defeat the Dragon</h4>
<p>A fierce dragon has appeared near the village! Defeat it to save the locals.</p>
<div class="progress-bar">
<div class="progress" id="dragon-progress"></div>
</div>
<p>Reward: 100 Gold, 100 XP, Dragon Scale</p>
<button class="button" onclick="startQuest('dragon')">Start Quest</button>
</div>
<div class="card" id="final-quest" style="display: none;">
<h4>The Ancient Evil (Final)</h4>
<p>The final threat emerges after you've proven your strength! Vanquish it!</p>
<div class="progress-bar">
<div class="progress" id="final-progress"></div>
</div>
<p>Reward: 200 Gold, 200 XP, Legendary Relic</p>
<button class="button" onclick="startQuest('final')">Start Quest</button>
</div>
<h3>Side Quests</h3>
<div class="card" id="crystal-quest">
<h4>Collect Magic Crystals</h4>
<p>Gather magical crystals scattered around the forest. Watch out for monsters!</p>
<div class="progress-bar">
<div class="progress" id="crystal-progress"></div>
</div>
<p>Reward: 50 Gold, 50 XP, Magic Crystal</p>
<button class="button" onclick="startQuest('crystal')">Start Quest</button>
</div>
<div class="card" id="orc-quest">
<h4>Eliminate the Orc Bandits</h4>
<p>A group of orc bandits is attacking travelers. Defeat them to restore peace!</p>
<div class="progress-bar">
<div class="progress" id="orc-progress"></div>
</div>
<p>Reward: 80 Gold, 70 XP, Orc Tusk</p>
<button class="button" onclick="startQuest('orc')">Start Quest</button>
</div>
</div>
<!-- ===============================
Items
=============================== -->
<div id="items" style="display: none;">
<h2>Inventory</h2>
<p>Click an item to use/equip/sell it (if applicable).</p>
<div id="inventory"></div>
</div>
<!-- ===============================
Companions
=============================== -->
<div id="friends" style="display: none;">
<h2>Companions</h2>
<p>Hire companions who fight alongside you!</p>
<input type="text" id="friendName" placeholder="Companion name" />
<button class="button" onclick="hireCompanion()">Hire Companion</button>
<h3>Your Companions</h3>
<ul id="companion-list"></ul>
<p class="muted">* Each companion has its own level, HP, and Attack. They also gain XP when you do.</p>
</div>
<!-- ===============================
Character
=============================== -->
<div id="character" style="display: none;">
<h2>Character</h2>
<img src="a.png" alt="Character" id="character-image"/>
<p>Name: <span id="character-name"></span></p>
<p>Level: <span id="character-level"></span></p>
<p>HP: <span id="character-hp"></span> / <span id="character-maxhp"></span></p>
<p>XP: <span id="character-xp"></span> / <span id="character-nextLevelXp"></span></p>
<p>Gold: <span id="character-gold"></span></p>
<p>Attack: <span id="character-attack"></span></p>
<p>Defense: <span id="character-defense"></span></p>
<p>Skill Points: <span id="character-skillpoints"></span></p>
<p>Active Buffs/Debuffs: <span id="character-buffs">None</span></p>
<p>Special Items: <span id="character-items">None</span></p>
<p class="muted">Portrait changes when you buy or pull an art (Store / Gacha).</p>
</div>
<!-- ===============================
Art Gallery
=============================== -->
<div id="art" style="display: none;">
<h2>アートギャラリー</h2>
<p class="muted">所持済みのアートは「Set as Character Art」でキャラクター画像に設定できます。ガチャでも入手できます。</p>
<div class="card">
<h3>Your Art Collection</h3>
<div id="art-collection-summary" class="muted"></div>
</div>
<div class="gallery-grid" id="art-gallery-grid"></div>
</div>
<!-- ===============================
Battle
=============================== -->
<div id="battle" style="display: none;">
<h2>Battle Arena</h2>
<p>Choose an enemy to fight or wait for random encounters in the wild!</p>
<div class="battle-container">
<div class="enemy-card">
<h3>Slime</h3>
<p>HP: 30</p>
<p>Attack: 1-3</p>
<p>Reward: 10 Gold, 10 XP</p>
<button class="button" onclick="startBattle('slime')">Fight Slime</button>
</div>
<div class="enemy-card">
<h3>Goblin</h3>
<p>HP: 50</p>
<p>Attack: 2-5</p>
<p>Reward: 20 Gold, 20 XP</p>
<button class="button" onclick="startBattle('goblin')">Fight Goblin</button>
</div>
<div class="enemy-card">
<h3>Orc Warrior</h3>
<p>HP: 80</p>
<p>Attack: 5-8</p>
<p>Reward: 40 Gold, 40 XP</p>
<button class="button" onclick="startBattle('orcEnemy')">Fight Orc</button>
</div>
</div>
<div class="log" id="battle-log"></div>
</div>
<!-- ===============================
Store
=============================== -->
<div id="store" style="display: none;">
<h2>Store</h2>
<p>Use your gold to purchase or sell items!</p>
<div class="card">
<h3>Buy Items</h3>
<div>
<h4>Minor Health Potion (20 Gold)</h4>
<button class="button" onclick="buyItem('Minor Health Potion')">Buy</button>
</div>
<div>
<h4>Major Health Potion (50 Gold)</h4>
<button class="button" onclick="buyItem('Major Health Potion')">Buy</button>
</div>
<div>
<h4>Iron Sword (80 Gold)</h4>
<button class="button" onclick="buyItem('Iron Sword')">Buy</button>
</div>
<div>
<h4>Steel Armor (100 Gold)</h4>
<button class="button" onclick="buyItem('Steel Armor')">Buy</button>
</div>
<div>
<h4>Lucky Ring (120 Gold)</h4>
<button class="button" onclick="buyItem('Lucky Ring')">Buy</button>
</div>
</div>
<!-- アート購入:購入するとキャラクター絵が変わる -->
<div class="card">
<h3>Art Shop(購入でキャラクター画像が変わる)</h3>
<p class="muted">Buy an art → it becomes “Owned” and you can set it anytime. (Gacha also adds Owned.)</p>
<div id="art-shop-list"></div>
</div>
<div class="card">
<h3>Sell Items</h3>
<p>Click an item in your inventory to sell it, if possible.</p>
<p class="muted">(You can't sell special quest items or currently equipped gear.)</p>
</div>
</div>
<!-- ===============================
Craft
=============================== -->
<div id="craft" style="display: none;">
<h2>Item Crafting</h2>
<p>Combine items to create something new!</p>
<div class="card">
<h3>Example Recipes</h3>
<ul>
<li>Dragon Scale + Orc Tusk => Dragon Tusk Lance (Weapon)</li>
<li>Magic Crystal + Magic Crystal => Greater Crystal (Special)</li>
</ul>
<p>Select any two items from your inventory to craft (if a valid recipe exists).</p>
</div>
<p>Currently Selected: <span id="craft-selection">None</span></p>
<button class="button" id="craft-button" onclick="attemptCraft()" disabled>Craft</button>
</div>
<!-- ===============================
Skills
=============================== -->
<div id="skills" style="display: none;">
<h2>Skills</h2>
<p>Use skill points to learn or upgrade skills!</p>
<p>You have <span id="skill-point-display"></span> skill points.</p>
<ul class="skill-list" id="skill-list"></ul>
</div>
<!-- ===============================
QuestLog
=============================== -->
<div id="questlog" style="display: none;">
<h2>Quest Log</h2>
<ul id="quest-log-list"></ul>
</div>
<!-- ===============================
Achievements
=============================== -->
<div id="achievements" style="display: none;">
<h2>Achievements</h2>
<ul id="achievement-list"></ul>
</div>
</div>
</main>
<!-- ===============================
フッター
=============================== -->
<footer>
<div class="container">
© 2025 Aran Red Fantasy
</div>
</footer>
<!-- ===============================
ログインモーダル
=============================== -->
<div class="modal-bg" id="login-modal-bg" style="display: none;">
<div class="modal">
<h2>Enter Your Name</h2>
<input type="text" id="loginName" placeholder="Your name" />
<br/><br/>
<button class="button" onclick="confirmLogin()">Login</button>
<button class="button" onclick="closeLoginModal()">Cancel</button>
</div>
</div>
<!-- ===============================
アイテム使用モーダル
=============================== -->
<div class="modal-bg" id="item-modal-bg" style="display: none;">
<div class="modal">
<h2 id="item-modal-title">Use/Equip Item</h2>
<p id="item-modal-description"></p>
<button class="button" onclick="confirmItemUse()">Use/Equip</button>
<button class="button" onclick="closeItemModal()">Cancel</button>
</div>
</div>
<!-- ===============================
アートプレビューモーダル
=============================== -->
<div class="modal-bg" id="art-modal-bg" style="display: none;">
<div class="modal">
<h2 id="art-modal-title">Art Preview</h2>
<img id="art-modal-img" alt="Art Preview" />
<p class="muted" id="art-modal-desc"></p>
<div style="margin-top:10px;">
<button class="button" id="art-modal-set-btn" onclick="confirmSetPortrait()">Set as Character Art</button>
<button class="button" onclick="closeArtModal()">Close</button>
</div>
</div>
</div>
<!-- ===============================
BGM本体(継続再生のためページ切替の外に置く)
=============================== -->
<audio id="bgm" loop preload="auto" playsinline>
<source src="http://tyosuke20xx.com/fjordnosundakaze.mp3" type="audio/mpeg">
</audio>
<!-- ===============================
JavaScript
=============================== -->
<script>
// -------------------------------------------
// ページ切り替え
// -------------------------------------------
function showPage(page) {
const pages = [
"home", "quests", "items", "friends", "character",
"art",
"battle", "store", "craft", "skills", "questlog", "achievements"
];
pages.forEach(p => {
const pageElement = document.getElementById(p);
const linkElement = document.getElementById(p + '-link');
if (!pageElement) return;
if (p === page) {
pageElement.style.display = "block";
if (linkElement) linkElement.classList.add("active");
} else {
pageElement.style.display = "none";
if (linkElement) linkElement.classList.remove("active");
}
});
if (page === "skills") refreshSkillList();
if (page === "questlog") updateQuestLog();
if (page === "achievements") updateAchievementList();
if (page === "art") renderArtGallery();
if (page === "store") renderArtShop();
}
// -------------------------------------------
// ローカルストレージキー
// -------------------------------------------
const LS_KEY_USER = "ARF_Username_Ultimate";
const LS_KEY_CHARACTER = "ARF_Character_Ultimate";
const LS_KEY_INVENTORY = "ARF_Inventory_Ultimate";
const LS_KEY_COMPANIONS = "ARF_Companions_Ultimate";
const LS_KEY_QUESTS = "ARF_Quests_Ultimate";
const LS_KEY_SKILLS = "ARF_Skills_Ultimate";
const LS_KEY_DAYTIME = "ARF_Daytime_Ultimate";
const LS_KEY_WEATHER = "ARF_Weather_Ultimate";
const LS_KEY_ACHIEVEMENT = "ARF_Achievement_Ultimate";
// BGM状態
const LS_KEY_BGM = "ARF_BGM_STATE_Ultimate";
// -------------------------------------------
// ★アート定義(SSR1〜SSR3 + UR1〜UR10)
// -------------------------------------------
const ART_LIST = [
// SSR(追加)
{ key:"SSR1", name:"SSR Art 1", url:"http://tyosuke20xx.com/SSR1.png", cost: 20, rarity:"SSR" },
{ key:"SSR2", name:"SSR Art 2", url:"http://tyosuke20xx.com/SSR2.png", cost: 20, rarity:"SSR" },
{ key:"SSR3", name:"SSR Art 3", url:"http://tyosuke20xx.com/SSR3.png", cost: 20, rarity:"SSR" },
// UR
{ key:"UR1", name:"UR Art 1", url:"http://tyosuke20xx.com/UR1.png", cost: 30, rarity:"UR" },
{ key:"UR2", name:"UR Art 2", url:"http://tyosuke20xx.com/UR2.png", cost: 30, rarity:"UR" },
{ key:"UR3", name:"UR Art 3", url:"http://tyosuke20xx.com/UR3.png", cost: 30, rarity:"UR" },
{ key:"UR4", name:"UR Art 4", url:"http://tyosuke20xx.com/UR4.png", cost: 30, rarity:"UR" },
{ key:"UR5", name:"UR Art 5", url:"http://tyosuke20xx.com/UR5.png", cost: 30, rarity:"UR" },
{ key:"UR6", name:"UR Art 6", url:"http://tyosuke20xx.com/UR6.png", cost: 30, rarity:"UR" },
{ key:"UR7", name:"UR Art 7", url:"http://tyosuke20xx.com/UR7.png", cost: 30, rarity:"UR" },
{ key:"UR8", name:"UR Art 8", url:"http://tyosuke20xx.com/UR8.png", cost: 30, rarity:"UR" },
{ key:"UR9", name:"UR Art 9", url:"http://tyosuke20xx.com/UR9.png", cost: 30, rarity:"UR" },
{ key:"UR10", name:"UR Art 10", url:"http://tyosuke20xx.com/UR10.png", cost: 30, rarity:"UR" }
];
// -------------------------------------------
// ★ガチャ設定(SSR/UR抽選)
// -------------------------------------------
const GACHA_COST = 10; // 1回10G
const GACHA_RATE_UR = 10; // UR 10%
const GACHA_RATE_SSR = 90; // SSR 90%(残り)
// -------------------------------------------
// キャラクター情報
// -------------------------------------------
let character = {
name: "Adventurer",
level: 1,
hp: 50,
maxHp: 50,
xp: 0,
nextLevelXp: 100,
gold: 0,
attack: 5,
defense: 2,
skillPoints: 0,
location: "Town",
specialItems: [],
buffs: [],
ownedArtKeys: [],
portraitUrl: "a.png"
};
// -------------------------------------------
// クエスト情報
// -------------------------------------------
let mainQuests = {
dragon: {
name: "Defeat the Dragon",
progress: 0,
reward: { gold: 100, xp: 100, items: ["Dragon Scale"] },
isRunning: false,
isCompleted: false,
unlockNext: "final",
locked: false
},
final: {
name: "The Ancient Evil",
progress: 0,
reward: { gold: 200, xp: 200, items: ["Legendary Relic"] },
isRunning: false,
isCompleted: false,
unlockNext: null,
locked: true
}
};
let sideQuests = {
crystal: {
name: "Collect Magic Crystals",
progress: 0,
reward: { gold: 50, xp: 50, items: ["Magic Crystal"] },
isRunning: false,
isCompleted: false,
unlockNext: null,
locked: false
},
orc: {
name: "Eliminate the Orc Bandits",
progress: 0,
reward: { gold: 80, xp: 70, items: ["Orc Tusk"] },
isRunning: false,
isCompleted: false,
unlockNext: null,
locked: false
}
};
function getAllQuests() {
return { ...mainQuests, ...sideQuests };
}
// -------------------------------------------
// インベントリ
// -------------------------------------------
let inventory = [];
// -------------------------------------------
// 仲間
// -------------------------------------------
let companions = [];
// -------------------------------------------
// スキル
// -------------------------------------------
let skills = {
Fireball: {
name: "Fireball",
level: 0,
maxLevel: 3,
cost: 1,
description: "Deal extra magic damage in battle"
},
Heal: {
name: "Heal",
level: 0,
maxLevel: 3,
cost: 1,
description: "Restores some HP at the start of battle"
}
};
// -------------------------------------------
// バトル用エネミー
// -------------------------------------------
const enemies = {
slime: {
name: "Slime",
hp: 30,
attackMin: 1,
attackMax: 3,
rewardGold: 10,
rewardXp: 10
},
goblin: {
name: "Goblin",
hp: 50,
attackMin: 2,
attackMax: 5,
rewardGold: 20,
rewardXp: 20
},
orcEnemy: {
name: "Orc Warrior",
hp: 80,
attackMin: 5,
attackMax: 8,
rewardGold: 40,
rewardXp: 40
}
};
// -------------------------------------------
// ストアアイテム
// -------------------------------------------
const storeItems = {
"Minor Health Potion": { name: "Minor Health Potion", type: "potion", heal: 20, cost: 20 },
"Major Health Potion": { name: "Major Health Potion", type: "potion", heal: 50, cost: 50 },
"Iron Sword": { name: "Iron Sword", type: "weapon", attack: 5, cost: 80, equipped: false },
"Steel Armor": { name: "Steel Armor", type: "armor", defense: 5, cost: 100, equipped: false },
"Lucky Ring": { name: "Lucky Ring", type: "accessory", attack: 1, defense: 1, cost: 120, equipped: false }
};
// -------------------------------------------
// クラフト用レシピ
// -------------------------------------------
const craftRecipes = [
{
components: ["Dragon Scale", "Orc Tusk"].sort(),
result: { name: "Dragon Tusk Lance", type: "weapon", attack: 10, equipped: false }
},
{
components: ["Magic Crystal", "Magic Crystal"].sort(),
result: { name: "Greater Crystal", type: "special" }
}
];
// -------------------------------------------
// 昼夜 & 天候
// -------------------------------------------
let currentHour = 12;
let currentWeather = "Sunny";
const possibleWeathers = ["Sunny","Rainy","Storm","Cloudy"];
// -------------------------------------------
// 実績
// -------------------------------------------
let achievements = {
firstKill: { name: "First Blood", description: "Defeat your first enemy.", isUnlocked: false },
level5: { name: "Rising Hero", description: "Reach Level 5.", isUnlocked: false },
quest3: { name: "Quest Hunter", description: "Complete 3 Quests.", isUnlocked: false }
};
// -------------------------------------------
// onload
// -------------------------------------------
window.onload = function() {
loadLocalData();
// BGM(継続再生 & 状態保存)
loadBgmState();
wireBgmAutoSave();
updateEnableBtn();
setBgmStatus(isMusicPlaying ? "On (will resume)" : "Off");
resumeBgmOnNextUserActionIfNeeded();
updateCharacterInfo();
updateQuestVisibility();
updateInventoryDisplay();
updateCompanionList();
updateDayNightDisplay();
updateWeatherDisplay();
updateAchievementList();
renderArtShop();
renderArtGallery();
showPage('home');
document.getElementById("current-location").textContent = character.location;
};
// -------------------------------------------
// ローカルストレージ: 読込/保存/リセット
// -------------------------------------------
function loadLocalData() {
let storedName = localStorage.getItem(LS_KEY_USER);
if (storedName) character.name = storedName;
let storedChar = localStorage.getItem(LS_KEY_CHARACTER);
if (storedChar) {
try {
const parsed = JSON.parse(storedChar);
character = { ...character, ...parsed };
} catch(e) {}
}
if (!Array.isArray(character.ownedArtKeys)) character.ownedArtKeys = [];
if (!character.portraitUrl) character.portraitUrl = "a.png";
let storedInv = localStorage.getItem(LS_KEY_INVENTORY);
if (storedInv) { try { inventory = JSON.parse(storedInv); } catch(e) {} }
let storedComp = localStorage.getItem(LS_KEY_COMPANIONS);
if (storedComp) { try { companions = JSON.parse(storedComp); } catch(e) {} }
let storedMQ = localStorage.getItem(LS_KEY_QUESTS+"_main");
if (storedMQ) { try { mainQuests = JSON.parse(storedMQ); } catch(e) {} }
let storedSQ = localStorage.getItem(LS_KEY_QUESTS+"_side");
if (storedSQ) { try { sideQuests = JSON.parse(storedSQ); } catch(e) {} }
let storedSkills = localStorage.getItem(LS_KEY_SKILLS);
if (storedSkills) { try { skills = JSON.parse(storedSkills); } catch(e) {} }
let storedHour = localStorage.getItem(LS_KEY_DAYTIME+"_hour");
if (storedHour) currentHour = parseInt(storedHour, 10);
let storedWeather = localStorage.getItem(LS_KEY_WEATHER);
if (storedWeather) currentWeather = storedWeather;
let storedAchv = localStorage.getItem(LS_KEY_ACHIEVEMENT);
if (storedAchv) { try { achievements = JSON.parse(storedAchv); } catch(e) {} }
}
function saveLocalData() {
localStorage.setItem(LS_KEY_USER, character.name);
localStorage.setItem(LS_KEY_CHARACTER, JSON.stringify(character));
localStorage.setItem(LS_KEY_INVENTORY, JSON.stringify(inventory));
localStorage.setItem(LS_KEY_COMPANIONS, JSON.stringify(companions));
localStorage.setItem(LS_KEY_QUESTS+"_main", JSON.stringify(mainQuests));
localStorage.setItem(LS_KEY_QUESTS+"_side", JSON.stringify(sideQuests));
localStorage.setItem(LS_KEY_SKILLS, JSON.stringify(skills));
localStorage.setItem(LS_KEY_DAYTIME+"_hour", currentHour.toString());
localStorage.setItem(LS_KEY_WEATHER, currentWeather);
localStorage.setItem(LS_KEY_ACHIEVEMENT, JSON.stringify(achievements));
}
function logout() {
if (!confirm("All data will be cleared. Are you sure?")) return;
localStorage.clear();
location.reload();
}
// -------------------------------------------
// ログインモーダル
// -------------------------------------------
function showLoginModal() {
document.getElementById("login-modal-bg").style.display = "flex";
}
function closeLoginModal() {
document.getElementById("login-modal-bg").style.display = "none";
}
function confirmLogin() {
const inputName = document.getElementById("loginName").value.trim();
if (inputName) {
character.name = inputName;
saveLocalData();
updateCharacterInfo();
}
closeLoginModal();
}
// -------------------------------------------
// キャラクター情報表示更新
// -------------------------------------------
function updateCharacterInfo() {
document.getElementById("character-name").textContent = character.name;
document.getElementById("character-level").textContent = character.level;
document.getElementById("character-hp").textContent = character.hp;
document.getElementById("character-maxhp").textContent = character.maxHp;
document.getElementById("character-xp").textContent = character.xp;
document.getElementById("character-nextLevelXp").textContent = character.nextLevelXp;
document.getElementById("character-gold").textContent = character.gold;
document.getElementById("character-attack").textContent = character.attack;
document.getElementById("character-defense").textContent = character.defense;
document.getElementById("character-skillpoints").textContent = character.skillPoints;
const img = document.getElementById("character-image");
if (img) img.src = character.portraitUrl || "a.png";
if (character.specialItems.length > 0) {
document.getElementById("character-items").textContent = character.specialItems.join(", ");
} else {
document.getElementById("character-items").textContent = "None";
}
if (character.buffs.length > 0) {
document.getElementById("character-buffs").textContent = character.buffs.map(b => b.name).join(", ");
} else {
document.getElementById("character-buffs").textContent = "None";
}
saveLocalData();
checkAchievements();
}
// -------------------------------------------
// レベルアップ
// -------------------------------------------
function addXp(amount) {
character.xp += amount;
while (character.xp >= character.nextLevelXp) {
character.level++;
character.xp -= character.nextLevelXp;
character.nextLevelXp = character.level * 100;
character.maxHp += 20;
character.hp = character.maxHp;
character.attack += 1;
character.defense += 1;
character.skillPoints += 1;
showHomeMessage(`Level up! Now Level ${character.level} (+1 Skill Point).`);
for (let c of companions) {
c.level++;
c.hp = c.maxHp;
c.attack++;
}
}
updateCharacterInfo();
}
// -------------------------------------------
// バフ/デバフ
// -------------------------------------------
function addBuff(buffObj) {
character.buffs.push(buffObj);
updateCharacterInfo();
}
function processBuffsEachTurn(logElm) {
for (let i = character.buffs.length - 1; i >= 0; i--) {
const b = character.buffs[i];
if (b.effectType === "dot") {
character.hp -= b.effectValue;
if (character.hp < 0) character.hp = 0;
logMessage(logElm, `[${b.name}] You take ${b.effectValue} damage! (HP: ${character.hp})`);
}
b.turns--;
if (b.turns <= 0) {
logMessage(logElm, `[${b.name}] effect ended.`);
character.buffs.splice(i, 1);
}
}
}
// -------------------------------------------
// ホームメッセージ
// -------------------------------------------
function showHomeMessage(msg) {
const homeMessage = document.getElementById('home-message');
homeMessage.innerHTML = `<div class="message">${msg}</div>`;
}
// -------------------------------------------
// 昼夜
// -------------------------------------------
function updateDayNightDisplay() {
let dnElm = document.getElementById("day-night-display");
let hourStr = (currentHour < 10) ? "0"+currentHour : currentHour;
let isNight = (currentHour >= 18 || currentHour < 6);
let dayNight = isNight ? "Night" : "Day";
dnElm.innerHTML = `<p>Time: ${hourStr}:00 (${dayNight})</p>`;
}
function advanceTime() {
currentHour += 6;
if (currentHour >= 24) currentHour -= 24;
saveLocalData();
updateDayNightDisplay();
showHomeMessage("Time passes by...");
}
// -------------------------------------------
// 天候
// -------------------------------------------
function updateWeatherDisplay() {
const wElm = document.getElementById("weather-display");
wElm.innerHTML = `<p>Weather: ${currentWeather}</p>`;
}
function changeWeatherRandom() {
currentWeather = possibleWeathers[Math.floor(Math.random() * possibleWeathers.length)];
updateWeatherDisplay();
saveLocalData();
}
// -------------------------------------------
// ランダムイベント
// -------------------------------------------
function triggerRandomEvent() {
const randomRoll = Math.random();
let msg = "";
if (randomRoll < 0.2) {
msg = "A traveling merchant appears, offering rare goods (not yet implemented).";
} else if (randomRoll < 0.4) {
changeWeatherRandom();
msg = `The weather suddenly changes to ${currentWeather}!`;
} else if (randomRoll < 0.6) {
addBuff({ name: 'Poison', turns: 3, effectType: 'dot', effectValue: 3 });
msg = "You stepped on a poisonous trap! You are now poisoned.";
} else {
msg = "Nothing special happens.";
}
showHomeMessage(msg);
}
// -------------------------------------------
// 宿屋
// -------------------------------------------
function restAtInn() {
if (character.gold < 10) {
showHomeMessage("Not enough gold to rest at the inn!");
return;
}
character.gold -= 10;
character.hp = character.maxHp;
for (let c of companions) c.hp = c.maxHp;
showHomeMessage("You and your companions rest at the inn and recover full HP.");
updateCharacterInfo();
}
// -------------------------------------------
// ロケーション移動 + ランダムエンカウント
// -------------------------------------------
function moveLocation(newLocation) {
character.location = newLocation;
document.getElementById("current-location").textContent = newLocation;
saveLocalData();
const logElm = document.getElementById('location-log');
logElm.textContent = `You moved to ${newLocation}.`;
let encounterChance = 0;
if (newLocation === "Town") encounterChance = 0;
else if (newLocation === "Forest") encounterChance = 40;
else if (newLocation === "Dungeon") encounterChance = 70;
else if (newLocation === "Mountain") encounterChance = 50;
const roll = Math.random() * 100;
if (roll < encounterChance) {
const enemyKeys = Object.keys(enemies);
const randEnemyKey = enemyKeys[Math.floor(Math.random() * enemyKeys.length)];
logElm.textContent += `\nA wild ${enemies[randEnemyKey].name} appears!`;
startBattle(randEnemyKey);
}
}
// -------------------------------------------
// 仲間の雇用
// -------------------------------------------
function hireCompanion() {
const input = document.getElementById('friendName');
let name = input.value.trim();
if (!name) return;
let newCompanion = { name, level: 1, hp: 30, maxHp: 30, attack: 2 };
companions.push(newCompanion);
input.value = "";
updateCompanionList();
saveLocalData();
showHomeMessage(`${name} joined your party!`);
}
function updateCompanionList() {
const listElm = document.getElementById('companion-list');
listElm.innerHTML = "";
companions.forEach(c => {
const li = document.createElement("li");
li.textContent = `${c.name} (Lv ${c.level}, HP ${c.hp}/${c.maxHp}, ATK ${c.attack})`;
listElm.appendChild(li);
});
}
// -------------------------------------------
// インベントリ表示
// -------------------------------------------
let selectedItemIndex = null;
let selectedForCraft = [];
function updateInventoryDisplay() {
const invElm = document.getElementById('inventory');
invElm.innerHTML = "";
if (inventory.length === 0) {
invElm.innerHTML = "<p>Your inventory is empty.</p>";
return;
}
inventory.forEach((item, index) => {
const div = document.createElement("div");
div.className = "inventory-item";
div.textContent = item.name;
if (item.equipped) div.style.border = "2px solid #4CAF50";
div.onclick = () => onInventoryItemClick(index);
invElm.appendChild(div);
});
}
function onInventoryItemClick(index) {
if (document.getElementById("craft").style.display === "block") {
toggleCraftSelection(index);
return;
}
selectedItemIndex = index;
const item = inventory[index];
const modalTitle = document.getElementById("item-modal-title");
const modalDesc = document.getElementById("item-modal-description");
if (item.type === "potion") {
modalTitle.textContent = `Use ${item.name}?`;
modalDesc.textContent = `This potion restores ${item.heal} HP.`;
} else if (item.type === "weapon") {
modalTitle.textContent = `Equip ${item.name}?`;
modalDesc.textContent = `Weapon (+${item.attack} Attack).`;
} else if (item.type === "armor") {
modalTitle.textContent = `Equip ${item.name}?`;
modalDesc.textContent = `Armor (+${item.defense} Defense).`;
} else if (item.type === "accessory") {
modalTitle.textContent = `Equip ${item.name}?`;
modalDesc.textContent = `Accessory (+${item.attack} ATK, +${item.defense} DEF).`;
} else {
modalTitle.textContent = item.name;
modalDesc.textContent = "A special item. No direct use/equip.";
}
if (canSellItem(item)) {
modalDesc.textContent += `\n(Sell price: ${sellPrice(item)} Gold)`;
}
document.getElementById("item-modal-bg").style.display = "flex";
}
function closeItemModal() {
document.getElementById("item-modal-bg").style.display = "none";
selectedItemIndex = null;
}
function confirmItemUse() {
if (selectedItemIndex === null) return;
const item = inventory[selectedItemIndex];
if (item.type === "potion") {
character.hp += item.heal;
if (character.hp > character.maxHp) character.hp = character.maxHp;
inventory.splice(selectedItemIndex, 1);
showHomeMessage(`${item.name} used! You recovered ${item.heal} HP.`);
}
else if (item.type === "weapon") {
unequipItem("weapon");
item.equipped = true;
character.attack += item.attack;
showHomeMessage(`${item.name} equipped. (+${item.attack} Attack)`);
}
else if (item.type === "armor") {
unequipItem("armor");
item.equipped = true;
character.defense += item.defense;
showHomeMessage(`${item.name} equipped. (+${item.defense} Defense)`);
}
else if (item.type === "accessory") {
unequipItem("accessory");
item.equipped = true;
character.attack += item.attack;
character.defense += item.defense;
showHomeMessage(`${item.name} equipped. (+${item.attack} ATK, +${item.defense} DEF)`);
}
else {
if (canSellItem(item)) {
let price = sellPrice(item);
character.gold += price;
inventory.splice(selectedItemIndex, 1);
showHomeMessage(`You sold ${item.name} for ${price} Gold.`);
} else {
showHomeMessage(`You can't use ${item.name} right now.`);
}
}
updateCharacterInfo();
updateInventoryDisplay();
closeItemModal();
}
function unequipItem(type) {
for (let i = 0; i < inventory.length; i++) {
let it = inventory[i];
if (it.type === type && it.equipped) {
it.equipped = false;
if (type === "weapon") character.attack -= it.attack;
else if (type === "armor") character.defense -= it.defense;
else if (type === "accessory") { character.attack -= it.attack; character.defense -= it.defense; }
}
}
}
function canSellItem(item) {
if (item.equipped) return false;
if (item.type === "special") return false;
return !["weapon","armor","accessory","potion"].includes(item.type) ? true : false;
}
function sellPrice(item) { return 30; }
// -------------------------------------------
// クエストUI
// -------------------------------------------
function updateQuestVisibility() {
const finalQuestCard = document.getElementById("final-quest");
finalQuestCard.style.display = (!mainQuests.final.locked) ? "block" : "none";
}
function startQuest(questKey) {
let q = mainQuests[questKey] || sideQuests[questKey];
if (!q) return;
if (q.isRunning || q.isCompleted) return;
if (q.locked) {
showHomeMessage("This quest is locked. Complete the previous quest first!");
return;
}
q.isRunning = true;
q.progress = 0;
updateProgressBar(questKey);
let progressInterval = setInterval(() => {
q.progress += 5;
if (q.progress > 100) q.progress = 100;
updateProgressBar(questKey);
if (q.progress === 100) {
clearInterval(progressInterval);
completeQuest(questKey);
}
}, 400);
}
function completeQuest(questKey) {
let q = mainQuests[questKey] || sideQuests[questKey];
q.isRunning = false;
q.isCompleted = true;
character.gold += q.reward.gold;
addXp(q.reward.xp);
q.reward.items.forEach(it => character.specialItems.push(it));
showHomeMessage(`${q.name} completed! You got ${q.reward.gold} Gold, ${q.reward.xp} XP, and ${q.reward.items.join(", ")}.`);
if (q.unlockNext) {
if (mainQuests[q.unlockNext]) mainQuests[q.unlockNext].locked = false;
else if (sideQuests[q.unlockNext]) sideQuests[q.unlockNext].locked = false;
}
updateQuestVisibility();
updateCharacterInfo();
saveLocalData();
}
function updateProgressBar(questKey) {
const bar = document.getElementById(questKey + '-progress');
let q = mainQuests[questKey] || sideQuests[questKey];
if (bar) bar.style.width = q.progress + '%';
}
// -------------------------------------------
// バトル
// -------------------------------------------
function startBattle(enemyKey) {
const enemyDef = enemies[enemyKey];
if (!enemyDef) return;
const logElm = document.getElementById('battle-log');
logElm.innerHTML = `A wild ${enemyDef.name} appears! (HP: ${enemyDef.hp})`;
if (skills.Heal.level > 0) {
const healAmount = skills.Heal.level * 10;
character.hp += healAmount;
if (character.hp > character.maxHp) character.hp = character.maxHp;
logMessage(logElm, `[Skill: Heal Lv${skills.Heal.level}] You healed ${healAmount} HP!`);
updateCharacterInfo();
}
let enemyHp = enemyDef.hp;
let battleInterval = setInterval(() => {
processBuffsEachTurn(logElm);
if (character.hp <= 0) {
clearInterval(battleInterval);
logMessage(logElm, "You have been defeated...");
saveLocalData();
return;
}
let baseDamage = getRandomInt(character.attack - 2, character.attack + 2);
if (baseDamage < 1) baseDamage = 1;
if (skills.Fireball.level > 0) {
let extra = skills.Fireball.level * 2;
baseDamage += extra;
logMessage(logElm, `[Fireball Lv${skills.Fireball.level}] Extra ${extra} magic damage!`);
}
let totalCompanionDamage = 0;
companions.forEach(c => { if (c.hp > 0) totalCompanionDamage += c.attack; });
let totalDamage = baseDamage + totalCompanionDamage;
enemyHp -= totalDamage;
logMessage(logElm, `You (and companions) deal ${totalDamage} damage! (Enemy HP: ${Math.max(enemyHp, 0)})`);
if (enemyHp <= 0) {
clearInterval(battleInterval);
logMessage(logElm, `You defeated the ${enemyDef.name}!`);
character.gold += enemyDef.rewardGold;
addXp(enemyDef.rewardXp);
updateCompanionXP(enemyDef.rewardXp);
updateCharacterInfo();
saveLocalData();
achievements.firstKill.isUnlocked = true;
updateAchievementList();
return;
}
let eAtk = getRandomInt(enemyDef.attackMin, enemyDef.attackMax);
let dmgToPlayer = eAtk - character.defense;
if (dmgToPlayer < 1) dmgToPlayer = 1;
character.hp -= dmgToPlayer;
if (character.hp < 0) character.hp = 0;
logMessage(logElm, `The ${enemyDef.name} hits you for ${dmgToPlayer}. (Your HP: ${character.hp})`);
updateCharacterInfo();
if (character.hp <= 0) {
clearInterval(battleInterval);
logMessage(logElm, "You have been defeated...");
saveLocalData();
return;
}
}, 800);
}
function logMessage(logElm, msg) {
const p = document.createElement("p");
p.textContent = msg;
logElm.appendChild(p);
logElm.scrollTop = logElm.scrollHeight;
}
function updateCompanionXP(amount) {
for (let c of companions) {
c.level += Math.floor(amount/50);
c.maxHp += 5;
c.hp = c.maxHp;
c.attack += 1;
}
updateCompanionList();
}
// -------------------------------------------
// ストア購入
// -------------------------------------------
function buyItem(itemKey) {
const itemDef = storeItems[itemKey];
if (!itemDef) return;
if (character.gold < itemDef.cost) {
showHomeMessage(`You don't have enough gold to buy ${itemDef.name}.`);
return;
}
character.gold -= itemDef.cost;
let newItem = JSON.parse(JSON.stringify(itemDef));
if (["weapon","armor","accessory"].includes(newItem.type)) newItem.equipped = false;
inventory.push(newItem);
showHomeMessage(`You bought ${newItem.name}!`);
updateCharacterInfo();
updateInventoryDisplay();
}
// -------------------------------------------
// アート所持/購入/設定
// -------------------------------------------
function isArtOwned(artKey) {
return character.ownedArtKeys.includes(artKey);
}
function grantArt(artKey, setAsPortrait=false) {
const art = ART_LIST.find(a => a.key === artKey);
if (!art) return false;
if (!isArtOwned(art.key)) {
character.ownedArtKeys.push(art.key);
}
if (setAsPortrait) {
character.portraitUrl = art.url;
}
saveLocalData();
updateCharacterInfo();
renderArtShop();
renderArtGallery();
return true;
}
function buyArt(artKey) {
const art = ART_LIST.find(a => a.key === artKey);
if (!art) return;
if (isArtOwned(art.key)) {
showHomeMessage(`You already own ${art.name}.`);
return;
}
if (character.gold < art.cost) {
showHomeMessage(`Not enough gold to buy ${art.name}. Need ${art.cost} Gold.`);
return;
}
character.gold -= art.cost;
// 購入したら即キャラ絵変更
grantArt(art.key, true);
showHomeMessage(`Purchased ${art.name}! Character portrait changed.`);
}
function setPortraitFromArt(artKey) {
const art = ART_LIST.find(a => a.key === artKey);
if (!art) return;
if (!isArtOwned(art.key)) {
showHomeMessage("You don't own this art yet. Buy it in Store or pull it from Gacha.");
return;
}
character.portraitUrl = art.url;
saveLocalData();
updateCharacterInfo();
showHomeMessage(`Character portrait set to ${art.name}.`);
}
// Storeのアート一覧描画
function renderArtShop() {
const wrap = document.getElementById("art-shop-list");
if (!wrap) return;
wrap.innerHTML = "";
ART_LIST.forEach(a => {
const owned = isArtOwned(a.key);
const rarityBadge = a.rarity === "UR"
? `<span class="badge rarity-ur">UR</span>`
: `<span class="badge rarity-ssr">SSR</span>`;
const row = document.createElement("div");
row.style.marginBottom = "10px";
row.innerHTML = `
<div style="display:flex; align-items:center; justify-content:space-between; gap:10px; flex-wrap:wrap;">
<div>
<strong>${a.name}</strong>
${rarityBadge}
<span class="muted">(${a.cost} Gold)</span>
${owned ? `<span class="badge owned">Owned</span>` : `<span class="badge">Not Owned</span>`}
</div>
<div>
<button class="button" onclick="buyArt('${a.key}')" ${owned ? 'disabled class="button disabled"' : ''} ${owned ? 'disabled' : ''}>Buy</button>
<button class="button" onclick="openArtModal('${a.key}')">Preview</button>
</div>
</div>
`;
wrap.appendChild(row);
});
}
// ギャラリー描画
function renderArtGallery() {
const grid = document.getElementById("art-gallery-grid");
const sum = document.getElementById("art-collection-summary");
if (!grid || !sum) return;
const ownedCount = character.ownedArtKeys.length;
sum.textContent = `Owned: ${ownedCount} / ${ART_LIST.length}`;
grid.innerHTML = "";
ART_LIST.forEach(a => {
const owned = isArtOwned(a.key);
const item = document.createElement("div");
item.className = "gallery-item";
const img = document.createElement("img");
img.src = a.url;
img.alt = a.name;
img.loading = "lazy";
img.style.cursor = "pointer";
img.onclick = () => openArtModal(a.key);
const meta = document.createElement("div");
meta.className = "gallery-meta";
meta.innerHTML = `
<div>
<strong>${a.name}</strong><br/>
<span class="muted">${a.cost} Gold</span>
</div>
<div style="display:flex; gap:6px; align-items:center;">
${a.rarity === "UR"
? `<span class="badge rarity-ur">UR</span>`
: `<span class="badge rarity-ssr">SSR</span>`
}
${owned ? `<span class="badge owned">Owned</span>` : `<span class="badge">Not Owned</span>`}
</div>
`;
const actions = document.createElement("div");
actions.className = "gallery-actions";
const btnPreview = document.createElement("button");
btnPreview.className = "button";
btnPreview.textContent = "Preview";
btnPreview.onclick = () => openArtModal(a.key);
const btnSet = document.createElement("button");
btnSet.className = "button";
btnSet.textContent = "Set as Character Art";
btnSet.disabled = !owned;
if (!owned) btnSet.classList.add("disabled");
btnSet.onclick = () => setPortraitFromArt(a.key);
actions.appendChild(btnPreview);
actions.appendChild(btnSet);
item.appendChild(img);
item.appendChild(meta);
item.appendChild(actions);
grid.appendChild(item);
});
}
// -------------------------------------------
// ★ガチャ(SSR1〜SSR3/UR1〜UR10から抽選)
// - 引いたアートはOwnedに追加
// - 1枚目だけは演出的にキャラ絵も即変更(setAsPortrait=true)
// -------------------------------------------
function pullArtGacha(times) {
const totalCost = GACHA_COST * times;
const status = document.getElementById("gacha-status");
const resultWrap = document.getElementById("gacha-result");
resultWrap.innerHTML = "";
if (character.gold < totalCost) {
status.textContent = `Not enough gold. Need ${totalCost} Gold.`;
showHomeMessage(`Not enough gold for gacha. Need ${totalCost} Gold.`);
return;
}
character.gold -= totalCost;
let pulled = [];
for (let i=0; i<times; i++) {
const art = rollOneArt();
pulled.push(art);
// 1枚目だけ即ポートレート変更(継続仕様)
grantArt(art.key, i === 0);
}
updateCharacterInfo();
status.textContent = `Pulled ${times} time(s). Cost ${totalCost} Gold.`;
// 表示
pulled.forEach((a, idx) => {
const card = document.createElement("div");
card.className = "gacha-card";
card.innerHTML = `
<img src="${a.url}" alt="${a.name}">
<div class="p">
<strong>${a.name}</strong><br/>
<span class="muted">${a.rarity}</span>
${isArtOwned(a.key) ? `<span class="badge owned" style="margin-left:6px;">Owned</span>` : ``}
</div>
`;
card.onclick = () => openArtModal(a.key);
resultWrap.appendChild(card);
});
showHomeMessage(`Gacha result: ${pulled.map(a => a.rarity + " " + a.key).join(", ")}`);
saveLocalData();
renderArtShop();
renderArtGallery();
}
function rollOneArt() {
const r = Math.random() * 100;
let rarity = (r < GACHA_RATE_UR) ? "UR" : "SSR";
const pool = ART_LIST.filter(a => a.rarity === rarity);
// 念のため
if (pool.length === 0) return ART_LIST[Math.floor(Math.random() * ART_LIST.length)];
return pool[Math.floor(Math.random() * pool.length)];
}
// -------------------------------------------
// アートプレビュー・モーダル
// -------------------------------------------
let pendingArtKey = null;
function openArtModal(artKey) {
const art = ART_LIST.find(a => a.key === artKey);
if (!art) return;
pendingArtKey = artKey;
document.getElementById("art-modal-title").textContent = `${art.name} (${art.rarity})`;
document.getElementById("art-modal-img").src = art.url;
const owned = isArtOwned(art.key);
document.getElementById("art-modal-desc").textContent =
owned ? "Owned: You can set this as your character art." : "Not owned: Buy it in Store or pull it from Gacha.";
const setBtn = document.getElementById("art-modal-set-btn");
setBtn.disabled = !owned;
if (!owned) setBtn.classList.add("disabled");
else setBtn.classList.remove("disabled");
document.getElementById("art-modal-bg").style.display = "flex";
}
function closeArtModal() {
document.getElementById("art-modal-bg").style.display = "none";
pendingArtKey = null;
}
function confirmSetPortrait() {
if (!pendingArtKey) return;
setPortraitFromArt(pendingArtKey);
closeArtModal();
}
// -------------------------------------------
// クラフト関連
// -------------------------------------------
function toggleCraftSelection(invIndex) {
const item = inventory[invIndex];
if (selectedForCraft.includes(invIndex)) {
selectedForCraft = selectedForCraft.filter(i => i !== invIndex);
} else {
if (selectedForCraft.length >= 2) {
showHomeMessage("You can only select up to 2 items for crafting.");
return;
}
selectedForCraft.push(invIndex);
}
updateCraftSelectionDisplay();
}
function updateCraftSelectionDisplay() {
let names = selectedForCraft.map(i => inventory[i].name);
if (names.length === 0) names.push("None");
document.getElementById("craft-selection").textContent = names.join(" & ");
document.getElementById("craft-button").disabled = (selectedForCraft.length < 2);
}
function attemptCraft() {
if (selectedForCraft.length < 2) return;
let itemA = inventory[selectedForCraft[0]];
let itemB = inventory[selectedForCraft[1]];
let combo = [itemA.name, itemB.name].sort();
let craftedItem = null;
for (let r of craftRecipes) {
if (r.components[0] === combo[0] && r.components[1] === combo[1]) {
craftedItem = r.result;
break;
}
}
if (!craftedItem) {
showHomeMessage("No valid recipe found for these items.");
selectedForCraft = [];
updateCraftSelectionDisplay();
return;
}
let idxA = Math.max(selectedForCraft[0], selectedForCraft[1]);
let idxB = Math.min(selectedForCraft[0], selectedForCraft[1]);
inventory.splice(idxA, 1);
inventory.splice(idxB, 1);
inventory.push(craftedItem);
showHomeMessage(`You crafted: ${craftedItem.name}!`);
selectedForCraft = [];
updateCraftSelectionDisplay();
updateInventoryDisplay();
saveLocalData();
}
// -------------------------------------------
// スキル
// -------------------------------------------
function refreshSkillList() {
document.getElementById("skill-point-display").textContent = character.skillPoints;
const listElm = document.getElementById("skill-list");
listElm.innerHTML = "";
for (let sKey in skills) {
let sk = skills[sKey];
let li = document.createElement("li");
li.innerHTML = `
<strong>${sk.name} (Lv${sk.level}/${sk.maxLevel})</strong>
- ${sk.description}
${
sk.level < sk.maxLevel
? `(<button onclick="learnSkill('${sKey}')">Upgrade (cost ${sk.cost})</button>)`
: ''
}
`;
listElm.appendChild(li);
}
}
function learnSkill(skillKey) {
let skill = skills[skillKey];
if (!skill) return;
if (skill.level >= skill.maxLevel) {
showHomeMessage(`${skill.name} is already at max level.`);
return;
}
if (character.skillPoints < skill.cost) {
showHomeMessage(`Not enough skill points to upgrade ${skill.name}.`);
return;
}
character.skillPoints -= skill.cost;
skill.level++;
showHomeMessage(`You upgraded ${skill.name} to level ${skill.level}.`);
updateCharacterInfo();
refreshSkillList();
}
// -------------------------------------------
// クエストログ
// -------------------------------------------
function updateQuestLog() {
const logElm = document.getElementById("quest-log-list");
logElm.innerHTML = "";
let allQ = getAllQuests();
for (let key in allQ) {
let q = allQ[key];
let status = q.isCompleted ? "Completed" : (q.isRunning ? "In Progress" : "Not Started");
let li = document.createElement("li");
li.textContent = `${q.name}: ${status}`;
logElm.appendChild(li);
}
}
// -------------------------------------------
// 実績
// -------------------------------------------
function checkAchievements() {
if (character.level >= 5) achievements.level5.isUnlocked = true;
let completedCount = 0;
let allQ = getAllQuests();
for (let key in allQ) if (allQ[key].isCompleted) completedCount++;
if (completedCount >= 3) achievements.quest3.isUnlocked = true;
saveLocalData();
updateAchievementList();
}
function updateAchievementList() {
const listElm = document.getElementById("achievement-list");
listElm.innerHTML = "";
for (let aKey in achievements) {
let a = achievements[aKey];
let status = a.isUnlocked ? "Unlocked" : "Locked";
let li = document.createElement("li");
li.textContent = `${a.name} - ${a.description} [${status}]`;
listElm.appendChild(li);
}
}
// -------------------------------------------
// BGM(継続再生 & 状態保存 & 復帰)
// -------------------------------------------
let isMusicPlaying = false;
let bgmUnlocked = false;
let wantAutoResume = false;
function setBgmStatus(text) {
const s = document.getElementById("bgm-status");
if (s) s.textContent = "Status: " + text;
}
function saveBgmState() {
const audio = document.getElementById("bgm");
if (!audio) return;
const state = {
unlocked: bgmUnlocked,
playing: isMusicPlaying,
volume: audio.volume,
time: audio.currentTime
};
localStorage.setItem(LS_KEY_BGM, JSON.stringify(state));
}
function loadBgmState() {
const audio = document.getElementById("bgm");
if (!audio) return;
const raw = localStorage.getItem(LS_KEY_BGM);
if (!raw) return;
try {
const st = JSON.parse(raw);
bgmUnlocked = !!st.unlocked;
isMusicPlaying = !!st.playing;
wantAutoResume = isMusicPlaying;
if (typeof st.volume === "number") audio.volume = st.volume;
if (typeof st.time === "number") {
audio.addEventListener("loadedmetadata", () => {
try { audio.currentTime = Math.max(0, st.time); } catch(e) {}
}, { once: true });
}
} catch(e) {}
}
function wireBgmAutoSave() {
const audio = document.getElementById("bgm");
if (!audio) return;
audio.addEventListener("play", () => { isMusicPlaying = true; saveBgmState(); });
audio.addEventListener("pause", () => { isMusicPlaying = false; saveBgmState(); });
audio.addEventListener("volumechange", saveBgmState);
let lastSave = 0;
audio.addEventListener("timeupdate", () => {
const now = Date.now();
if (now - lastSave > 4000) {
lastSave = now;
saveBgmState();
}
});
}
function updateEnableBtn() {
const btn = document.getElementById("bgm-enable-btn");
if (!btn) return;
if (bgmUnlocked) {
btn.textContent = "BGM Enabled";
btn.classList.add("disabled");
btn.disabled = true;
} else {
btn.textContent = "Enable BGM (First Click)";
btn.classList.remove("disabled");
btn.disabled = false;
}
}
function resumeBgmOnNextUserActionIfNeeded() {
if (!bgmUnlocked || !wantAutoResume) return;
const audio = document.getElementById("bgm");
if (!audio) return;
const resumeOnce = () => {
audio.play().then(() => {
isMusicPlaying = true;
wantAutoResume = false;
setBgmStatus("On (Resumed)");
saveBgmState();
}).catch(() => {
setBgmStatus("Blocked (Enable again)");
});
};
document.addEventListener("pointerdown", resumeOnce, { once: true });
document.addEventListener("keydown", resumeOnce, { once: true });
}
function enableBGM() {
const audio = document.getElementById("bgm");
if (!audio) return;
audio.volume = 0.6;
audio.play().then(() => {
bgmUnlocked = true;
isMusicPlaying = true;
wantAutoResume = false;
updateEnableBtn();
setBgmStatus("On");
showHomeMessage("BGM Enabled & Playing");
saveBgmState();
}).catch(() => {
setBgmStatus("Blocked (Click again)");
showHomeMessage("BGM blocked by browser. Click Enable BGM again.");
});
}
function toggleMusic() {
const audio = document.getElementById("bgm");
if (!audio) return;
if (!bgmUnlocked) {
showHomeMessage("First, click 'Enable BGM (First Click)'.");
setBgmStatus("Locked");
return;
}
if (!isMusicPlaying) {
audio.play().then(() => {
isMusicPlaying = true;
setBgmStatus("On");
showHomeMessage("Music On");
saveBgmState();
}).catch(() => {
setBgmStatus("Blocked");
showHomeMessage("Music could not be played (browser block).");
});
} else {
audio.pause();
isMusicPlaying = false;
setBgmStatus("Off");
showHomeMessage("Music Off");
saveBgmState();
}
}
// -------------------------------------------
// 汎用ランダム整数
// -------------------------------------------
function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min);
}
</script>
</body>
</html>
Apple Vision Pro2発売予想
「Apple Vision Pro 2」みたいな “本当の第2世代フラッグシップ” は、
早くても 2027〜2028 年ごろ と見ておくのが現実的。
それより前に出るのは、
- すでに発表済みの M5版 Vision Pro(マイナーチェンジ)(2025年10月発売)Lifewire
- さらにその後の 廉価版「Vision Air」(仮)(2027年量産開始見込み)UploadVR+1
という「派生モデル」で、本気の Vision Pro 2 とは少し別ラインと考えた方がいい。
1. 今の公式&有力リークの流れ
① Vision Pro (初代) → M5チップ版
- 2024年:初代 Vision Pro 発売($3,499)
- 2025年10月:M5チップ搭載のアップグレード版 Vision Pro 発表&発売
- M5で性能&AI処理大幅アップ
- バッテリー持ち向上、120Hzリフレッシュレート
- 新しい「Dual Knit Band」で装着感改善Lifewire
👉 これは 「Vision Pro 1.5」的なマイナーチェンジ。
② 廉価版ライン「Vision Air」(仮)
- アナリスト Ming-Chi Kuo などのレポートで
「Vision Air」は2027年後半に量産開始予定、Vision Proより40%以上軽く、価格も半額以下を目指す と報告。UploadVR+1 - 価格も $1,500〜$1,800 くらいを狙うと言われている(あくまで噂)。
③ 本来の「Vision Pro 2」計画
- 以前のリークでは、
- もっと軽く
- 高性能ディスプレイ
- バッテリー改善
- 価格も少し下げた Vision Pro 2 を「Vision Air の後」に出す構想があった。MacRumors+1
- しかし 2024年時点で
「Vision Pro 2 の開発一時停止」「まずは廉価版に集中」 という報道も出ている。MacRumors+1
つまり Apple の中でも
先に安いモデルで市場を広げるか?
それとも高級路線を維持するか?
という路線変更が何度も揺れている状態。
2. じゃあ「Vision Pro 2」はいつ出そう?
公開情報とリークを全部まとめて、ジョブズ風に乱暴に整理すると:
- 2025年:M5版 Vision Pro(マイナーアップデート) → これはもう発表済み。Lifewire
- 2026年:新しい XR/メガネ系ハードは出ない見込み(アナリストレポート複数が「2026年は空白」と予測)。Medium+1
- 2027年:廉価版 Vision Air が量産&発売(予定)。UploadVR+1
- そのあと:Vision Pro 2(真の第2世代)が来る
- MacRumors などは、Vision Air のあとにハイエンド Pro 2 を計画していたが、時期は2028年ごろに後ろ倒しになっていると報じている。MacRumors
なので、現実的な予想は:
Vision Pro 2 (第2世代フラッグシップ)
→ 2027年末〜2028年以降が本命
「2026年発売」という噂も一部あるけど、
- 2026年は新ヘッドセット無し予想
- Vision Air が 2027 年に控えている
この2点を考えると、2026年 Vision Pro 2 はかなり望み薄。
3. どんな進化がありそう?
現時点の予想スペック(リーク+業界予測)では:
- チップ:M4 or M5 以降(AI処理強化、Apple Intelligence 連携前提)UC Today+1
- ディスプレイ:より省電力で高輝度なOLED / micro-OLED(サプライチェーン側の情報多数)oled-info.com+1
- 重量:Vision Air と同等か、それ以上の軽量化(長時間装着を前提に)Road to VR
- 価格戦略:
- Vision Air:普及価格帯
- Vision Pro 2:ハイエンド路線維持(でも初代よりはやや下げる可能性)
4. ざっくり年表イメージ
- 2024:Vision Pro (初代)
- 2025:Vision Pro M5版(性能&快適性アップのマイナーチェンジ)
- 2026:新XR本体は出ない可能性が高い
- 2027:廉価版 Vision Air
- 2028〜:本命の Vision Pro 2(第2世代フラッグシップ)
5. ユーザー視点の「買いタイミング」
あなたの立場で考えると:
- 今すぐ体験したい
→ 2025年の M5版 Vision Pro は「開発者・クリエイター用ハイエンド機」としてはかなり完成度高いはず。 - できるだけコスパ重視+フルダイブの入口体験したい
→ 2027年の Vision Air 待ちが無難。 - 最高の完成度まで待つ派
→ 2028年以降の Vision Pro 2 をターゲットにして、今はQuestとかで遊びながら様子見。
もし「自分のVRサービス/ゲームをどの世代向けに作るか?」って視点で戦略を立てたいなら、
- 2025〜2027:Vision Pro (初代+M5) + 他社ヘッドセット向けに土台を作る期間
- 2027〜2030:Vision Air & Vision Pro 2 向けに本命のフルスケール版を投下する期間
ってロードマップで考えるのが現実的だと思う。
Tsumugi
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Tsumugi</title>
<!-- Favicon -->
<link rel="shortcut icon"
href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path fill='%23667eea' d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z'/></svg>"/>
<!-- Tailwind CSS v2 -->
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet"/>
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.4.0/css/all.min.css"/>
<style>
:root {
--grad-a: #667eea;
--grad-b: #764ba2;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: radial-gradient(circle at top left, #1f2937 0%, #111827 40%, #020617 100%);
min-height: 100vh;
color: #111827;
}
.glass-effect {
background: radial-gradient(circle at top left, rgba(255,255,255,0.15), rgba(255,255,255,0.03));
backdrop-filter: blur(18px);
border-radius: 18px;
border: 1px solid rgba(255,255,255,0.16);
}
.card-hover { transition: all 0.25s ease; }
.card-hover:hover { transform: translateY(-3px); box-shadow: 0 20px 30px -12px rgba(0,0,0,0.45); }
.gradient-text {
background: linear-gradient(90deg, var(--grad-a), var(--grad-b));
-webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;
}
.timeline-post {
background: rgba(15,23,42,0.96);
border-radius: 18px;
box-shadow: 0 14px 30px -16px rgba(0,0,0,0.7);
transition: all 0.25s ease;
border-left: 4px solid var(--grad-a);
}
.timeline-post:hover { transform: translateX(4px); }
.profile-avatar {
width: 100px; height: 100px; border-radius: 50%;
object-fit: cover; border: 3px solid rgba(255,255,255,0.9);
box-shadow: 0 10px 24px rgba(0,0,0,0.35);
}
.mini-avatar {
width: 46px; height: 46px; border-radius: 50%; object-fit: cover;
border: 2px solid rgba(255,255,255,0.9);
}
.btn-primary {
background: linear-gradient(135deg, var(--grad-a), var(--grad-b));
border: none; transition: all 0.2s ease;
}
.btn-primary:hover { transform: translateY(-1px); box-shadow: 0 10px 18px rgba(0,0,0,0.4); }
.section-divider {
height: 3px; background: linear-gradient(90deg, var(--grad-a), var(--grad-b));
border-radius: 999px; margin: 2rem 0;
}
.username-badge {
background: radial-gradient(circle at top left, var(--grad-a), var(--grad-b));
color: white; padding: 0.2rem 0.6rem; border-radius: 999px;
font-size: 0.75rem; font-weight: 600; display: inline-flex;
align-items: center; margin-left: 0.5rem;
}
.username-badge i { margin-right: 4px; }
.share-menu { position: absolute; z-index: 50; min-width: 180px; right: 0; top: 110%; background: #020617; border-radius: 12px; box-shadow: 0 18px 45px rgba(0,0,0,0.75); border: 1px solid rgba(148,163,184,0.5); }
.share-menu button { width: 100%; text-align: left; padding: 10px 20px; border: none; background: none; cursor: pointer; font-size: 0.95rem; color: #e5e7eb; }
.share-menu button:hover { background: rgba(51,65,85,0.9); }
.status-indicator { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; }
.status-active { background-color: #10b981; animation: pulse 1.5s infinite; }
.status-inactive { background-color: #6b7280; }
.log-container {
max-height: 180px; overflow-y: auto; background: rgba(15,23,42,0.85);
border-radius: 10px; padding: 10px; margin-top: 10px; font-size: 0.8rem;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
border: 1px solid rgba(148,163,184,0.5); color: #e5e7eb;
}
.error-message { color: #fecaca; background: rgba(127,29,29,0.6); padding: 8px; border-radius: 8px; margin: 5px 0; }
.success-message { color: #bbf7d0; background: rgba(6,95,70,0.6); padding: 8px; border-radius: 8px; margin: 5px 0; }
@keyframes pulse { 0%,100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.4; transform: scale(1.1); } }
.dark .glass-effect { background: rgba(15,23,42,0.92); border: 1px solid rgba(148,163,184,0.4); }
.dark .timeline-post { background: #020617; color: #f9fafb; border-left-color: #4f46e5; }
.dark .share-menu { background: #020617; color: #e5e7eb; }
.dark .success-message { background: rgba(6,95,70,0.7); }
.dark .error-message { background: rgba(127,29,29,0.7); }
.icon-label {
font-size: 0.8rem; color: #e5e7eb; text-transform: uppercase; letter-spacing: 0.05em;
}
@media print {
body { background: white !important; -webkit-print-color-adjust: exact; }
.glass-effect { background: white !important; backdrop-filter: none !important; border: 1px solid #e5e7eb !important; }
.timeline-post { background: white !important; color: #111827 !important; }
}
</style>
</head>
<body class="dark text-gray-100">
<!-- ログイン/登録モーダル -->
<div id="auth-modal" class="fixed inset-0 bg-black bg-opacity-60 flex items-center justify-center z-50" style="display:none">
<div class="bg-gray-900 rounded-2xl shadow-2xl w-full max-w-sm p-6 border border-gray-700">
<h2 class="text-2xl font-bold mb-4 text-center gradient-text" id="auth-title">ログイン</h2>
<div id="auth-error" class="error-message mb-2" style="display:none"></div>
<form id="auth-form" autocomplete="off">
<div class="mb-3">
<label class="block mb-1 text-xs font-semibold text-gray-300">メールアドレス</label>
<input type="email" id="auth-email" class="w-full border border-gray-700 bg-gray-800 rounded px-3 py-2 text-sm text-gray-100" required>
</div>
<div class="mb-3">
<label class="block mb-1 text-xs font-semibold text-gray-300">パスワード</label>
<input type="password" id="auth-password" class="w-full border border-gray-700 bg-gray-800 rounded px-3 py-2 text-sm text-gray-100" required>
</div>
<button type="submit" class="btn-primary w-full py-2 rounded-lg text-white font-semibold text-sm mt-2">
<i class="fas fa-sign-in-alt mr-2"></i>ログイン
</button>
</form>
<div class="mt-4 text-center">
<button id="toggle-auth-mode" class="text-indigo-400 underline text-xs">新規登録はこちら</button>
</div>
</div>
</div>
<!-- ヘッダー -->
<header class="glass-effect mx-4 mt-4 p-6 border border-indigo-500/40">
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between space-y-4 lg:space-y-0">
<div class="text-left">
<h1 class="text-3xl md:text-4xl font-extrabold text-white mb-1 tracking-tight">
<i class="fas fa-comments mr-3 text-indigo-300"></i>
<span class="gradient-text">Tsumugi</span>
<span class="ml-2 text-xs px-2 py-1 rounded-full bg-indigo-500/20 border border-indigo-400/60 align-middle">Verse Core v3.0</span>
</h1>
<p class="text-indigo-100 text-sm md:text-base opacity-90">
次世代ソーシャルネットワーク • RSS / BOT 専用エディション(AI機能なし)
</p>
</div>
<div class="flex flex-wrap items-center justify-end gap-3">
<div class="flex items-center bg-slate-900/70 rounded-2xl px-3 py-2 shadow-inner border border-slate-700">
<img id="header-profile-icon" class="mini-avatar" src="https://via.placeholder.com/80" alt="プロフィール">
<div class="ml-3 text-left">
<div class="font-semibold text-sm" id="header-username">未設定</div>
<div class="text-xs text-slate-300 opacity-75" id="header-user-email"></div>
</div>
</div>
<button onclick="toggleDarkMode()" class="btn-primary px-4 py-2 rounded-full text-white text-xs flex items-center">
<i class="fas fa-moon mr-2"></i><span>テーマ切替</span>
</button>
<button onclick="showSystemStatus()" class="bg-slate-900 hover:bg-slate-800 px-4 py-2 rounded-full text-white text-xs border border-slate-600 flex items-center">
<i class="fas fa-info-circle mr-2"></i>ステータス
</button>
<button onclick="clearVerseCache()" class="bg-yellow-400 hover:bg-yellow-500 px-4 py-2 rounded-full text-black text-xs flex items-center">
<i class="fas fa-broom mr-2"></i>キャッシュクリア
</button>
<button id="logout-btn" onclick="logout()" class="bg-red-600 hover:bg-red-700 px-4 py-2 rounded-full text-white text-xs flex items-center hidden">
<i class="fas fa-sign-out-alt mr-2"></i>ログアウト
</button>
</div>
</div>
</header>
<!-- メイン -->
<div class="max-w-6xl mx-auto px-4 py-6" id="main-content" style="display:none">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- 左カラム:プロフィール/BOT/RSS -->
<div class="lg:col-span-1 space-y-6">
<!-- プロフィール -->
<div class="glass-effect p-6 card-hover border border-slate-600">
<h3 class="text-2xl font-bold gradient-text mb-4 flex items-center">
<i class="fas fa-user-circle mr-2 text-indigo-300"></i>プロフィール
</h3>
<div class="text-center mb-6">
<img id="profile-icon" class="profile-avatar mx-auto mb-4" src="https://via.placeholder.com/100" alt="プロフィール">
<input type="file" id="profile-upload" accept="image/*" onchange="uploadProfileIcon(event)" class="hidden">
<button onclick="document.getElementById('profile-upload').click()" class="btn-primary px-4 py-2 rounded-full text-white text-sm">
<i class="fas fa-camera mr-2"></i>プロフィール画像
</button>
</div>
<div class="space-y-4">
<div>
<label class="block text-gray-200 font-semibold mb-1 text-xs">ユーザー名</label>
<input type="text" id="username" class="w-full p-3 border border-slate-600 rounded-lg bg-slate-900 text-gray-100 text-sm" placeholder="ユーザー名を入力" maxlength="20">
</div>
<div>
<label class="block text-gray-200 font-semibold mb-1 text-xs">自己紹介</label>
<textarea id="self-intro" class="w-full p-3 border border-slate-600 rounded-lg bg-slate-900 text-gray-100 text-sm" rows="4" placeholder="自己紹介を入力"></textarea>
</div>
<button onclick="saveProfile()" class="btn-primary w-full py-2 rounded-lg text-white text-sm font-semibold">
<i class="fas fa-save mr-2"></i>プロフィール保存
</button>
<div class="p-3 bg-slate-900/80 rounded-lg border border-slate-700">
<h5 class="font-semibold text-gray-200 mb-2 text-xs">プレビュー</h5>
<div class="text-gray-300 text-sm">
<div class="font-semibold mb-1" id="username-preview">未設定</div>
<div id="self-intro-preview" class="text-xs whitespace-pre-line min-h-8">まだ自己紹介がありません</div>
</div>
</div>
</div>
</div>
<!-- BOT/Feed アイコン設定 -->
<div class="glass-effect p-6 card-hover border border-indigo-500/40">
<h3 class="text-xl font-bold text-indigo-100 mb-4 flex items-center">
<i class="fas fa-icons mr-2 text-indigo-300"></i>BOT / Feed アイコン設定
</h3>
<div class="space-y-4 text-xs">
<div class="flex items-center space-x-3">
<img id="bot-icon-preview" class="mini-avatar" src="https://cdn-icons-png.flaticon.com/512/4712/4712109.png" alt="BOT">
<div class="flex-1">
<div class="icon-label">BOT / Markov BOT</div>
<input type="file" id="bot-icon-upload" accept="image/*" class="hidden" onchange="uploadIcon('bot', event)">
<button onclick="document.getElementById('bot-icon-upload').click()" class="bg-slate-900 hover:bg-slate-800 px-3 py-1 rounded-full text-gray-100 text-[11px] border border-slate-600 mt-1">
<i class="fas fa-robot mr-1"></i>BOTアイコン変更
</button>
</div>
</div>
<div class="flex items-center space-x-3">
<img id="feed-icon-preview" class="mini-avatar" src="https://cdn-icons-png.flaticon.com/512/3416/3416046.png" alt="Feed">
<div class="flex-1">
<div class="icon-label">RSS FEED BOT</div>
<input type="file" id="feed-icon-upload" accept="image/*" class="hidden" onchange="uploadIcon('feed', event)">
<button onclick="document.getElementById('feed-icon-upload').click()" class="bg-slate-900 hover:bg-slate-800 px-3 py-1 rounded-full text-gray-100 text-[11px] border border-slate-600 mt-1">
<i class="fas fa-rss mr-1"></i>Feedアイコン変更
</button>
</div>
</div>
</div>
</div>
<!-- RSS自動投稿機能 -->
<div class="glass-effect p-6 card-hover border border-amber-500/40">
<h3 class="text-xl font-bold text-amber-100 mb-4">
<i class="fas fa-rss mr-2 text-amber-300"></i>RSS自動投稿(全体共有)
<span class="status-indicator" id="rss-status"></span>
<span id="rss-status-text" class="text-xs opacity-75">停止中</span>
</h3>
<div>
<input id="rss-url" type="text" class="w-full p-2 border border-slate-600 rounded-lg bg-slate-900 text-gray-100 text-xs mb-2" placeholder="RSSフィードURLを入力">
<button onclick="addRssFeed()" class="btn-primary w-full py-2 rounded-lg text-white mb-2 text-xs">
<i class="fas fa-plus mr-2"></i>追加
</button>
<div id="rss-list" class="mb-3 text-xs"></div>
<div class="flex items-center space-x-2 mb-2">
<input type="number" id="rss-interval" class="w-1/2 p-2 border border-slate-600 rounded-lg bg-slate-900 text-gray-100 text-xs" min="10" max="3600" value="300" placeholder="間隔(秒)">
<button onclick="setRssInterval()" class="btn-primary flex-1 py-2 rounded-lg text-white text-xs">
<i class="fas fa-clock mr-2"></i>間隔設定
</button>
</div>
<div class="flex items-center space-x-2 mb-2">
<button onclick="fetchRssNow()" class="btn-primary flex-1 py-2 rounded-lg text-white text-xs">
<i class="fas fa-sync mr-2"></i>今すぐ取得
</button>
<button onclick="stopRssAuto()" class="bg-red-600 hover:bg-red-700 flex-1 py-2 rounded-lg text-white text-xs">
<i class="fas fa-stop mr-2"></i>自動停止
</button>
</div>
<div class="flex items-center space-x-2 text-xs">
<button onclick="setAllRssEnabled(true)" class="btn-primary flex-1 py-2 rounded-lg text-white text-[11px]">
<i class="fas fa-toggle-on mr-1"></i>すべてON
</button>
<button onclick="setAllRssEnabled(false)" class="bg-gray-600 hover:bg-gray-700 flex-1 py-2 rounded-lg text-white text-[11px]">
<i class="fas fa-toggle-off mr-1"></i>すべてOFF
</button>
</div>
<div id="rss-log" class="log-container text-xs mt-3"></div>
</div>
</div>
<!-- BOT機能 -->
<div class="glass-effect p-6 card-hover border border-emerald-500/40">
<h3 class="text-xl font-bold text-emerald-100 mb-4">
<i class="fas fa-robot mr-2 text-emerald-300"></i>BOT機能
<span class="status-indicator" id="bot-status"></span>
<span id="bot-status-text" class="text-xs opacity-75">停止中</span>
</h3>
<div class="space-y-4">
<div>
<textarea id="botContent" class="w-full p-3 border border-slate-600 rounded-lg bg-slate-900 text-gray-100 text-sm" rows="3" placeholder="BOT投稿内容"></textarea>
<button onclick="postBotMessage()" class="btn-primary w-full mt-2 py-2 rounded-lg text-white text-xs">
<i class="fas fa-robot mr-2"></i>BOT投稿
</button>
</div>
<div>
<input type="number" id="botIntervalSec" class="w-full p-2 border border-slate-600 rounded-lg bg-slate-900 text-gray-100 text-xs" placeholder="マルコフ自動投稿間隔(秒)" min="10" max="3600" value="60">
<div class="flex space-x-2 mt-2">
<button onclick="postMarkovBot()" class="btn-primary flex-1 py-2 rounded-lg text-white text-xs">
<i class="fas fa-dice mr-2"></i>マルコフ生成
</button>
<button onclick="startBotAutoPost()" class="btn-primary flex-1 py-2 rounded-lg text-white text-xs">
<i class="fas fa-play mr-2"></i>自動開始
</button>
<button onclick="stopBotAutoPost()" class="bg-red-600 hover:bg-red-700 flex-1 py-2 rounded-lg text-white text-xs">
<i class="fas fa-stop mr-2"></i>停止
</button>
</div>
</div>
<div class="text-emerald-100 text-xs opacity-80">
<i class="fas fa-info-circle mr-1"></i>
マルコフ連鎖ではユーザー/BOT投稿のみを学習し、RSS記事本文は学習対象から除外します。
</div>
<div id="bot-log" class="log-container text-xs"></div>
</div>
</div>
</div>
<!-- 右カラム:投稿&タイムライン -->
<div class="lg:col-span-2 space-y-6">
<!-- 新規投稿 -->
<div class="glass-effect p-6 card-hover border border-slate-600">
<h3 class="text-2xl font-bold gradient-text mb-4 flex items-center">
<i class="fas fa-edit mr-2 text-indigo-300"></i>新規投稿
</h3>
<div>
<textarea id="postContent" class="w-full p-4 border border-slate-600 rounded-lg bg-slate-900 text-gray-100 text-sm" rows="4" placeholder="今何を考えていますか?(Ctrl+Enter で投稿)" maxlength="500"></textarea>
<div class="mt-4 flex flex-col md:flex-row md:items-center md:justify-between space-y-3 md:space-y-0">
<div class="text-indigo-100 text-xs opacity-90 flex items-center space-x-2">
<i class="fas fa-info-circle"></i>
<span>あなたの思いを共有しましょう</span>
<span id="char-count" class="ml-2 px-2 py-1 rounded-full bg-slate-900 border border-slate-600">(0/500)</span>
</div>
<div class="flex space-x-2">
<button onclick="createUserPost()" class="btn-primary px-5 py-2 rounded-lg text-white text-xs font-semibold">
<i class="fas fa-paper-plane mr-2"></i>投稿する
</button>
</div>
</div>
</div>
</div>
<div class="section-divider"></div>
<!-- タイムライン -->
<div class="glass-effect p-6 border border-slate-600">
<div class="flex flex-col md:flex-row md:justify-between md:items-center mb-4 space-y-3 md:space-y-0">
<h3 class="text-2xl font-bold text-indigo-100 flex items-center">
<i class="fas fa-stream mr-2 text-indigo-300"></i>タイムライン
<span id="post-count" class="text-sm font-normal opacity-75 ml-2">(0件の投稿)</span>
</h3>
<div class="flex space-x-2">
<button onclick="clearAllPosts()" class="bg-red-600 hover:bg-red-700 px-3 py-1 rounded text-white text-xs flex items-center">
<i class="fas fa-trash mr-1"></i>全削除
</button>
<button onclick="exportData()" class="bg-emerald-600 hover:bg-emerald-700 px-3 py-1 rounded text-white text-xs flex items-center">
<i class="fas fa-download mr-1"></i>エクスポート
</button>
</div>
</div>
<!-- フィルター&検索 -->
<div class="flex flex-col md:flex-row md:items-center md:justify-between mb-4 space-y-3 md:space-y-0">
<div class="flex flex-wrap gap-2 text-xs">
<button id="filter-all" onclick="setFilter('all')" class="px-3 py-1 rounded-full border border-slate-600 bg-indigo-600 text-white flex items-center">
<i class="fas fa-globe mr-1"></i>すべて
</button>
<button id="filter-user" onclick="setFilter('user')" class="px-3 py-1 rounded-full border border-slate-600 text-slate-200 flex items-center">
<i class="fas fa-user mr-1"></i>ユーザー
</button>
<button id="filter-bot" onclick="setFilter('bot')" class="px-3 py-1 rounded-full border border-slate-600 text-slate-200 flex items-center">
<i class="fas fa-robot mr-1"></i>BOT
</button>
<button id="filter-feed" onclick="setFilter('feed')" class="px-3 py-1 rounded-full border border-slate-600 text-slate-200 flex items-center">
<i class="fas fa-rss mr-1"></i>Feed
</button>
</div>
<div class="relative w-full md:w-64">
<input id="timeline-search" type="text" class="w-full pl-8 pr-3 py-2 rounded-full bg-slate-900 border border-slate-600 text-xs text-slate-100" placeholder="キーワード検索(本文・ユーザー名)">
<i class="fas fa-search text-slate-400 text-xs absolute left-2.5 top-1/2 transform -translate-y-1/2"></i>
</div>
</div>
<div id="timeline" class="space-y-4"></div>
<div id="empty-timeline" class="text-center py-12 text-slate-200 opacity-80">
<i class="fas fa-comments text-4xl mb-4 text-indigo-300"></i>
<p class="text-lg">まだ投稿がありません</p>
<p class="text-xs text-slate-300">最初の投稿をして、タイムラインを始めましょう!</p>
</div>
</div>
</div>
</div>
</div>
<footer class="glass-effect mx-4 mb-4 p-4 text-center border border-slate-700">
<p class="text-slate-200 opacity-80 text-xs">
<i class="fas fa-copyright mr-1"></i>
2025 Verse – 次世代ソーシャルネットワーク v3.0
<span class="ml-4 inline-flex items-center">
<i class="fas fa-rss mr-1 text-amber-300"></i>共有RSS / 個別ON/OFF / BOT・マルコフ自動投稿
</span>
</p>
</footer>
<script>
// ==== 初期RSS ====
const PRESET_RSS = [
"http://2ch-2.net/rss/all.xml",
"http://2ch-ranking.net/rss/livemarket1.rdf",
"http://2ch-ranking.net/rss/livemarket2.rdf",
"http://kabumatome.doorblog.jp/index.rdf",
"http://momoniji.com/feed",
"http://oekakigakusyuu.blog97.fc2.com/?xml",
"http://otanews.livedoor.biz/atom.xml",
"http://otanews.livedoor.biz/index.rdf",
"http://news4vip.livedoor.biz/index.rdf",
"http://news.kakaku.com/prdnews/rss.asp",
"http://www.jma-net.go.jp/rss/jma.rss",
"http://rss.asahi.com/rss/asahi/newsheadlines.rdf",
"https://uploadvr.com/feed/",
"http://www.atmarkit.co.jp/rss/rss2dc.xml",
"http://liginc.co.jp/feed",
"http://liginc.co.jp/feed/",
"http://blog.livedoor.jp/shachiani/index.rdf",
"http://manga.lemon-s.com/atom.xml",
"http://b.hatena.ne.jp/search/text?safe=on&q=%E3%82%BB%E3%82%AD%E3%83%A5%E3%83%AA%E3%83%86%E3%82%A3&users=500&mode=rss",
"http://creator-life.net/feed/",
"http://feedblog.ameba.jp/rss/ameblo/ca-1pixel/rss20.xml",
"http://rssblog.ameba.jp/ca-1pixel/rss20.xml",
"http://weekly.ascii.jp/cate/1/rss.xml",
"http://blog.livedoor.jp/coleblog/atom.xml",
"http://2chantena.antenam.biz/rss1s.rss",
"http://www.4gamer.net/rss/index.xml",
"http://www.4gamer.net/rss/news_topics.xml",
"http://nyan.eggtree.net/feed.xml",
"http://nunnnunn.hatenablog.com/rss",
"http://www.nikkansports.com/general/atom.xml",
"http://feeds.afpbb.com/rss/afpbb/access_ranking",
"http://akiba-pc.watch.impress.co.jp/cda/rss/akiba-pc.rdf",
"https://area.autodesk.jp/rss.xml",
"http://av.watch.impress.co.jp/sublink/av.rdf",
"http://rss.allabout.co.jp/aa/latest/ch/netpc/",
"http://www.ar-ch.org/atom.xml",
"http://feeds.arstechnica.com/arstechnica/BAaf",
"https://feeds.feedburner.com/awwwards-sites-of-the-day",
"http://news.bbc.co.uk/rss/newsonline_uk_edition/front_page/rss091.xml",
"http://www.criteo.com/blog/rss/",
"https://blueskyweb.xyz/rss.xml",
"http://boingboing.net/rss.xml",
"http://www.cc2.co.jp/blog/?feed=rss2",
"http://cgarena.com/cgarena.xml",
"http://cgtracking.net/feed",
"http://japan.cnet.com/rss/index.rdf",
"http://newclassic.jp/feed",
"https://www.cssmania.com/feed/",
"http://ceron.jp/top/?type=rss",
"http://blog.btrax.com/jp/comments/feed/",
"http://2ch.logpo.jp/1hour.xml",
"http://menthas.com/javascript/rss",
"http://www.nhk.or.jp/rss/news/cat0.xml",
"http://ozpa-h4.com/feed/",
"https://www.youtube.com/feeds/videos.xml?channel_id=UC1DCedRgGHBdm81E1llLhOQ",
"http://rass.blog43.fc2.com/?xml",
"http://stackoverflow.com/feeds",
"http://www.slideshare.net/rss/latest",
"http://www.jp.square-enix.com/whatsnew2/whatsnew.rdf",
"http://www.ituore.com/feed",
"http://synodos.jp/comments/feed",
"http://www.shinkigensha.co.jp/feed/",
"http://e-shuushuu.net/index.rss",
"http://slashdot.org/index.rss",
"http://feeds.feedburner.com/TheHackersNews?format=xml",
"http://googleblog.blogspot.com/atom.xml",
"http://www.theregister.co.uk/tonys/slashdot.rdf",
"http://thinkit.co.jp/rss.xml",
"http://blog.livedoor.jp/news23vip/atom.xml",
"http://blog.livedoor.jp/news23vip/index.rdf",
"http://www.webcreatorbox.com/feed/",
"http://web-d.navigater.info/atom.xml",
"http://2ch-c.net/?xml_all",
"http://smhn.info/feed",
"http://feeds.japan.zdnet.com/rss/zdnet/all.rdf",
"http://20kaido.com/index.rdf",
"http://2chnode.com/rss/feed/all",
"http://akiba-souken.com/feed/all/",
"http://amaebi.net/index.rdf",
"http://amakakeru.blog59.fc2.com/?xml",
"http://artskype.com/rss/feed.xml",
"http://asitagamienai.blog118.fc2.com/?xml",
"http://beta.egmnow.com/feed/",
"http://blog.livedoor.jp/ogenre/index.rdf",
"http://blog.nicovideo.jp/atom.xml",
"http://blog.tsubuani.com/feed",
"http://blogs.adobe.com/flex/atom.xml",
"http://blogs.adobe.com/index.xml",
"http://bm.s5-style.com/feed",
"http://business.nikkeibp.co.jp/rss/all_nbo.rdf",
"http://createlier.sitemix.jp/feed/",
"http://crocro.com/news/nc.cgi?action=search&skin=rdf_srch_xml",
"http://d.hatena.ne.jp/thk/rss",
"http://damage0.blomaga.jp/index.rdf",
"http://danbooru.donmai.us/posts.atom",
"http://danbooru.donmai.us/posts.atom?tags=rss",
"http://dengekionline.com/cate/11/rss.xml",
"http://dictionary.reference.com/wordoftheday/wotd.rss",
"http://doujin-games88.net/feed",
"http://doujin.sekurosu.com/rss",
"http://dousyoko.blog.fc2.com/?xml",
"http://eroaniblog.blog.fc2.com/?xml",
"http://eroanimedougakan.blog.fc2.com/?xml",
"http://erogetrailers.com/api?md=latest",
"http://eronizimage.blog.fc2.com/?xml",
"http://erosanime.blog121.fc2.com/?xml",
"http://erotaganime.blog.fc2.com/?xml",
"http://feed.nikkeibp.co.jp/rss/nikkeibp/index.rdf",
"http://feed.rssad.jp/rss/gigazine/rss_2.0",
"http://feed.rssad.jp/rss/jcast/index.xml",
"http://feed.rssad.jp/rss/klug/fxnews/rss5.xml",
"http://feedblog.ameba.jp/rss/ameblo/yusayusa0211/rss20.xml",
"http://feeds.adobe.com/xml/rss.cfm?query=byMostRecent&languages=1",
"http://feeds.builder.japan.zdnet.com/rss/builder/all.rdf",
"http://feeds.fc2.com/fc2/xml?host=anrism.blog&format=xml",
"http://feeds.fc2.com/fc2/xml?host=kahouha2jigen.blog&format=xml",
"http://feeds.feedburner.com/gekiura",
"http://feeds.journal.mycom.co.jp/rss/mycom/index",
"http://feeds.reuters.com/reuters/JPTopNews?format=xml",
"http://galten705.blog.fc2.com/?xml",
"http://gamanjiru.net/feed",
"http://gamanjiru.net/feed/atom",
"http://gamebiz.jp/?feed=rss",
"http://gamenode.jp/rss/feed/all",
"http://ggsoku.com/feed/atom/",
"http://girlcelly.blog.fc2.com/?xml&trackback",
"http://hairana.blog.fc2.com/?xml",
"http://haruka-yumenoato.net/static/rss/index.rss",
"http://headline.harikonotora.net/rss2.xml",
"http://hentaidoujinanime.com/?xml",
"http://homepage1.nifty.com/maname/index.rdf",
"http://horiemon.com/feed/",
"http://ideahacker.net/feed/",
"http://itpro.nikkeibp.co.jp/rss/develop.rdf",
"http://itpro.nikkeibp.co.jp/rss/news.rss",
"http://itpro.nikkeibp.co.jp/rss/oss.rdf",
"http://itpro.nikkeibp.co.jp/rss/win.rdf",
"http://japan.internet.com/rss/rdf/index.rdf",
"http://jp.leopard-raws.org/rss.php",
"http://jp.techcrunch.com/feed/",
"http://kakaku.com/trendnews/rss.xml",
"http://kamisoku.blog47.fc2.com/?xml",
"http://kanesoku.com/index.rdf",
"http://kibougamotenai.blog.fc2.com/?xml",
"http://kiisu.jpn.org/rss/now.xml",
"http://konachan.com/post/piclens?page=1&tags=loli",
"http://labo.tv/2chnews/index.xml",
"http://lineblog.me/yamamotoichiro/atom.xml",
"http://majimougen.blog.fc2.com/?xml",
"http://mantan-web.jp/rss/mantan.xml",
"http://matome.naver.jp/feed/hot",
"http://matome.naver.jp/feed/tech",
"http://matome.sekurosu.com/rss",
"http://mizuhonokuni2ch.com/?xml",
"http://momoiroanime.blog.fc2.com/?xml",
"http://moroahedoujin.com/?xml",
"http://nesingazou.blog.fc2.com/?xml",
"http://newnews-moe.com/index.rdf",
"http://news.ameba.jp/index.xml",
"http://news.com.com/2547-1_3-0-5.xml",
"http://news.nicovideo.jp/?rss=2.0",
"http://news.nicovideo.jp/ranking/hot?rss=2.0",
"http://newsbiz.yahoo.co.jp/topnews.rss",
"http://nijitora.blog.fc2.com/?xml",
"http://nodvd21ver2.blog.fc2.com/?xml",
"http://orebibou.com/feed/",
"http://osu.ppy.sh/feed/ranked/",
"http://otakomu.jp/feed",
"http://pcgameconquest.blog.fc2.com/?xml",
"http://picks.dir.yahoo.co.jp/dailypicks/rss/",
"http://piknik2ch.blog76.fc2.com/?xml",
"http://plus.appgiga.jp/feed/user",
"http://purisoku.com/index.rdf",
"http://rdsig.yahoo.co.jp/RV=1/RU=aHR0cDovL3NlYXJjaHJhbmtpbmcueWFob28uY28uanAvcnNzL2J1cnN0X3JhbmtpbmctcnNzLnhtbA--;_ylt=A2RhjFhfAi9XEi0A6Glhdu57",
"http://read2ch.net/rss/",
"http://rss.dailynews.yahoo.co.jp/fc/computer/rss.xml",
"http://rss.rssad.jp/rss/akibapc/akiba-pc.rdf",
"http://rss.rssad.jp/rss/ascii/biz/rss.xml",
"http://rss.rssad.jp/rss/ascii/hobby/rss.xml",
"http://rss.rssad.jp/rss/ascii/it/rss.xml",
"http://rss.rssad.jp/rss/ascii/mac/rss.xml",
"http://rss.rssad.jp/rss/ascii/pc/rss.xml",
"http://rss.rssad.jp/rss/ascii/rss.xml",
"http://rss.rssad.jp/rss/codezine/new/20/index.xml",
"http://rss.rssad.jp/rss/forest/rss.xml",
"http://rss.rssad.jp/rss/gihyo/feed/atom",
"http://rss.rssad.jp/rss/headline/headline.rdf",
"http://rss.rssad.jp/rss/impresswatch/pcwatch.rdf",
"http://rss.rssad.jp/rss/itm/1.0/makoto.xml",
"http://rss.rssad.jp/rss/itm/1.0/netlab.xml",
"http://rss.rssad.jp/rss/itm/2.0/kw_akiba.xml",
"http://rss.rssad.jp/rss/itm/2.0/kw_android_appli.xml",
"http://rss.rssad.jp/rss/itm/2.0/kw_apple.xml",
"http://rss.rssad.jp/rss/itm/2.0/kw_facebook.xml",
"http://rss.rssad.jp/rss/itm/2.0/kw_google.xml",
"http://rss.rssad.jp/rss/itm/2.0/kw_ipad.xml",
"http://rss.rssad.jp/rss/itm/2.0/kw_iphone.xml",
"http://rss.rssad.jp/rss/itm/2.0/kw_iphone_appli.xml",
"http://rss.rssad.jp/rss/itm/2.0/kw_mixi.xml",
"http://rss.rssad.jp/rss/itm/2.0/kw_smartphone.xml",
"http://rss.rssad.jp/rss/itm/2.0/kw_twitter.xml",
"http://rss.rssad.jp/rss/itm/2.0/kw_ustream.xml",
"http://rss.rssad.jp/rss/itm/2.0/kw_youtube.xml",
"http://rss.rssad.jp/rss/itmbizid/1.0/bizid.xml",
"http://rss.rssad.jp/rss/itmnews/2.0/news_bursts.xml",
"http://rss.rssad.jp/rss/japaninternetcom/index.rdf",
"http://rss.rssad.jp/rss/oshietekun/atom.xml",
"http://rss.rssad.jp/rss/slashdot/slashdot.rss",
"http://rss.rssad.jp/rss/zaikeishimbun/main.xml",
"http://rssc.dokoda.jp/r/8a1dd8f128047929ba4390dab3c8065e/http/searchranking.yahoo.co.jp/realtime_buzz/",
"http://sakurabaryo.com/feed/",
"http://sankei.jp.msn.com/rss/news/points.xml",
"http://sankei.jp.msn.com/rss/news/west_points.xml",
"http://search.goo.ne.jp/rss/newkw.rdf",
"http://sekurosu.com/rss",
"http://streaming.yahoo.co.jp/rss/newly/anime/",
"http://sub0000528116.hmk-temp.com/wordpress/?feed=rss2",
"http://sukebei.nyaa.se/?page=rss&sort=2",
"http://tenshoku.mynavi.jp/knowhow/rss.xml",
"http://tensinyakimeshi.blog98.fc2.com/?xml",
"http://thefreedom12.blog41.fc2.com/?xml",
"http://togetter.com/rss/hot/culture/62",
"http://togetter.com/rss/hot/culture/63",
"http://torimatome.main.jp/blogs/comments/feed",
"http://torimatome.main.jp/blogs/feed",
"http://toshinokyouko.com/rss.php",
"http://tvanimedouga.blog93.fc2.com/?xml",
"http://uranourainformation.blog21.fc2.com/?xml",
"http://video.fc2.com/a/feed_popular.php?m=week",
"http://weather.livedoor.com/forecast/rss/area/400010.xml",
"http://wotopi.jp/feed",
"http://www.100shiki.com/feed",
"http://www.alistapart.com/rss.xml",
"http://www.anime-sharing.com/forum/external.php?type=RSS2&forumids=36",
"http://www.anime-sharing.com/forum/external.php?type=RSS2&forumids=38",
"http://www.anime-sharing.com/forum/external.php?type=RSS2&forumids=47",
"http://www.blosxom.com/?feed=rss2",
"http://www.britannica.com/eb/dailycontent/rss",
"http://www.csmonitor.com/rss/top.rss",
"http://www.ehackingnews.com/feeds/posts/default",
"http://www.falcom.co.jp/new.xml",
"http://www.famitsu.com/rss/category/fcom_game.rdf",
"http://www.famitsu.com/rss/fcom_all.rdf",
"http://www.ganganonline.com/rss/index.xml",
"http://www.ideaxidea.com/feed",
"http://www.itnews711.com/index.rdf",
"http://www.jp.playstation.com/whatsnew/whatsnew.rdf",
"http://www.keyman.or.jp/rss/v1/?rss_type=all",
"http://www.koubo.co.jp/rss.xml",
"http://www.nyaa.se/?page=rss&sort=2",
"http://www.nyaa.se/?page=rss&user=118009",
"http://www.nytimes.com/services/xml/rss/userland/HomePage.xml",
"http://www.phianime.tv/feed/",
"http://www.rebootdevelop.hr/feed/",
"http://www.rictus.com/muchado/feed/",
"http://www.sbcr.jp/atom.xml",
"http://www.slashgear.com/comments/feed/",
"http://www.torrent-anime.com/feed",
"http://www.torrent-anime.com/feed/",
"http://www.webimemo.com/feed/",
"http://www.wired.com/news_drop/netcenter/netcenter.rdf",
"http://www.xvideos.com/rss/rss.xml",
"http://www.youtube.com/rss/user/KADOKAWAanime/videos.rss",
"http://www.youtube.com/rss/user/demosouko/videos.rss",
"http://www.yukawanet.com/index.rdf",
"http://www.zou3.net/php/rss/nikkei2rss.php?head=main",
"http://xml.ehgt.org/ehtracker.xml",
"http://xml.metafilter.com/rss.xml",
"http://xvideos.2jiero.info/feed",
"http://yaraon.blog109.fc2.com/?xml",
"http://yusaani.com/home/feed/",
"http://zipdeyaruo.blog42.fc2.com/?xml",
"http://www.portalgraphics.net/rss/latest_image_list.xml",
"http://api.syosetu.com/writernovel/430380.Atom",
"http://creive.me/feed/",
"http://gihyo.jp/dev/feed/atom",
"http://gihyo.jp/feed/rss1",
"http://hakase255.blog135.fc2.com/?xml",
"http://2ch-ranking.net/rss/zenban.rdf",
"http://www.isus.jp/feed/",
"http://www.jiji.com/rss/ranking.rdf",
"http://jp.gamesindustry.biz/rss/index.xml",
"https://www.youtube.com/feeds/videos.xml?channel_id=UCx1nAvtVDIsaGmCMSe8ofsQ",
"http://zakuzaku911.com/index.rdf",
"http://ke-tai.org/blog/feed/",
"http://data.newantenna.net/ero/rss/all.xml",
"http://developer.mixi.co.jp/feed/atom",
"http://neoneetch.blog.fc2.com/?xml",
"http://rss.itmedia.co.jp/rss/1.0/netlab.xml",
"http://netgeek.biz/feed",
"http://blog.esuteru.com/index.rdf",
"http://b.hatena.ne.jp/hotentry/game.rss",
"http://b.hatena.ne.jp/hotentry.rss",
"http://mobile.seisyun.net/rss/hot.rdf",
"http://yomi.mobi/rss/hot.rdf",
"http://saymygame.com/feed/",
"http://blog.webcreativepark.net/atom.xml",
"http://buhidoh.net/?xml",
"http://www.webcyou.com/?feed=rss2",
"http://withnews.jp/rss/consumer/new.rdf",
"https://yande.re/post/atom?tags=loli",
"http://blog.livedoor.jp/nizigami/atom.xml",
"http://nvmzaq.blog.fc2.com/?xml",
"http://keieimanga.net/index.rdf",
"http://megumi.ldblog.jp/atom.xml",
"http://kirik.tea-nifty.com/diary/index.rdf",
"http://sinri.net/comments/feed",
"http://himasoku.com/atom.xml",
"http://himasoku.com/index.rdf",
"http://20kaido.com/index.rdf",
"http://h723.blog.fc2.com/?xml",
"http://onecall2ch.com/index.rdf",
"http://www.forest.impress.co.jp/rss.xml",
"http://www.zaikei.co.jp/rss/sections/it.xml",
"http://akiba.keizai.biz/rss.xml",
"http://agag.tw/feed/2d-popular.rss",
"http://adult-vr.jp/feed/",
"http://www.anige-sokuhouvip.com/?xml",
"http://animeanime.jp/rss/index.rdf",
"http://alfalfalfa.com/index.rdf",
"http://feeds.feedburner.com/fc2/GhfA",
"http://erogetaiken072.blog.fc2.com/?xml",
"http://otanew.jp/atom.xml",
"http://jin115.com/index.rdf",
"http://www.onlinegamer.jp/rss/news.rdf",
"http://karapaia.livedoor.biz/index.rdf",
"http://getnews.jp/feed/ext/orig",
"http://www.gungho.co.jp/news/xml/rss.xml",
"http://blog.livedoor.jp/kinisoku/index.rdf",
"http://feeds.gizmodo.jp/rss/gizmodo/index.xml",
"http://himado.in/?sort=movie_id&rss=1",
"http://k-tai.impress.co.jp/cda/rss/ktai.rdf",
"http://gehasoku.com/atom.xml",
"http://feedblog.ameba.jp/rss/ameblo/principia-ca/rss20.xml",
"http://zai.diamond.jp/list/feed/rssfxnews",
"http://capacitor.blog.fc2.com/?xml",
"http://blog.livedoor.jp/vipsister23/index.rdf",
"http://vipsister23.com/atom.xml",
"http://b.hatena.ne.jp/search/tag?safe=on&q=2ch&users=500&mode=rss",
"http://b.hatena.ne.jp/search/tag?safe=on&q=%E3%83%8D%E3%83%83%E3%83%88%E3%83%AF%E3%83%BC%E3%82%AF&users=500&mode=rss",
"http://b.hatena.ne.jp/search/tag?safe=off&q=%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0&users=500&mode=rss",
"http://shikaku2ch.doorblog.jp/atom.xml",
"http://dotinstall.com/lessons.rss",
"http://2ch-ranking.net/rss/news4vip.rdf",
"http://blog.livedoor.jp/insidears/index.rdf",
"http://2ch-ranking.net/rss/newsplus.rdf",
"http://2ch-ranking.net/rss/news.rdf",
"http://nullpoantenna.com/game.rdf",
"http://workingnews.blog117.fc2.com/?xml",
"http://bm.s5-style.com/feed",
"http://2ch-ranking.net/rss/ghard.rdf",
"http://www.724685.com/blog/rss.xml",
"http://www.yukawanet.com/index.rdf",
"http://2ch-ranking.net/rss/bizplus.rdf",
"http://www.nicovideo.jp/ranking/fav/daily/all?rss=2.0&lang=ja-jp",
"http://www.tarikin.net/rss0.rdf",
"http://blog.livedoor.jp/dqnplus/index.rdf",
"http://www.seojapan.com/blog/feed",
"http://2ch-ranking.net/rss/morningcoffee.rdf",
"http://2ch-ranking.net/mt50k.rdf",
"http://rssblog.ameba.jp/yandereotto/rss20.xml",
"https://business.nikkei.com/rss/sns/nb.rdf",
"http://daredemopc.blog51.fc2.com/?xml",
"http://erogetaikenban.blog65.fc2.com/?xml",
"http://news.goo.ne.jp/rss/topstories/gootop/index.rdf",
"http://lanovelien.blog121.fc2.com/?xml",
"http://news.livedoor.com/topics/rss/eco.xml",
"http://ragnarokonline.gungho.jp/index.rdf",
"http://rocketnews24.com/feed/",
"https://news.denfaminicogamer.jp/feed",
"http://www.igda.jp/?feed=rss2",
"http://feeds.cnn.co.jp/cnn/rss"
];
// ==== アプリ状態 ====
if (!localStorage.getItem('verse_shared_rssFeeds')) {
localStorage.setItem('verse_shared_rssFeeds', JSON.stringify(PRESET_RSS));
}
let users = JSON.parse(localStorage.getItem('verse_users') || '[]');
let currentUser = JSON.parse(localStorage.getItem('verse_currentUser') || 'null');
let posts = JSON.parse(localStorage.getItem('verse_posts') || '[]');
let isDarkMode = localStorage.getItem('verse_darkMode') === 'true';
let isInitialized = false;
// BOT / RSS 状態
let botInterval = null;
let rssInterval = null;
// タイムラインフィルタ&検索
let currentFilter = 'all';
let currentSearch = '';
// 共有RSS設定
let sharedRssFeeds = JSON.parse(localStorage.getItem('verse_shared_rssFeeds') || '[]');
let sharedRssInterval = Number(localStorage.getItem('verse_shared_rssInterval')) || 300;
let sharedRssLastIds = JSON.parse(localStorage.getItem('verse_shared_rssLastIds') || '{}');
let sharedRssEnabled = JSON.parse(localStorage.getItem('verse_shared_rssEnabled') || '{}');
// アイコン設定(BOT, Feed)
let verseIcons = JSON.parse(localStorage.getItem('verse_icons') || 'null');
if (!verseIcons) {
verseIcons = {
bot: 'https://cdn-icons-png.flaticon.com/512/4712/4712109.png',
feed: 'https://cdn-icons-png.flaticon.com/512/3416/3416046.png'
};
localStorage.setItem('verse_icons', JSON.stringify(verseIcons));
}
function saveIcons() {
localStorage.setItem('verse_icons', JSON.stringify(verseIcons));
updateAllUI();
}
function uploadIcon(type, e) {
const f = e.target.files[0];
if (!f) return;
if (f.size > 5 * 1024 * 1024) { alert('5MB以下にしてください。'); return; }
const r = new FileReader();
r.onload = () => {
verseIcons[type] = r.result;
saveIcons();
alert(type.toUpperCase() + ' アイコンを更新しました');
};
r.readAsDataURL(f);
}
// ==== キャッシュクリア ====
function clearVerseCache() {
if (!confirm('Tsumugi / Verse のローカルキャッシュ(ユーザー, 投稿, RSS設定など)をすべて削除します。よろしいですか?')) return;
Object.keys(localStorage).forEach(k => {
if (k.startsWith('verse_')) localStorage.removeItem(k);
});
alert('ローカルキャッシュを削除しました。ページを再読み込みします。');
location.reload();
}
// ===== 認証UI =====
function showAuthModal(mode = 'login', errorMsg = '') {
document.getElementById('auth-title').textContent = (mode === 'register') ? '新規登録' : 'ログイン';
document.getElementById('auth-form').authMode = mode;
document.getElementById('auth-email').value = '';
document.getElementById('auth-password').value = '';
document.getElementById('auth-modal').style.display = '';
document.getElementById('main-content').style.display = 'none';
document.getElementById('auth-error').textContent = errorMsg || '';
document.getElementById('auth-error').style.display = errorMsg ? '' : 'none';
document.getElementById('toggle-auth-mode').textContent = (mode === 'register') ? 'ログインはこちら' : '新規登録はこちら';
}
function hideAuthModal() {
document.getElementById('auth-modal').style.display = 'none';
document.getElementById('main-content').style.display = '';
}
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('auth-form').onsubmit = function(e) {
e.preventDefault();
const email = document.getElementById('auth-email').value.trim().toLowerCase();
const password = document.getElementById('auth-password').value;
if (!email || !password) {
showAuthModal(this.authMode, 'メールアドレスとパスワードを入力してください');
return;
}
if (this.authMode === 'register') {
if (users.find(u => u.email === email)) {
showAuthModal('register', 'このメールアドレスは既に登録されています');
return;
}
const newUser = {
email,
password,
profile: { icon: 'https://via.placeholder.com/100', username: email.split('@')[0], selfIntro: '' }
};
users.push(newUser);
localStorage.setItem('verse_users', JSON.stringify(users));
currentUser = { email };
localStorage.setItem('verse_currentUser', JSON.stringify(currentUser));
showAuthModal('login', '登録完了!ログインしてください');
} else {
const user = users.find(u => u.email === email && u.password === password);
if (!user) { showAuthModal('login', 'メールアドレスまたはパスワードが違います'); return; }
currentUser = { email };
localStorage.setItem('verse_currentUser', JSON.stringify(currentUser));
hideAuthModal();
initializeApp();
}
};
document.getElementById('toggle-auth-mode').onclick = function() {
const mode = (document.getElementById('auth-title').textContent === '新規登録') ? 'login' : 'register';
showAuthModal(mode);
};
if (!currentUser) showAuthModal('login'); else { hideAuthModal(); initializeApp(); }
});
function logout() {
localStorage.removeItem('verse_currentUser');
currentUser = null;
stopRssAuto();
stopBotAutoPost();
showAuthModal('login');
}
// ===== 初期化 =====
function initializeApp() {
if (isInitialized) return;
if (!currentUser) { showAuthModal('login'); return; }
users = JSON.parse(localStorage.getItem('verse_users') || '[]');
posts = JSON.parse(localStorage.getItem('verse_posts') || '[]');
isDarkMode = localStorage.getItem('verse_darkMode') === 'true';
const user = users.find(u => u.email === currentUser.email);
window.profile = user ? user.profile : { icon: 'https://via.placeholder.com/100', username: 'ゲストユーザー', selfIntro: '' };
updateAllUI();
updateStatusIndicators();
updateRssUI();
if (isDarkMode) {
document.body.classList.add('dark');
} else {
document.body.classList.remove('dark');
}
document.getElementById('main-content').style.display = '';
document.getElementById('logout-btn').classList.remove('hidden');
isInitialized = true;
startRssAuto();
addLog('bot-log', 'BOT機能初期化完了', 'success');
addLog('rss-log', 'RSS自動投稿(全体共有)初期化完了', 'success');
}
// ===== 投稿(ユーザー/BOT/Feed/Markov) =====
function createUserPost() {
const ta = document.getElementById('postContent');
const txt = ta.value.trim();
if (!txt) return alert('投稿内容を入力してください。');
if (!currentUser) return alert('ログインが必要です。');
createPost(txt, 'user', profile.username, profile.icon);
ta.value = '';
updateCharCount();
}
function createPost(content, type = 'user', username = null, icon = null, extra = {}) {
if (!content || !content.trim()) return false;
let finalIcon = icon;
if (!finalIcon) {
if (type === 'bot' || type === 'markov') finalIcon = verseIcons.bot;
else if (type === 'feed') finalIcon = verseIcons.feed;
else finalIcon = profile.icon;
}
const post = {
id: Date.now() + Math.random(),
content: content.trim(),
likes: 0,
timestamp: new Date().toLocaleString('ja-JP'),
type,
username: username || profile.username,
icon: finalIcon,
userEmail: currentUser ? currentUser.email : '',
...extra
};
posts.unshift(post);
saveData();
renderTimeline();
return true;
}
function likePost(id) {
const idx = posts.findIndex(p => p.id === id);
if (idx >= 0) {
posts[idx].likes++;
saveData();
renderTimeline();
}
}
function deletePost(id) {
if (!confirm('この投稿を削除しますか?')) return;
posts = posts.filter(p => p.id !== id);
saveData();
renderTimeline();
}
function clearAllPosts() {
if (!confirm('全ての投稿を削除しますか?')) return;
posts = [];
saveData();
renderTimeline();
}
function exportData() {
const blob = new Blob([JSON.stringify(posts, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'verse_posts.json';
a.click();
URL.revokeObjectURL(url);
}
// ===== タイムライン描画 =====
function escapeHtml(s) {
return (s || '').replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"','\'':'''}[m]));
}
function renderTimeline() {
const tl = document.getElementById('timeline');
const emp = document.getElementById('empty-timeline');
const cnt = document.getElementById('post-count');
if (!tl || !emp || !cnt) return;
let displayPosts = posts.slice();
// フィルター
displayPosts = displayPosts.filter(p => {
if (currentFilter === 'user' && p.type !== 'user') return false;
if (currentFilter === 'bot' && !['bot','markov'].includes(p.type)) return false;
if (currentFilter === 'feed' && p.type !== 'feed') return false;
return true;
});
// 検索
if (currentSearch && currentSearch.trim() !== '') {
const q = currentSearch.trim().toLowerCase();
displayPosts = displayPosts.filter(p => {
const text = (p.content || '') + ' ' + (p.username || '');
return text.toLowerCase().includes(q);
});
}
if (displayPosts.length === 0) {
tl.innerHTML = '';
emp.style.display = 'block';
cnt.textContent = '(0件の投稿)';
return;
}
emp.style.display = 'none';
cnt.textContent = `(${displayPosts.length}件の投稿)`;
tl.innerHTML = displayPosts.map(p => {
const info = {
bot: '<i class="fas fa-robot mr-1"></i>BOT',
markov: '<i class="fas fa-dice mr-1"></i>MarkovBOT',
user: '<i class="fas fa-user mr-1"></i>ユーザー',
feed: '<i class="fas fa-rss mr-1"></i>FEEDBOT'
}[p.type] || '<i class="fas fa-user mr-1"></i>';
const main = p.link
? `<a href="${p.link}" target="_blank" class="text-sky-400 underline">${escapeHtml(p.content)}</a>`
: `${escapeHtml(p.content)}`;
return `
<div class="timeline-post p-6">
<div class="flex justify-between items-start mb-4">
<div class="flex items-center space-x-3">
<img src="${p.icon}" class="w-10 h-10 rounded-full object-cover border border-slate-500" onerror="this.src='https://via.placeholder.com/40'">
<div>
<div class="flex items-center">
<span class="font-semibold text-slate-100">${escapeHtml(p.username)}</span>
<span class="username-badge text-[10px]">${info}</span>
</div>
<div class="text-[11px] text-slate-400">${p.timestamp}</div>
</div>
</div>
</div>
<div class="text-slate-100 mb-4 leading-relaxed text-sm">${main}</div>
<div class="flex items-center space-x-4 pt-4 border-t border-slate-700">
<button onclick="likePost(${p.id})" class="flex items-center space-x-2 text-slate-300 hover:text-red-400 text-xs">
<i class="fas fa-heart"></i><span>${p.likes}</span>
</button>
<div class="relative">
<button onclick="toggleShareMenu(${p.id})" class="flex items-center space-x-2 text-slate-300 hover:text-sky-400 text-xs">
<i class="fas fa-share"></i><span>シェア</span>
</button>
<div id="share-menu-${p.id}" class="share-menu hidden">
<button onclick="shareToX(${p.id})"><i class="fab fa-x-twitter text-sky-400 mr-2"></i>Xでシェア</button>
<button onclick="shareToLine(${p.id})"><i class="fab fa-line text-green-400 mr-2"></i>LINEでシェア</button>
<button onclick="copyPost(${p.id})"><i class="fas fa-copy mr-2"></i>コピー</button>
</div>
</div>
<button onclick="deletePost(${p.id})" class="flex items-center space-x-2 text-slate-400 hover:text-red-400 ml-auto text-xs">
<i class="fas fa-trash"></i><span>削除</span>
</button>
</div>
</div>
`;
}).join('');
}
function getPostContentText(id) {
const p = posts.find(x => x.id === id);
if (!p) return '';
const tmp = document.createElement('div');
tmp.innerHTML = p.content;
return tmp.textContent || tmp.innerText || '';
}
function toggleShareMenu(id) {
document.querySelectorAll('[id^="share-menu-"]').forEach(el => el.classList.add('hidden'));
const m = document.getElementById('share-menu-' + id);
if (m) m.classList.toggle('hidden');
}
function shareToX(id) {
const t = encodeURIComponent(getPostContentText(id));
const u = encodeURIComponent(location.href);
window.open(`https://twitter.com/intent/tweet?text=${t}&url=${u}`, '_blank');
}
function shareToLine(id) {
const u = encodeURIComponent(location.href);
window.open(`https://social-plugins.line.me/lineit/share?url=${u}`, '_blank');
}
function copyPost(id) {
const t = getPostContentText(id);
if (navigator.clipboard) {
navigator.clipboard.writeText(t).then(() => alert('コピーしました')).catch(() => fallbackCopy(t));
} else fallbackCopy(t);
}
function fallbackCopy(t) {
const ta = document.createElement('textarea');
ta.value = t; document.body.appendChild(ta);
ta.select(); document.execCommand('copy');
document.body.removeChild(ta);
alert('コピーしました');
}
// ===== タイムライン フィルタ&検索 =====
function setFilter(f) {
currentFilter = f;
['all','user','bot','feed'].forEach(k => {
const btn = document.getElementById('filter-' + k);
if (!btn) return;
if (k === f) {
btn.classList.add('bg-indigo-600','text-white');
} else {
btn.classList.remove('bg-indigo-600','text-white');
}
});
renderTimeline();
}
// ===== RSS UI(個別ON/OFF + 一括ON/OFF) =====
function updateRssUI() {
const listDiv = document.getElementById('rss-list');
if (!listDiv) return;
if (!sharedRssFeeds || sharedRssFeeds.length === 0) {
listDiv.innerHTML = '<div class="text-slate-200 text-[11px] opacity-80">RSSフィード未登録</div>';
} else {
listDiv.innerHTML = sharedRssFeeds.map((url, i) => {
const enabled = sharedRssEnabled[url] !== false;
const enc = encodeURIComponent(url);
return `
<div class="flex items-center space-x-2 bg-slate-900 rounded px-2 py-2 mb-1 border border-slate-700 text-[11px]">
<input type="checkbox" ${enabled ? 'checked' : ''} onchange="toggleRssEnabled('${enc}', this.checked)" title="ON/OFF">
<div class="truncate flex-1 text-slate-100" title="${escapeHtml(url)}">${escapeHtml(url)}</div>
<button onclick="delRssFeed(${i})" class="text-red-400 hover:text-red-500" title="削除"><i class="fas fa-trash"></i></button>
</div>
`;
}).join('');
}
const iv = document.getElementById('rss-interval');
if (iv) iv.value = sharedRssInterval || 300;
}
function toggleRssEnabled(encUrl, on) {
const url = decodeURIComponent(encUrl);
sharedRssEnabled[url] = !!on;
saveSharedRss();
addLog('rss-log', `FEED ${on ? 'ON' : 'OFF'}: ${url}`, 'info');
}
function setAllRssEnabled(on) {
(sharedRssFeeds || []).forEach(u => sharedRssEnabled[u] = !!on);
saveSharedRss();
updateRssUI();
addLog('rss-log', `全フィードを${on ? 'ON' : 'OFF'}にしました`, 'success');
}
function saveSharedRss() {
localStorage.setItem('verse_shared_rssFeeds', JSON.stringify(sharedRssFeeds));
localStorage.setItem('verse_shared_rssInterval', String(sharedRssInterval));
localStorage.setItem('verse_shared_rssLastIds', JSON.stringify(sharedRssLastIds));
localStorage.setItem('verse_shared_rssEnabled', JSON.stringify(sharedRssEnabled));
}
function addRssFeed() {
const url = document.getElementById('rss-url').value.trim();
if (!/^https?:\/\/.+/.test(url)) { addLog('rss-log', '正しいRSSフィードURLを入力してください', 'error'); return; }
if (!sharedRssFeeds) sharedRssFeeds = [];
if (sharedRssFeeds.includes(url)) { addLog('rss-log', 'すでに登録済みです', 'error'); return; }
sharedRssFeeds.push(url);
sharedRssEnabled[url] = true;
saveSharedRss();
updateRssUI();
addLog('rss-log', `RSS追加: ${url}`, 'success');
document.getElementById('rss-url').value = '';
}
function delRssFeed(i) {
if (!sharedRssFeeds[i]) return;
if (!confirm('このフィードを削除しますか?')) return;
const url = sharedRssFeeds[i];
sharedRssFeeds.splice(i, 1);
delete sharedRssEnabled[url];
delete sharedRssLastIds[url];
saveSharedRss();
updateRssUI();
addLog('rss-log', 'RSS削除', 'info');
}
function setRssInterval() {
const iv = document.getElementById('rss-interval').valueAsNumber || 300;
if (iv < 10) return alert('間隔は10秒以上で設定してください。');
sharedRssInterval = iv;
saveSharedRss();
updateRssUI();
startRssAuto();
addLog('rss-log', `自動投稿間隔を${iv}秒に設定`, 'success');
}
function fetchRssNow() { fetchRssFeeds(); }
function fetchRssFeeds() {
if (!sharedRssFeeds || sharedRssFeeds.length === 0) return;
sharedRssFeeds.forEach(feedUrl => {
if (sharedRssEnabled[feedUrl] === false) {
addLog('rss-log', `OFFのため取得スキップ: ${feedUrl}`, 'info');
return;
}
fetch('https://api.rss2json.com/v1/api.json?rss_url=' + encodeURIComponent(feedUrl))
.then(resp => resp.json())
.then(data => {
if (!data.items || !data.items.length) return;
let lastId = sharedRssLastIds[feedUrl] || '';
let newItems = [];
for (const item of data.items) {
const guid = item.guid || item.link || item.pubDate || item.title;
if (!lastId || String(guid) > String(lastId)) newItems.push(item);
}
if (newItems.length === 0) return;
newItems.reverse().forEach(item => {
const guid = item.guid || item.link || item.pubDate || item.title;
if (!posts.some(p => p.type === 'feed' && p.link === item.link)) {
createPost(item.title, 'feed', 'FEEDBOT', verseIcons.feed, { link: item.link });
addLog('rss-log', `新しい記事: ${item.title}`, 'success');
}
sharedRssLastIds[feedUrl] = guid;
});
saveSharedRss();
})
.catch(() => addLog('rss-log', 'RSS取得エラー: ' + feedUrl, 'error'));
});
}
function startRssAuto() {
stopRssAuto();
fetchRssFeeds();
rssInterval = setInterval(fetchRssFeeds, (sharedRssInterval || 300) * 1000);
updateStatusIndicators();
addLog('rss-log', `RSS自動投稿を開始 (${sharedRssInterval}秒間隔)`, 'success');
}
function stopRssAuto() {
if (rssInterval) clearInterval(rssInterval);
rssInterval = null;
updateStatusIndicators();
addLog('rss-log', 'RSS自動投稿を停止しました', 'info');
}
// ===== BOT =====
function postBotMessage() {
const ta = document.getElementById('botContent');
const txt = ta.value.trim();
if (!txt) return alert('BOT投稿内容を入力してください。');
if (!currentUser) return alert('ログインが必要です。');
if (createPost(txt, 'bot', 'BOT', verseIcons.bot)) {
ta.value = '';
addLog('bot-log', `BOT投稿: "${txt.substring(0, 30)}..."`, 'success');
}
}
function generateMarkovText() {
let text = posts
.filter(p => ['user','bot','markov'].includes(p.type))
.map(p => {
const d = document.createElement('div');
d.innerHTML = p.content;
return (d.textContent || d.innerText || '')
.replace(/\s+/g, ' ').replace(/https?:\/\/\S+/g, '').trim();
}).join(' ');
if (text.length < 20) {
const fallbacks = [
"今日はいい天気ですね!",
"最近面白いニュースありましたか?",
"新しいアイデアが浮かんできました。",
"みんなはどう思いますか?"
];
return fallbacks[Math.floor(Math.random() * fallbacks.length)];
}
const tokens = text.match(/[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\w]+|[。、!?\r\n]/g) || [];
if (tokens.length < 2) return tokens.join('');
const markov = {};
for (let i = 0; i < tokens.length - 2; i++) {
const key = tokens[i] + '|' + tokens[i+1];
if (!markov[key]) markov[key] = [];
markov[key].push(tokens[i+2]);
}
let idx = Math.floor(Math.random() * (tokens.length - 2));
let key = tokens[idx] + '|' + tokens[idx+1];
let result = [tokens[idx], tokens[idx+1]];
let maxLen = 60 + Math.floor(Math.random() * 40);
for (let i = 0; i < maxLen; i++) {
const nexts = markov[key];
if (!nexts || nexts.length === 0) break;
const next = nexts[Math.floor(Math.random() * nexts.length)];
result.push(next);
if (/[。!?\n]/.test(next)) break;
key = result[result.length - 2] + '|' + result[result.length - 1];
}
return result.join('').replace(/\n/g, '');
}
function postMarkovBot() {
const txt = generateMarkovText();
if (createPost(txt, 'markov', 'MarkovBOT', verseIcons.bot)) {
addLog('bot-log', `マルコフ投稿: "${txt.substring(0, 40)}..."`, 'success');
}
}
function startBotAutoPost() {
const iv = document.getElementById('botIntervalSec').valueAsNumber || 60;
if (iv < 10) { alert('間隔は10秒以上で設定してください。'); return; }
stopBotAutoPost();
setTimeout(postMarkovBot, 3000);
botInterval = setInterval(postMarkovBot, iv * 1000);
updateStatusIndicators();
addLog('bot-log', `マルコフBOT自動投稿開始 (${iv}秒間隔)`, 'success');
}
function stopBotAutoPost() {
if (botInterval) {
clearInterval(botInterval);
botInterval = null;
updateStatusIndicators();
addLog('bot-log', 'マルコフBOT自動投稿を停止しました', 'info');
}
}
// ===== 共通UI/保存 =====
function addLog(id, msg, type = 'info') {
const el = document.getElementById(id);
const ts = new Date().toLocaleTimeString('ja-JP');
const div = document.createElement('div');
const cls = { error: 'error-message', success: 'success-message', info: 'text-slate-100 text-[11px]' }[type] || 'text-slate-100 text-[11px]';
div.className = cls;
div.innerHTML = `<span class="opacity-70">[${ts}]</span> ${escapeHtml(msg)}`;
if (el) {
el.appendChild(div);
el.scrollTop = el.scrollHeight;
while (el.children.length > 100) el.removeChild(el.firstChild);
}
try { console.log(`[${ts}] ${msg}`); } catch(_) {}
}
function updateStatusIndicators() {
const botI = document.getElementById('bot-status');
const botT = document.getElementById('bot-status-text');
if (botI && botT) {
const active = botInterval !== null;
botI.className = `status-indicator ${active ? 'status-active' : 'status-inactive'}`;
botT.textContent = active ? '動作中' : '停止中';
}
const rssI = document.getElementById('rss-status');
const rssT = document.getElementById('rss-status-text');
if (rssI && rssT) {
const active = rssInterval !== null;
rssI.className = `status-indicator ${active ? 'status-active' : 'status-inactive'}`;
rssT.textContent = active ? '動作中' : '停止中';
}
}
function showSystemStatus() {
alert(
`=== Verse システムステータス ===
全体投稿数: ${posts.length}
RSS登録数: ${sharedRssFeeds.length}
BOT投稿数: ${posts.filter(p => ['bot', 'markov'].includes(p.type)).length}
BOT自動投稿: ${botInterval ? '動作中' : '停止中'}
RSS自動投稿: ${rssInterval ? '動作中' : '停止中'}`
);
}
function uploadProfileIcon(e) {
const f = e.target.files[0];
if (!f) return;
if (f.size > 5 * 1024 * 1024) { alert('5MB以下にしてください。'); return; }
const r = new FileReader();
r.onload = () => {
profile.icon = r.result;
saveProfileNoAlert();
updateAllUI();
alert('プロフィール画像更新!');
};
r.readAsDataURL(f);
}
function saveProfile() {
const un = document.getElementById('username').value.trim();
const si = document.getElementById('self-intro').value.trim();
if (un.length > 20) { alert('ユーザー名は20文字以内で。'); return; }
profile.username = un || 'ゲストユーザー';
profile.selfIntro = si;
saveProfileNoAlert();
updateAllUI();
alert('プロフィール保存!');
}
function saveProfileNoAlert() {
users = JSON.parse(localStorage.getItem('verse_users') || '[]');
const idx = users.findIndex(u => u.email === (currentUser && currentUser.email));
if (idx >= 0) {
users[idx].profile = profile;
localStorage.setItem('verse_users', JSON.stringify(users));
}
}
function updateAllUI() {
const pi = document.getElementById('profile-icon');
const hi = document.getElementById('header-profile-icon');
if (pi) pi.src = profile.icon;
if (hi) hi.src = profile.icon;
['username-preview','header-username'].forEach(id => {
const el = document.getElementById(id);
if (el) el.textContent = profile.username;
});
const sip = document.getElementById('self-intro-preview');
if (sip) sip.textContent = profile.selfIntro || 'まだ自己紹介がありません';
const emailEl = document.getElementById('header-user-email');
if (emailEl && currentUser) emailEl.textContent = currentUser.email;
const bi = document.getElementById('bot-icon-preview');
const fi = document.getElementById('feed-icon-preview');
if (bi) bi.src = verseIcons.bot;
if (fi) fi.src = verseIcons.feed;
renderTimeline();
}
function updateCharCount() {
const pc = document.getElementById('postContent');
const cc = document.getElementById('char-count');
if (pc && cc) {
const l = pc.value.length;
cc.textContent = `(${l}/500)`;
cc.style.color = l > 450 ? '#fca5a5' : '';
}
}
function toggleDarkMode() {
isDarkMode = !isDarkMode;
if (isDarkMode) {
document.body.classList.add('dark');
} else {
document.body.classList.remove('dark');
}
localStorage.setItem('verse_darkMode', isDarkMode.toString());
}
function saveData() { localStorage.setItem('verse_posts', JSON.stringify(posts)); }
// 入力UIフック
document.addEventListener('DOMContentLoaded', () => {
const pc = document.getElementById('postContent');
if (pc) {
pc.addEventListener('input', updateCharCount);
pc.addEventListener('keydown', e => { if (e.key === 'Enter' && e.ctrlKey) { e.preventDefault(); createUserPost(); } });
}
const ui = document.getElementById('username');
if (ui) ui.addEventListener('input', () => {
const v = ui.value.trim() || 'ゲストユーザー';
['username-preview','header-username'].forEach(id => {
const el = document.getElementById(id);
if (el) el.textContent = v;
});
});
const si = document.getElementById('self-intro');
if (si) si.addEventListener('input', () => {
const v = si.value.trim() || 'まだ自己紹介がありません';
const el = document.getElementById('self-intro-preview');
if (el) el.textContent = v;
});
const ts = document.getElementById('timeline-search');
if (ts) ts.addEventListener('input', () => {
currentSearch = ts.value || '';
renderTimeline();
});
setFilter('all');
});
window.addEventListener('beforeunload', () => { stopBotAutoPost(); stopRssAuto(); saveData(); });
window.addEventListener('error', e => { addLog('bot-log', `システムエラー: ${e.message}`, 'error'); });
</script>
</body>
</html>
リアルモンスターARゲーム 企画書
リアルモンスターARゲーム 企画書(ドラフト)
本資料は「現実世界×位置情報×AR×収集・育成・共闘」を核にした新規モバイルゲームの企画書です。既存IP(例:某有名モンスターIP)への依存はせず、完全オリジナルIPを前提とします。
1. エレベーターピッチ / コンセプト
- 一言:街を歩けば“精霊(ファント)”が棲む。現実世界の光・天気・場所が戦略に変わる、散歩が冒険になるARコレクションRPG。
- USP(独自性)
- “現実環境がギミック”:時刻/天気/騒音/明るさ/混雑度をゲームロジックに反映。
- “協調AR”:近接プレイヤーで同一AR個体を同時観測・共同捕獲・共闘。
- “安全×自治体連携”:観光/商店街/防災訓練などの公的イベント連動で社会実装。
- “歩行以外の楽しみ”:自宅ARテラリウム、エンカウントカード、ミニゲーム。
2. ターゲット / ペルソナ
- 主要層:10〜40代のライト〜ミドルゲーマー、散歩/旅行好き、写真・SNS発信層。
- 準主要層:親子(7歳以上推奨)、健康志向ユーザー、コレクター気質。
- 地域:まずは日本主要都市→地方観光地→海外(アジア→グローバル)。
3. 世界観 / 主要キャラクター(例)
- 設定:人の感情と自然のエネルギーから生まれる“ファント”。都市/水辺/森林/寺社/工場などバイオームごとに生態が異なる。
- スターター3体(例):
- フレアット(炎/活力)…晴天・高温時に強化。
- ミズクル(潮/好奇心)…水辺・雨天で出現率UP。
- リーファ(森/癒し)…緑地で回復スキル特化。
4. 主要ゲームサイクル
4.1 コアループ
- 移動 → 2) 現実の地点でスキャン/エンカウント → 3) AR捕獲/バトル → 4) 育成/合成/装備 → 5) ミッション/イベント →(1に戻る)
4.2 ショート/ミドル/ロングサイクル
- ショート(1〜5分):その場スキャン→一体と遭遇→ミニゲームで捕獲→写真→SNS共有。
- ミドル(10〜30分):近隣スポット巡り→連鎖ボーナス→日課ミッション消化→協力レイド。
- ロング(数日〜週):育成/進化ルート選択→地形限定種の収集→シーズンイベント攻略。
5. ゲームシステム詳細
5.1 位置情報×天候×時間
- 出現テーブル:バイオーム×天候(晴/曇/雨/雪/強風)×時間帯(朝/昼/夕/夜)×気温でドロップ/遭遇率を可変。
- 行動制限:夜間は住宅地のスポーンを抑制、駅構内は安全配慮でエンカウント無し。
5.2 AR捕獲(スキルベース)
- ARリングを端末のモーションで合わせる→端末の照度/安定度で成功率変化。
- 近接協力:2〜4人同時の“リンクキャスト”で捕獲ゲージ共有&強個体出現。
5.3 バトル
- 属性:炎/水/森/雷/岩/光/影(例)。2スキル+1奥義のリアルタイム・ライトアクション。
- 位置ギミック:磁場(駅周辺)/潮位(水辺)/騒音(繁華街)でバフ/デバフ。
- レイド:ランドマークに定時/随時で発生、ARで巨大個体が出現。
5.4 育成/進化/合成
- 成長ルート:探索型/戦闘型/支援型の3系統。分岐進化は場所・時刻・天候を条件化。
- 装備(護符):地域素材からクラフト。観光地限定素材でご当地ビルド。
5.5 拠点/テラリウム
- 自宅でARテラリウムを配置。収集個体が生活し、環境飾り(植物/雨/灯り)を置くとパッシブ効果。
5.6 ソーシャル
- 近接ルーム:半径20mで自動ルーム結成→トレード/共闘。
- 写真モード:被写体検出・ポーズ指示・露出/被写界深度調整→#ハッシュ共有。
6. 安全設計・配慮
- ながらスマホ抑止:一定速度以上でプレイ制限/操作簡略化。
- 立入禁止検知:学校/私有地/危険区域はスポーン無効化。
- 夜間配慮:22:00〜6:00は住宅地レイド停止。
- プライバシー:位置情報はボロノイ領域等で空間的匿名化、サーバ保存は最小限(ハッシュ化/丸め)。
7. マネタイズ
- コスメ課金:スキン/ポーズ/ARエフェクト(性能差なし)。
- バトルパス:探索/撮影/協力でポイント→限定装飾。
- ローカルコラボ:商店街/自治体のデジタルスタンプラリー有料参加。
- 広告:任意視聴でブースト(年齢配慮)。
- 確率表示/資金決済法対応:ガチャ導入時は提供割合の明示、天井/重複救済を実装。
8. KPI / 事業目標
- 主要KPI:D1/7/30リテンション、WAU/MAU、課金率、AR写真投稿率、協力レイド参加率、平均歩数。
- 目標(Year1):
- MAU 50万 / 月、課金率 4.0%、AR写真月間投稿 30万件、平均セッション8分×2回/日。
9. コンテンツ運用
- シーズン制(6〜8週):新バイオーム/限定進化/レイド。
- 地域イベント:花見/夏祭り/紅葉/雪祭り/花火大会と連動。
- クリエイタープログラム:写真/ARハントの二次創作を促進(ガイドライン整備)。
10. 技術アーキテクチャ(想定)
- クライアント:Unity + AR Foundation(ARKit/ARCore対応)、LiDAR対応端末は平面/深度優遇。
- サーバ:マッチング/レイドはPhoton Fusion/Realtime、常時はFirebase + Cloud Functions。
- 地図/POI:Mapbox/Google Maps Platform。安全レイヤ(危険/禁止区域)を別テーブル管理。
- データ:ビッグクエリで行動データ解析。位置データは粗度を保つ。
- チート対策:移動速度/加速度の多軸判定、ルート一貫性、端末脱獄検知。
11. 開発体制 / スケジュール(例)
- チーム編成(初期10〜15名):
- PM 1 / プランナー 2 / クライアントエンジニア 4 / サーバ 2 / アート 3 / データ 1 / QA 2
- ロードマップ:
- M0-1(調査):プロトタイプ:AR捕獲、位置/天候連動、近接同期。
- M2-4(α):基本育成・写真・小規模レイド、テラリウム初期版。
- M5-6(CBT):地元2都市でクローズド、KPIチューニング。
- M7-9(OBT):全国主要都市展開、自治体タイアップ開始。
- M10-12(正式):シーズン1開幕、海外ローカライズ準備。
12. 画面遷移 / UIワイヤ(テキスト)
- ホーム/マップ:上部:天気/時間、中央:ARスキャン、下部:近接ルーム/ミッション/撮影。
- 遭遇AR:カメラ起動→平面検出→ファント投影→リング合わせ→捕獲。
- バトル:左スワイプ回避/右タップ攻撃/長押し奥義、上部に環境バフ表示。
- テラリウム:ルーム編集→飾り設置→効果プレビュー→SNS投稿。
13. 法務・コンプライアンス
- 位置情報/個人情報:プライバシーポリシー、DPO設定、オプトイン/オプトアウト。
- 年少者保護:夜間プレイ抑制、課金上限、保護者同意フロー。
- 著作権/商標:キャラ/名称はオリジナル創作。第三者IP非使用方針。
- 景表法:確率型アイテムは表記・救済策・履歴開示。
14. 競合/差別化の要点
- 既存:位置×収集(GO系)、位置×ハンティング(MH系)、歩行×育成(Bloom系)。
- 差別化:環境ギミックの“戦略化”、協調ARの常設、観光/自治体との恒常連携、写真×クリエイター経済。
15. 収支イメージ(概算・初年度)
- 開発費:2.5〜3.5億円(12ヶ月/15名規模、外注含む)。
- 運用費(月):サーバ/地図/分析/CS/イベントで1,500〜2,500万円。
- 売上:MAU50万×ARPPU月1,200円×課金率4% ≒ 月2.4億円想定(広告/コラボ除く)。
16. リスクと対策
- 初期離脱:チュートリアル短縮/“最初の一体”を確実に入手/撮影で成功体験。
- 安全問題:速度検知/危険地帯除外/夜間制限/注意喚起UI。
- 位置チート:速度と加速度の相関、履歴の異常検知、端末整合チェック。
- 季節性/天候偏重:屋内コンテンツ/テラリウム/日替りミッションで平滑化。
17. 成長戦略
- UGC推進:フォトコン、ARフィルタ配布、作品マーケット(審査制)。
- コラボ:ご当地キャラ/観光協会/鉄道/小売。現地での限定クエスト。
- 海外展開:各国の宗教/文化施設はスポーン除外設定を現地監修。
18. 参考KPI計測設計
- ファネル(DL→FTUE→Day1/7/30)、地図ヒートマップ(密度/滞在/回遊)、安全指標(危険接近アラート発生率)。
19. 添付:クエスト例
- “雨上がりの公園で”:雨後2時間以内の緑地で限定個体出現、足跡トレイル追跡ミニゲーム。
- “灯りの路地”:夜19–21時の商店街で光属性が出現、写真投稿でバフ。
次アクション(提案)
- プロトタイプ要件定義(AR捕獲・協調同期・環境バフの3本柱)
- フィールドテスト都市の選定(例:渋谷区/台東区)
- 自治体/商店街のヒアリング開始(安全/回遊/混雑配慮)
YESキリストBOT
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>YESキリスト BOT</title>
<!-- Tailwind(CDN) -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Font Awesome(アイコン) -->
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" rel="stylesheet">
<meta name="description" content="YESキリストBOT:優しく背中を押してくれるシンプルなチャットボット。今日の励まし、進むべき?などを相談できます。" />
<style>
/* スクロールバー控えめ */
* { scrollbar-width: thin; scrollbar-color: #cbd5e1 transparent; }
*::-webkit-scrollbar { height: 8px; width: 8px; }
*::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 8px; }
/* バブルの三角 */
.bubble:after{
content:""; position:absolute; bottom:-6px; left:16px; border:6px solid transparent; border-top-color:rgba(255,255,255,0.9);
filter: drop-shadow(0 1px 0 rgba(15,23,42,.05));
}
.bubble.me:after{ left:auto; right:16px; border-top-color:#dcfce7; }
</style>
</head>
<body class="min-h-screen bg-gradient-to-b from-slate-50 to-white text-slate-800">
<!-- コンテナ -->
<div class="mx-auto max-w-3xl px-4 py-6">
<!-- ヘッダー -->
<header class="flex items-center justify-between rounded-2xl bg-white/90 backdrop-blur shadow-sm p-4">
<div class="flex items-center gap-3">
<div class="h-10 w-10 rounded-full bg-emerald-500 text-white grid place-items-center">
<i class="fa-solid fa-dove"></i>
</div>
<div>
<h1 class="text-xl font-bold">YESキリスト BOT</h1>
<p class="text-xs text-slate-500">優しく「YES」で背中を押すチャット</p>
</div>
</div>
<div class="flex items-center gap-2">
<button id="btnClear" class="text-xs px-3 py-1.5 rounded-lg bg-slate-100 hover:bg-slate-200 transition">
履歴クリア
</button>
<button id="btnExport" class="text-xs px-3 py-1.5 rounded-lg bg-slate-100 hover:bg-slate-200 transition">
エクスポート
</button>
<label class="text-xs px-3 py-1.5 rounded-lg bg-slate-100 hover:bg-slate-200 transition cursor-pointer">
インポート<input id="fileImport" type="file" accept="application/json" class="hidden">
</label>
</div>
</header>
<!-- プリセット -->
<section class="mt-4 grid grid-cols-2 sm:grid-cols-4 gap-2">
<button class="preset chip">今日の励まし</button>
<button class="preset chip">挑戦していい?</button>
<button class="preset chip">許してもいい?</button>
<button class="preset chip">進むべき?</button>
</section>
<!-- チャット -->
<main id="chat" class="mt-4 h-[60vh] overflow-y-auto rounded-2xl bg-white/90 backdrop-blur p-4 shadow-sm space-y-4">
<!-- 初期メッセージ -->
</main>
<!-- 入力欄 -->
<form id="composer" class="mt-4 flex items-end gap-2">
<textarea id="input" rows="1" placeholder="ここに相談を書いてね(例:新しいことに挑戦しても大丈夫?)"
class="flex-1 resize-none rounded-2xl border border-slate-200 bg-white p-3 focus:outline-none focus:ring-2 focus:ring-emerald-300"></textarea>
<button id="btnSend" type="submit" class="h-11 px-4 rounded-2xl bg-emerald-500 text-white hover:bg-emerald-600 transition">
<i class="fa-solid fa-paper-plane"></i>
</button>
</form>
<!-- 使い方 -->
<details class="mt-4 rounded-2xl bg-slate-50 p-4 text-sm text-slate-600">
<summary class="cursor-pointer font-semibold">使い方</summary>
<ul class="list-disc pl-5 mt-2 space-y-1">
<li>メッセージを送ると、YESキリストが優しく背中を押す言葉で返します。</li>
<li><code>/prayer</code> で短いお祈り風メッセージ、<code>/bless</code> で祝福文。</li>
<li>履歴はブラウザに保存されます(ローカルのみ)。</li>
</ul>
</details>
</div>
<script>
// ====== 設定 ======
const STORAGE_KEY = 'yeschrist_history_v1';
// YESキリストの返答テンプレ
const YES_OPENERS = [
"あなたの心に、静かなYESが灯っています。",
"恐れずに、やさしいYESで一歩を。",
"迷いの中にいても、大丈夫。答えはYESです。",
"小さな信頼が、大きなYESへと育ちます。",
"あなたの良き思いに、YESを重ねましょう。"
];
const YES_ENCOURAGE = [
"試みは愛によって導かれ、愛は前進にYESと言います。",
"完全でなくていい。歩き出す勇気にYES。",
"扉は叩く者に開かれます。ノックにYES。",
"あなたの賜物は隠さずに、光の下へ。YES。",
"やさしさを選ぶ度に、道は明るくなります。YES。"
];
const YES_TAGS = [
"平安がありますように。",
"あなたは一人ではありません。",
"今日の小さな一歩を大切に。",
"心に光を。",
"祝福とともに。"
];
const PRAYERS = [
"天のやさしさがあなたを包み、歩みを照らしますように。アーメン。",
"弱さのときにこそ力が満ちますように。アーメン。",
"迷う心に静けさが与えられますように。アーメン。"
];
const BLESS = [
"あなたの決断に平安が伴いますように。",
"出るにも入るにも祝福が満ちますように。",
"今日の働きに恵みがありますように。"
];
// ====== DOM ======
const chat = document.getElementById('chat');
const input = document.getElementById('input');
const form = document.getElementById('composer');
const btnSend = document.getElementById('btnSend');
const btnExport = document.getElementById('btnExport');
const btnClear = document.getElementById('btnClear');
const fileImport = document.getElementById('fileImport');
document.querySelectorAll('.preset').forEach(el => el.classList.add(
'px-3','py-2','rounded-xl','bg-emerald-50','text-emerald-700','hover:bg-emerald-100','transition','text-sm','chip'
));
// ====== ユーティリティ ======
const nowStr = () => new Date().toLocaleString();
const rand = arr => arr[Math.floor(Math.random() * arr.length)];
const saveHistory = () => {
const items = [...chat.querySelectorAll('[data-msg]')].map(el => ({
role: el.dataset.role, text: el.dataset.msg, time: el.dataset.time
}));
localStorage.setItem(STORAGE_KEY, JSON.stringify(items));
};
const loadHistory = () => {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return [];
try { return JSON.parse(raw); } catch { return []; }
};
function appendMessage(role, text, time = nowStr()) {
const wrapper = document.createElement('div');
wrapper.className = role === 'user'
? 'flex justify-end'
: 'flex justify-start';
const bubble = document.createElement('div');
bubble.className = 'relative max-w-[85%] rounded-2xl p-3 bubble shadow-sm ' +
(role === 'user' ? 'bg-emerald-100 me' : 'bg-white/90');
bubble.textContent = text;
const meta = document.createElement('div');
meta.className = 'mt-1 text-[10px] text-slate-500 ' + (role === 'user' ? 'text-right' : 'text-left');
meta.textContent = time;
const container = document.createElement('div');
container.dataset.msg = text;
container.dataset.role = role;
container.dataset.time = time;
container.className = 'space-y-1';
container.appendChild(bubble);
container.appendChild(meta);
wrapper.appendChild(container);
chat.appendChild(wrapper);
chat.scrollTop = chat.scrollHeight;
}
function systemWelcome() {
appendMessage('assistant',
'ようこそ。YESキリストは、あなたの良き願いに「YES」で寄り添います。/prayer で短いお祈り、/bless で祝福文が届きます。');
}
function composeYesReply(userText) {
const lower = (userText || '').toLowerCase();
let opener = rand(YES_OPENERS);
let body = rand(YES_ENCOURAGE);
let tag = rand(YES_TAGS);
// ほんの少しだけ文脈スパイス
if (/[??]$/.test(userText)) {
opener = "その問いかけに、穏やかなYESが返っています。";
}
if (/(許|ゆる)す/.test(userText)) {
body = "赦しは心を自由にし、あなたを前へ押し出します。YES。";
}
if (/(挑戦|チャレンジ|challenge)/i.test(userText)) {
body = "小さくとも踏み出す一歩は尊く、次の景色を連れてきます。YES。";
}
if (/(進|やめ|辞め|やる|やら)/.test(userText)) {
tag = "平安のあるほうへ。YES。";
}
return `${opener}\n${body}\n${tag}`;
}
async function reply(userText) {
// コマンド
if (userText.trim().startsWith('/prayer')) {
appendMessage('assistant', rand(PRAYERS));
saveHistory(); return;
}
if (userText.trim().startsWith('/bless')) {
appendMessage('assistant', rand(BLESS));
saveHistory(); return;
}
// YES返答
const thinking = document.createElement('div');
thinking.className = 'text-xs text-slate-500';
thinking.textContent = '…考えています';
chat.appendChild(thinking); chat.scrollTop = chat.scrollHeight;
await new Promise(r => setTimeout(r, 300)); // 小さな演出
thinking.remove();
appendMessage('assistant', composeYesReply(userText));
saveHistory();
}
// ====== イベント ======
form.addEventListener('submit', async (e) => {
e.preventDefault();
const text = input.value.trim();
if (!text) return;
appendMessage('user', text);
input.value = '';
input.style.height = '44px';
saveHistory();
reply(text);
});
// 自動リサイズ
input.addEventListener('input', () => {
input.style.height = 'auto';
input.style.height = Math.min(input.scrollHeight, 160) + 'px';
});
// プリセット
document.querySelectorAll('.preset').forEach(btn => {
btn.addEventListener('click', () => {
const q = btn.textContent.trim();
appendMessage('user', q);
saveHistory();
reply(q);
});
});
// クリア
btnClear.addEventListener('click', () => {
if (!confirm('履歴をすべて削除しますか?')) return;
localStorage.removeItem(STORAGE_KEY);
chat.innerHTML = '';
systemWelcome();
});
// エクスポート
btnExport.addEventListener('click', () => {
const data = localStorage.getItem(STORAGE_KEY) ?? '[]';
const blob = new Blob([data], { type: 'application/json' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `yeschrist_history_${Date.now()}.json`;
a.click();
URL.revokeObjectURL(a.href);
});
// インポート
fileImport.addEventListener('change', async (e) => {
const file = e.target.files?.[0];
if (!file) return;
const text = await file.text();
try {
const arr = JSON.parse(text);
if (!Array.isArray(arr)) throw new Error('format');
localStorage.setItem(STORAGE_KEY, text);
chat.innerHTML = '';
arr.forEach(m => appendMessage(m.role, m.text, m.time));
} catch {
alert('インポート失敗:JSON形式が正しくありません。');
} finally {
fileImport.value = '';
}
});
// ====== 初期化 ======
(function init() {
const hist = loadHistory();
if (hist.length === 0) {
systemWelcome();
} else {
hist.forEach(m => appendMessage(m.role, m.text, m.time));
}
// 入力高さ初期
input.style.height = '44px';
})();
</script>
</body>
</html>
BCI出力の2030s
「脳に直接世界を書き込む」革命
→ 2030年代=フルダイブVRの本番スタート
- BCI出力とは?(超簡潔)
方向説明2030sで何が変わる?BCI入力(脳 → PC)思考でカーソル動かす今ある(Neuralink 2025実証)BCI出力(PC → 脳)脳に「映像・触覚・味」を送る2030sで実用化 ← これが鍵
- 2030sのBCI出力:何ができる?
視覚:4Kフルカラー映像を脳に直接投影
触覚:全身の「風」「痛み」「抱擁」
聴覚:立体音 + 音楽を脳内再生
味覚・嗅覚:ステーキの味 + バラの香り
- 2030sロードマップ(根拠付き)
年次技術マイルストーン2025視覚:点滅光を脳に書き込み(人間)2028視覚:16×16ピクセル映像2030視覚:低解像度動画(320p)
触覚:指先の圧力2032視覚:HD映像(1080p)
触覚:全身基本ハプティクス2035五感フルシミュレーション(視覚4K + 全身触覚 + 味覚)2037フルダイブVR商用化
- 2030sのBCI出力:どうやって実現?
技術説明2030sでの進化電極アレイ脳に針を刺して信号送受信1億電極(今:1万)光遺伝学光でニューロンをON/OFF非侵襲レーザーで出力ナノボット血管内を泳いで脳に信号2035年実用化予測AIマッピング個人脳をAIで学習1秒でキャリブレーション
- 2030sの「フルダイブVR」体験例
text【2037年、君の1日】 - 起床 → 思考「ログイン」
- 脳に「朝の光 + コーヒーの香り + 温かいマグカップ」
- 思考「剣を振る」→ 金属音 + 衝撃 + 鉄の匂い
- 思考「ログアウト」→ 現実に戻る
→ 現実と区別不能 - 2030sへの「個人準備」ロードマップ
時期アクション2025Vision Pro 2 + EEGで擬似フルダイブ2026Synchron BCI(血管内)で本物入力2028Neuralink出力ベータ参加2030BCI出力キット購入($10,000)2035フルダイブ常時装着
結論:2030s = 脳が「第二の現実」になる
BCI出力の2030s
→ 脳に「世界」を書き込める時代
→ フルダイブVRの本番スタート
Metaが開発してる超知能AIのしくみ
ざっくり言うと、Metaのいう「超知能(personal superintelligence)」は、Llama系の次世代“超”基盤モデルを中心に、自己改善ループと巨大インフラを噛み合わせた“個人に配る超強いAI”の構想です。公開情報ベースで、仕組みはだいたい下の積み上げになります。
仕組み(公開情報から見える設計)
1) 中核モデル(基盤)
- Llama 4 世代をベースにした“ネイティブ多モーダル”の長文対応モデル群(画像・音声・テキストを最初から扱える系統)。これはLlama 4の発表内容からの延長線上にあり、文脈長の大幅拡張が特徴です。ai.meta.com
- Metaは「個人向けの超知能」を明確に掲げており、UIの表現は“Personal Superintelligence”として整理されています。meta.com+1
2) 自己改善ループ(Self-Improving)
- ザッカーバーグは「人手を最小化した学習」「自己改善」を重視する新ラボ方針を説明。モデルが自律的に学習・改善していく方向を打ち出しています。WIRED
- 7/30の発表では“最初の一歩”として自己改善の兆しに言及し、最強モデルは全面公開しない方針も示唆。Live Science+1
3) データ&データエンジン
- 学習データの整備・合成を加速するため、Scale AI と大型提携(49%出資)・人材登用。大規模データパイプラインで継続的にモデルを回す狙い。AP News
4) インフラ(コンピュート&ネットワーク)
- 「数千億ドル規模」の投資で、複数地域に跨る超大型AIデータセンター群を建設。電力・冷却・高帯域ネットワークを含む“AI工場”化を推進。Reuters+1
- Meta公式エンジニアリングの発信でも、AI時代向けに設計を総張り替え(スケールアウト、AI向けネットワーキング、カスタムシリコンまで)。OCP 2025でも地域分散DCの設計指針を共有。fierce-network.com+3engineering.fb.com+3techblog.comsoc.org+3
5) 公開ポリシー(オープン vs クローズ)
- Llama系は“オープンウエイト”路線を続けつつ、超知能クラスは安全面を見て全部は公開しない可能性を明言。TechCrunch
まとめ(何が“超”なの?)
- モデル能力:多モーダル×超長文×自己改善で、タスク汎用性と継続学習性を押し上げる。WIRED+1
- データエンジン:実データ+合成データ+継続評価の循環で“学習→運用→改善”を高速回転。AP News
- AI工場(インフラ):超巨大DC、広域ネットワーク、(将来的な)カスタムチップまで含めた垂直統合で、莫大な計算を常時供給。Reuters+2engineering.fb.com+2
- 提供形態:一般向けには“個人の参謀”として出しつつ、最上位は慎重運用。meta.com+1
もっと深掘りしますか?たとえば「自己改善の技術的ループ(評価→データ生成→再学習)」「多モーダル推論パイプライン」「データセンター構成(電力・冷却・ネット)」など、図で噛み砕いて解説できます。
