人工生命体の作りかた

人工生命体(Artificial Life、以下A-Life)を作成する方法は、目的やアプローチによって異なりますが、以下に一般的な作り方と主要な手法を簡潔に説明します。A-Lifeは、生物学的生命の特性(自己複製、進化、適応など)をコンピュータシミュレーションやロボット工学などで再現する試みです。以下は主にソフトウェアベースのA-Lifeに焦点を当てた手順です。


1. 目的とモデルの定義

  • 目的を決める: 何を再現したいか(例: 進化、自己組織化、学習能力)を明確化。
  • モデルを選ぶ: シンプルなもの(例: セルオートマトン)から複雑なもの(例: 遺伝的アルゴリズム、ニューラルネットワーク)まで、モデルを選択。
    • 例: 「ライフゲーム」(Conway’s Game of Life)は単純なルールで複雑なパターンを生成。
    • 例: 進化をシミュレートするなら遺伝的アルゴリズム(GA)。

2. 基本要素の設計

人工生命体には以下の要素が必要です:

  • エージェント: 生命体を表す個体(例: ソフトウェア内のオブジェクトやロボット)。
  • 環境: エージェントが相互作用する空間(例: 2Dグリッド、3D仮想世界)。
  • ルール: 行動、繁殖、死などのルールを定義。
    • 例: 「エージェントはエネルギーが0になると死ぬ」「近くのエージェントと競争する」。
  • 適応性: 進化や学習を可能にする仕組み(例: 突然変異、交叉、強化学習)。

3. 実装方法

a. プログラミングによる実装

  • 言語: Python、Java、C++などが一般的。Pythonはライブラリが豊富で初心者向け。
    • 例: NumPyやDEAP(進化計算ライブラリ)で遺伝的アルゴリズムを実装。
  • ステップ:
    1. 環境を構築(例: 2Dグリッドや物理エンジン)。
    2. エージェントに属性を定義(例: 位置、エネルギー、遺伝子)。
    3. ルールを実装(例: 移動、繁殖、突然変異)。
    4. シミュレーションを繰り返し、結果を観察。

b. 既存ツールの活用

  • NetLogo: マルチエージェントシミュレーションに最適。
  • Avida: デジタル進化の研究プラットフォーム。
  • Unity/Blender: 3D環境でのA-Lifeシミュレーションに使用可能。

c. ハードウェアベース

  • ロボット工学を用いる場合、センサーやアクチュエータを備えた物理的なエージェントを作成。
  • 例: 自己組織化する小型ロボット群(Swarm Robotics)。

4. 進化と学習の組み込み

  • 遺伝的アルゴリズム(GA):
    • 個体に「遺伝子」を与え、適応度関数で評価。
    • 選択、交叉、突然変異を繰り返して進化をシミュレート。
  • 強化学習(RL):
    • エージェントが環境と相互作用し、報酬を最大化する行動を学習。
    • 例: OpenAIのGymやStable-Baselines3を使用。
  • ニューラルネットワーク:
    • ディープラーニングで複雑な行動を学習(例: TensorFlow、PyTorch)。

5. テストと改良

  • シミュレーションを実行し、意図した生命らしい振る舞い(例: 自己複製、適応)が現れるか確認。
  • パラメータ(例: 突然変異率、環境の複雑さ)を調整して結果を最適化。
  • 例: 進化が停滞する場合、突然変異率を上げる。

6. 具体例:簡単なA-Lifeのコード(Python)

以下は、遺伝的アルゴリズムで進化するエージェントの簡単な例です。

Python

import random
import numpy as np

# エージェントクラス
class Agent:
    def __init__(self, genes):
        self.genes = genes  # 遺伝子(例: [0.5, 0.3, 0.2])
        self.fitness = 0

    def calculate_fitness(self):
        # 適応度関数(例: 遺伝子の合計)
        self.fitness = sum(self.genes)

# 人口を初期化
population_size = 50
gene_length = 3
population = [Agent([random.random() for _ in range(gene_length)]) for _ in range(population_size)]

# 進化のループ
generations = 100
for generation in range(generations):
    # 適応度を計算
    for agent in population:
        agent.calculate_fitness()
    
    # 上位の個体を選択
    population.sort(key=lambda x: x.fitness, reverse=True)
    parents = population[:10]
    
    # 新しい世代を生成(交叉と突然変異)
    new_population = parents[:]
    while len(new_population) < population_size:
        parent1, parent2 = random.sample(parents, 2)
        child_genes = [(g1 + g2) / 2 for g1, g2 in zip(parent1.genes, parent2.genes)]
        # 突然変異
        if random.random() < 0.1:
            child_genes[random.randint(0, gene_length-1)] += random.uniform(-0.1, 0.1)
        new_population.append(Agent(child_genes))
    
    population = new_population

    # 結果を表示
    print(f"Generation {generation + 1}, Best Fitness: {population[0].fitness}")

このコードは、ランダムな遺伝子を持つエージェントが進化する簡単なシミュレーションです。


7. 倫理的配慮

  • A-Lifeが自律性や知能を持つ場合、倫理的問題(例: 制御不能、悪用)が発生する可能性がある。
  • 特にAIやロボットに応用する場合は、安全性と責任を考慮。

8. 参考リソース

  • 書籍: 「Artificial Life: An Overview」(Christopher G. Langton)
  • オンライン: MIT OpenCourseWareのA-Life講義、GitHubのA-Lifeプロジェクト。
  • コミュニティ: X上で「#ArtificialLife」タグを検索し、最新の研究やプロジェクトを追う。

もし具体的な目的(例: 特定の行動を再現、3D環境での実装)やプログラミングのレベルを教えていただければ、さらに詳細な手順やコードを提供できます!また、特定のツールやハードウェアに焦点を当てることも可能です。

神狩りのクロニクル – GOD HUNTER CHRONICLE –

ゲーム企画書

タイトル(仮):神狩りのクロニクル – GOD HUNTER CHRONICLE –
ジャンル   :ダークファンタジー・アクションRPG
プラットフォーム:PC(Steam)/CS機(PS / Switchクラス)想定
対象     :中高生〜30代のRPG/アクション好き・配信視聴者層


1. 企画概要

1-1. 一言コンセプト

「神を狩り、世界を書き換える“超没入・ダークファンタジーRPG”」

1-2. 企画意図・狙い

  • 『神を倒す=レイドボス級の快感』をシングルプレイでも味わえる構造にする。
  • 難しすぎる“死にゲー”ではなく、
    「強くなって再挑戦するのが気持ちいい“ご褒美タイプ難度”」を目指す。
  • 配信映えを意識し、
  • 神との戦闘演出
  • マルチエンディング
  • プレイヤーの選択でNPCや街が変化
    を用意してSNSと相性のいいRPGにする。

2. ターゲットユーザー

  • メインターゲット
  • 年齢:15〜30歳
  • 好み:
    • 王道/ダークファンタジー、アニメ調RPG
    • アクションRPG・軽めの“死にゲー”系タイトル
    • Vtuber/ゲーム配信を見る文化に馴染みあり
  • サブターゲット
  • 「最近あまりゲームしてないけど、話題作は1本くらいは触りたい」ライトゲーマー
  • ダークファンタジーの世界観だけでも楽しみたいストーリー重視派

3. ゲームの特徴(USP)

  1. 「神狩り」がテーマのダークファンタジーRPG
  • 通常の魔物ではなく、“神格存在”を狩ることが最終的な目的。
  • 各地に封印された神々は、それぞれ世界のルールを司っており
    → 倒す/救う選択がそのまま世界やエンディングを書き換える。
  1. 手触りの良いアクション+“運命書き換え”システム
  • 攻撃・回避・スキルというシンプル操作。
  • 「運命ゲージ」を使って
    • 一瞬だけ時間を巻き戻す
    • 未来の行動結果をチラ見する(敵の大技予告など)
  • これにより見た目はハードだけど、実は遊びやすいアクションRPGに。
  1. 神を狩るほど、世界が壊れていく/再構築されていく
  • 神を倒すと短期的には街が救われるが、
    長期的には世界のバランスが崩れる…など“ジレンマ構造”。
  • 全神を狩る/一部を救う/まったく倒さないなどでマルチエンディング。
  1. 拠点都市の成長&変質
  • 拠点となる都市は、クエストとストーリー進行で
    • 新しい店や施設が建つ
    • 住民の会話や雰囲気が変わる
  • 神狩りの結果によって、「豊かだが不気味」「貧しいが暖かい」など
    プレイヤーの選択が“街の空気”に反映される

4. ゲームシステム

4-1. 基本操作

  • 移動:左スティック / WASD
  • カメラ:右スティック / マウス
  • 攻撃:軽攻撃・重攻撃(コンボ可)
  • 回避:無敵ローリング
  • スキル:ショートカット登録(最大4つ)
  • 運命アクション:専用ボタン+スティックで選択(巻き戻し/未来視など)

4-2. バトルシステム

  • 形式:シームレスなフィールドアクション
  • パーティ:主人公+AI仲間(最大2人)
  • 仲間は「神狩り支持派」「反神狩り派」など思想が分かれ、
    選択次第で離反・加入も変化。
  • 運命ゲージシステム(コア)
  • 攻撃・被弾・敵撃破でゲージが溜まる。
  • 消費して以下を使用:
    • 巻き戻し:数秒前の状態に戻る(大技を食らう前など)
    • 未来視:数秒先の敵行動を“残像”として表示
  • → 「理不尽な死」を減らしつつ、ギリギリを攻める緊張感を演出。

5. 世界観・ストーリー概要(要約)

  • 遥か昔、世界は八柱の神々によって創られた。
  • しかし神々は次第に“人間を資源として消費する存在”となり、
    祈りと生贄を要求するようになった。
  • 主人公は「神を狩るために作られた“器”」として生まれ、
    自らの存在理由と世界の真実の間で揺れ動く。
  • メインテーマ:
  • 「神を殺してでも救いたい世界があるのか?」
  • 「世界のために、個人をどこまで犠牲にできるのか?」

(※ここはあとから一緒に掘り下げてもOK)


6. 開発スコープ(ざっくり)

  • メインストーリー:20〜30時間
  • サブクエスト・やり込み:+20〜40時間
  • フィールド:
  • 拠点都市1
  • 周辺の森/廃墟/雪山/砂漠/神殿など、エリア数 8〜10想定
  • 神ボス:6〜8体+裏ボス

7. マーケティングのフック

  • 「#神狩りRPG」でSNSハッシュタグ展開。
  • 序盤の神戦1体分を体験版として無料配信し、配信OKにする。
  • “神を倒すか救うか”でエンディング分岐 → 配信者の間でルート比較が生まれる設計。

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」はいつ出そう?

公開情報とリークを全部まとめて、ジョブズ風に乱暴に整理すると:

  1. 2025年:M5版 Vision Pro(マイナーアップデート) → これはもう発表済み。Lifewire
  2. 2026年:新しい XR/メガネ系ハードは出ない見込み(アナリストレポート複数が「2026年は空白」と予測)。Medium+1
  3. 2027年:廉価版 Vision Air が量産&発売(予定)UploadVR+1
  4. そのあと: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 向けに本命のフルスケール版を投下する期間

ってロードマップで考えるのが現実的だと思う。

二次裏のクローンサイト

tyosuke20xx.com/nijiura.html


<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>二次裏クローン(ローカルHTML版・強化)</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <style>
        /* ------------------------------
           全体レイアウト / ベーススタイル
        ------------------------------ */
        body {
            background: #f2f2e9;
            color: #333;
            font-family: "YuGothic", "Hiragino Kaku Gothic ProN", Meiryo, sans-serif;
            font-size: 14px;
            margin: 0;
            padding: 0;
        }
        header {
            background: #d8d8c0;
            padding: 10px;
            border-bottom: 1px solid #b0b08f;
        }
        header h1 {
            margin: 0;
            font-size: 18px;
        }
        header small {
            display: block;
            font-size: 11px;
            color: #555;
        }
        .container {
            width: 95%;
            max-width: 900px;
            margin: 10px auto 40px auto;
            background: #fff;
            border: 1px solid #ccc;
            padding: 10px 15px 20px 15px;
            box-sizing: border-box;
        }
        a {
            color: #0044cc;
            text-decoration: none;
            cursor: pointer;
        }
        a:hover {
            text-decoration: underline;
        }
        .hidden {
            display: none;
        }

        /* ------------------------------
           上部ナビ・ステータス
        ------------------------------ */
        .board-nav {
            font-size: 12px;
            padding: 5px 0 8px 0;
            border-bottom: 1px solid #ddd;
            margin-bottom: 10px;
            display: flex;
            flex-wrap: wrap;
            gap: 8px;
            align-items: center;
        }
        .board-nav .nav-left {
            flex: 1 1 auto;
        }
        .board-nav .nav-right {
            flex: 1 1 auto;
            text-align: right;
        }
        .board-nav label {
            font-size: 12px;
            margin-left: 6px;
        }
        .board-nav select,
        .board-nav input[type="text"] {
            font-size: 12px;
            padding: 2px 4px;
        }
        .board-nav input[type="checkbox"] {
            vertical-align: middle;
        }
        .stats {
            font-size: 11px;
            color: #555;
        }

        /* ------------------------------
           スレッド一覧
        ------------------------------ */
        .thread-list {
            margin-bottom: 20px;
        }
        .thread-item {
            border-bottom: 1px dotted #ccc;
            padding: 5px 0;
            display: flex;
            align-items: flex-start;
            gap: 4px;
        }
        .thread-main {
            flex: 1 1 auto;
        }
        .thread-item:last-child {
            border-bottom: none;
        }
        .thread-title {
            font-weight: bold;
        }
        .thread-category {
            font-size: 11px;
            color: #444;
            background: #f0f0d8;
            padding: 1px 4px;
            border-radius: 3px;
            margin-right: 4px;
        }
        .meta {
            font-size: 11px;
            color: #666;
        }

        /* お気に入りスター */
        .fav-toggle {
            font-size: 16px;
            cursor: pointer;
            user-select: none;
            padding: 0 2px;
        }
        .fav-true {
            color: #d89a00;
        }
        .fav-false {
            color: #ccc;
        }

        /* ------------------------------
           フォーム共通
        ------------------------------ */
        .form-block {
            margin: 20px 0;
            padding: 10px;
            background: #f7f7ee;
            border: 1px solid #ddd;
        }
        .form-block h2 {
            margin: 0 0 5px 0;
            font-size: 14px;
        }
        .form-row {
            margin-bottom: 5px;
        }
        label {
            font-size: 12px;
            display: inline-block;
            width: 70px;
        }
        input[type="text"],
        textarea,
        select {
            width: 90%;
            max-width: 500px;
            box-sizing: border-box;
            font-size: 13px;
        }
        textarea {
            height: 80px;
        }
        input[type="submit"],
        button {
            font-size: 12px;
            padding: 3px 10px;
            margin-right: 4px;
        }
        .notice {
            font-size: 11px;
            color: #999;
            margin-top: 5px;
        }

        /* ------------------------------
           レス表示
        ------------------------------ */
        .posts {
            margin-top: 10px;
        }
        .post {
            border-top: 1px dotted #ccc;
            padding: 5px 0;
        }
        .post:first-child {
            border-top: none;
        }
        .post-header {
            font-size: 12px;
            margin-bottom: 3px;
        }
        .post-body {
            font-size: 13px;
            white-space: pre-wrap;
            word-wrap: break-word;
        }
        .post-id {
            font-family: "Consolas", "Menlo", monospace;
            font-size: 11px;
            color: #666;
        }
        .post-no-link {
            font-weight: bold;
        }

        /* ------------------------------
           スレッドビューのナビ
        ------------------------------ */
        .nav-top {
            margin-bottom: 10px;
            font-size: 12px;
        }

        /* ------------------------------
           データツール用エリア
        ------------------------------ */
        #backup-text {
            width: 100%;
            max-width: 100%;
            box-sizing: border-box;
            font-size: 12px;
        }

        /* ------------------------------
           設定ブロック
        ------------------------------ */
        #settings-block label {
            width: auto;
        }
        #settings-block input[type="checkbox"] {
            width: auto;
        }
        #setting-last-name {
            width: 200px;
        }

        /* ------------------------------
           レスポンシブ調整
        ------------------------------ */
        @media (max-width: 600px) {
            label {
                display: block;
                width: auto;
                margin-bottom: 2px;
            }
            input[type="text"],
            textarea,
            select {
                width: 100%;
            }
            .board-nav {
                flex-direction: column;
                align-items: flex-start;
            }
            .board-nav .nav-right {
                text-align: left;
            }
            .thread-item {
                flex-direction: row;
            }
        }
    </style>
</head>
<body>
<header>
    <h1>二次裏クローン(ローカルHTML版・強化)</h1>
    <small>※ブラウザの localStorage に保存 / 本家とは一切関係ありません</small>
</header>

<div class="container" id="top">
    <!-- スレ一覧ビュー -->
    <div id="view-list">
        <div class="board-nav">
            <div class="nav-left">
                <span class="stats">
                    スレ数:<span id="stat-threads">0</span> /
                    総レス数:<span id="stat-posts">0</span>
                </span>
                <label>
                    カテゴリ
                    <select id="category-filter">
                        <option value="">全カテゴリ</option>
                        <option value="雑談">雑談</option>
                        <option value="ゲーム">ゲーム</option>
                        <option value="アニメ">アニメ</option>
                        <option value="ニュース">ニュース</option>
                        <option value="その他">その他</option>
                    </select>
                </label>
                <label>
                    <input type="checkbox" id="fav-only">
                    お気に入りのみ
                </label>
            </div>
            <div class="nav-right">
                <label>
                    並び替え
                    <select id="sort-select">
                        <option value="updated">更新順</option>
                        <option value="created">作成順</option>
                        <option value="id">スレ番号順</option>
                    </select>
                </label>
                <label>
                    検索
                    <input type="text" id="search-input" placeholder="タイトル・本文から検索">
                </label>
            </div>
        </div>

        <h2>スレッド一覧</h2>
        <div id="thread-list" class="thread-list">
            <!-- JSで一覧を描画 -->
        </div>

        <div class="form-block">
            <h2>新規スレッド作成</h2>
            <form id="new-thread-form">
                <div class="form-row">
                    <label>名前</label>
                    <input type="text" name="name" id="new-name" placeholder="名無し">
                </div>
                <div class="form-row">
                    <label>カテゴリ</label>
                    <select name="category" id="new-category">
                        <option value="雑談">雑談</option>
                        <option value="ゲーム">ゲーム</option>
                        <option value="アニメ">アニメ</option>
                        <option value="ニュース">ニュース</option>
                        <option value="その他">その他</option>
                    </select>
                </div>
                <div class="form-row">
                    <label>タイトル</label>
                    <input type="text" name="title" required>
                </div>
                <div class="form-row">
                    <label>本文</label>
                    <textarea name="body" required></textarea>
                </div>
                <div class="form-row">
                    <input type="submit" value="スレ立て">
                </div>
                <div class="notice">
                    ※超シンプルローカル実装。データはこのブラウザ内だけに保存されます。<br>
                    ※別ブラウザ・シークレットモードでは共有されません。
                </div>
            </form>
        </div>

        <!-- ローカルデータ管理ツール -->
        <div class="form-block" id="data-tools">
            <h2>ローカルデータ管理</h2>
            <p class="notice">
                このPC / ブラウザだけで使う簡易ツールです。<br>
                別環境へ移したい場合は JSON をエクスポートして保存してください。
            </p>
            <div class="form-row">
                <button type="button" id="btn-export">エクスポート</button>
                <button type="button" id="btn-import">インポート</button>
                <button type="button" id="btn-clear">全消去</button>
            </div>
            <div class="form-row">
                <textarea id="backup-text" rows="4" placeholder="エクスポートしたJSONがここに出力されます。ここに貼り付けてインポートもできます。"></textarea>
            </div>
        </div>

        <!-- 簡易設定 -->
        <div class="form-block" id="settings-block">
            <h2>ミニ設定</h2>
            <div class="form-row">
                <label>
                    <input type="checkbox" id="opt-autoname">
                    名前を記憶して自動で入れる
                </label>
            </div>
            <div class="form-row">
                <label>前回の名前</label>
                <input type="text" id="setting-last-name" placeholder="名無し">
            </div>
            <div class="form-row">
                <button type="button" id="btn-save-settings">設定を保存</button>
            </div>
            <div class="notice">
                ※名前を記憶しておくと、新規スレ・レス投稿時の名前欄に自動で反映されます。<br>
                ※これらも localStorage に保存されます。
            </div>
        </div>
    </div>

    <!-- 個別スレッドビュー -->
    <div id="view-thread" class="hidden">
        <div class="nav-top">
            <a id="back-to-list">&lt;&lt; スレ一覧に戻る</a> |
            <a href="#bottom">▼ 一番下へ</a> |
            <a href="#top">▲ ページ先頭へ</a>
        </div>

        <h2 id="thread-title"></h2>
        <div class="meta" id="thread-meta"></div>

        <div id="posts" class="posts">
            <!-- JSでレスを描画 -->
        </div>

        <div class="form-block">
            <h2>レスを書く</h2>
            <form id="reply-form">
                <div class="form-row">
                    <label>名前</label>
                    <input type="text" name="name" id="reply-name" placeholder="名無し">
                </div>
                <div class="form-row">
                    <label>本文</label>
                    <textarea name="body" id="reply-body" required></textarea>
                </div>
                <div class="form-row">
                    <input type="submit" value="レス投稿">
                </div>
                <div class="notice">
                    ※レス番号をクリックすると本文に「>>番号」が入ります。<br>
                    ※画像アップロード機能はこのHTML版には入れていません。<br>
                    ※本格運用したい場合はPHPやDB版で実装してください。
                </div>
            </form>
        </div>
    </div>
</div>

<div id="bottom"></div>

<script>
(function() {
    "use strict";

    const STORAGE_KEY = "nijiura_clone_threads_v1";
    const SETTINGS_KEY = "nijiura_clone_settings_v1";

    // --------------------
    // データ構造
    // --------------------
    // threads = [
    //   {
    //     id: number,
    //     title: string,
    //     category: string,
    //     favorite: boolean,
    //     createdAt: string,
    //     updatedAt: string,
    //     posts: [
    //       { id, name, body, createdAt, uid }
    //     ]
    //   }, ...
    // ]
    //
    // settings = {
    //   autoName: boolean,
    //   lastName: string
    // }

    let threads = loadThreads();
    let settings = loadSettings();
    let currentThreadId = null;
    let currentSort = "updated"; // updated / created / id

    const viewList = document.getElementById("view-list");
    const viewThread = document.getElementById("view-thread");

    const threadListEl = document.getElementById("thread-list");
    const threadTitleEl = document.getElementById("thread-title");
    const threadMetaEl = document.getElementById("thread-meta");
    const postsEl = document.getElementById("posts");

    const newThreadForm = document.getElementById("new-thread-form");
    const replyForm = document.getElementById("reply-form");
    const backToListBtn = document.getElementById("back-to-list");

    const searchInput = document.getElementById("search-input");
    const sortSelect = document.getElementById("sort-select");
    const categoryFilter = document.getElementById("category-filter");
    const favOnlyCheckbox = document.getElementById("fav-only");

    const statThreadsEl = document.getElementById("stat-threads");
    const statPostsEl = document.getElementById("stat-posts");

    const btnExport = document.getElementById("btn-export");
    const btnImport = document.getElementById("btn-import");
    const btnClear = document.getElementById("btn-clear");
    const backupText = document.getElementById("backup-text");

    const optAutoname = document.getElementById("opt-autoname");
    const settingLastName = document.getElementById("setting-last-name");
    const btnSaveSettings = document.getElementById("btn-save-settings");

    const newNameInput = document.getElementById("new-name");
    const newCategorySelect = document.getElementById("new-category");
    const replyNameInput = document.getElementById("reply-name");
    const replyBodyTextarea = document.getElementById("reply-body");

    // 既存データにカテゴリ・お気に入り・UIDがなければ補完
    migrateThreads();

    // 初期ソート&描画
    sortThreadsByUpdated();
    applySettingsToUI();
    renderThreadList();
    showListView();

    // --------------------
    // イベント: スレ立て
    // --------------------
    newThreadForm.addEventListener("submit", function(e) {
        e.preventDefault();
        const formData = new FormData(newThreadForm);
        let name = (formData.get("name") || "").toString().trim();
        const category = (formData.get("category") || "雑談").toString();
        const title = (formData.get("title") || "").toString().trim();
        const body = (formData.get("body") || "").toString().trim();

        if (!name) {
            name = "名無し";
        }

        if (!title || !body) {
            alert("タイトルと本文は必須です。");
            return;
        }

        const now = getNowStr();
        const newId = getNewThreadId();

        const newThread = {
            id: newId,
            title: title,
            category: category || "雑談",
            favorite: false,
            createdAt: now,
            updatedAt: now,
            posts: [
                {
                    id: 1,
                    name: name,
                    body: body,
                    createdAt: now,
                    uid: generateUid(newId, 1, name, body, now)
                }
            ]
        };

        threads.push(newThread);
        sortThreadsByUpdated();
        saveThreads(threads);

        // 名前を設定に反映
        updateLastNameSetting(name);

        newThreadForm.reset();
        currentSort = "updated";
        sortSelect.value = "updated";

        openThread(newId);
    });

    // --------------------
    // イベント: レス投稿
    // --------------------
    replyForm.addEventListener("submit", function(e) {
        e.preventDefault();
        if (currentThreadId === null) return;

        const formData = new FormData(replyForm);
        let name = (formData.get("name") || "").toString().trim();
        const body = (formData.get("body") || "").toString().trim();

        if (!name) {
            name = "名無し";
        }
        if (!body) {
            alert("本文は必須です。");
            return;
        }

        const thread = threads.find(t => t.id === currentThreadId);
        if (!thread) {
            alert("スレッドが見つかりません。");
            return;
        }

        const now = getNowStr();
        const newPostId = getNewPostId(thread);

        thread.posts.push({
            id: newPostId,
            name: name,
            body: body,
            createdAt: now,
            uid: generateUid(thread.id, newPostId, name, body, now)
        });
        thread.updatedAt = now;

        sortThreadsByUpdated();
        saveThreads(threads);

        // 名前を設定に反映
        updateLastNameSetting(name);

        replyForm.reset();

        renderThread(thread);
        renderThreadList(); // 更新日時が変わるので一覧も更新
    });

    // --------------------
    // イベント: 一覧に戻る
    // --------------------
    backToListBtn.addEventListener("click", function() {
        currentThreadId = null;
        showListView();
    });

    // --------------------
    // イベント: 並び替え&検索&フィルタ
    // --------------------
    sortSelect.addEventListener("change", function() {
        currentSort = this.value;
        renderThreadList();
    });

    searchInput.addEventListener("input", function() {
        renderThreadList();
    });

    categoryFilter.addEventListener("change", function() {
        renderThreadList();
    });

    favOnlyCheckbox.addEventListener("change", function() {
        renderThreadList();
    });

    // --------------------
    // イベント: データツール
    // --------------------
    btnExport.addEventListener("click", function() {
        try {
            const json = JSON.stringify(threads, null, 2);
            backupText.value = json;
            alert("現在のデータをJSONとして出力しました。必要ならコピーして保存してください。");
        } catch (e) {
            console.warn("export failed:", e);
            alert("エクスポートに失敗しました。");
        }
    });

    btnImport.addEventListener("click", function() {
        const text = backupText.value.trim();
        if (!text) {
            alert("インポートするJSONが入力されていません。");
            return;
        }
        if (!confirm("テキストエリアのJSONで現在のデータを上書きします。よろしいですか?")) {
            return;
        }
        try {
            const data = JSON.parse(text);
            if (!Array.isArray(data)) {
                alert("JSONの形式が不正です。(配列ではありません)");
                return;
            }
            threads = data;
            migrateThreads(); // 新フィールドを補完
            sortThreadsByUpdated();
            saveThreads(threads);
            currentThreadId = null;
            showListView();
            alert("インポートに成功しました。");
        } catch (e) {
            console.warn("import failed:", e);
            alert("JSONの解析に失敗しました。形式が正しいか確認してください。");
        }
    });

    btnClear.addEventListener("click", function() {
        if (!confirm("本当に全データを削除しますか?(取り消しできません)")) {
            return;
        }
        try {
            localStorage.removeItem(STORAGE_KEY);
        } catch (e) {
            console.warn("clear failed:", e);
        }
        threads = [];
        currentThreadId = null;
        renderThreadList();
        showListView();
        alert("全データを削除しました。");
    });

    // --------------------
    // イベント: 設定
    // --------------------
    btnSaveSettings.addEventListener("click", function() {
        const autoName = !!optAutoname.checked;
        const lastName = (settingLastName.value || "").toString().trim() || "名無し";
        settings.autoName = autoName;
        settings.lastName = lastName;
        saveSettings(settings);
        applySettingsToUI();
        alert("設定を保存しました。");
    });

    // --------------------
    // イベント: レス番クリック(>>アンカー挿入)
    // --------------------
    postsEl.addEventListener("click", function(e) {
        const target = e.target;
        if (target && target.classList.contains("post-no-link")) {
            e.preventDefault();
            const no = target.getAttribute("data-no");
            if (!no) return;
            insertAnchorToReply(">>" + no + "\n");
        }
    });

    // --------------------
    // ビュー切替
    // --------------------
    function showListView() {
        viewList.classList.remove("hidden");
        viewThread.classList.add("hidden");
        renderThreadList();
    }

    function showThreadView() {
        viewList.classList.add("hidden");
        viewThread.classList.remove("hidden");
    }

    // --------------------
    // レンダリング: 一覧
    // --------------------
    function renderThreadList() {
        updateStats();

        if (!threads.length) {
            threadListEl.innerHTML = "<p>まだスレッドはありません。</p>";
            return;
        }

        const query = (searchInput.value || "").toString().trim();
        const filterCategory = (categoryFilter.value || "").toString();
        const favOnly = !!favOnlyCheckbox.checked;

        let list = threads.slice();

        // 検索
        if (query) {
            list = list.filter(function(t) {
                const q = query;
                if (t.title && t.title.indexOf(q) !== -1) return true;
                if (Array.isArray(t.posts)) {
                    return t.posts.some(function(p) {
                        return p.body && p.body.indexOf(q) !== -1;
                    });
                }
                return false;
            });
        }

        // カテゴリフィルタ
        if (filterCategory) {
            list = list.filter(function(t) {
                return (t.category || "雑談") === filterCategory;
            });
        }

        // お気に入りのみ
        if (favOnly) {
            list = list.filter(function(t) {
                return !!t.favorite;
            });
        }

        // 並び替え
        if (currentSort === "created") {
            list.sort(function(a, b) {
                if (a.createdAt < b.createdAt) return 1;
                if (a.createdAt > b.createdAt) return -1;
                return 0;
            });
        } else if (currentSort === "id") {
            list.sort(function(a, b) {
                return b.id - a.id; // 新しい番号が上
            });
        } else {
            // 更新順は threads 自体を sortThreadsByUpdated で管理しているのでそのまま
            // ただしフィルタ・検索後も順序を維持するだけ
            list.sort(function(a, b) {
                if (a.updatedAt < b.updatedAt) return 1;
                if (a.updatedAt > b.updatedAt) return -1;
                return 0;
            });
        }

        if (!list.length) {
            threadListEl.innerHTML = "<p>条件に一致するスレッドはありません。</p>";
            return;
        }

        let html = "";
        list.forEach(function(t) {
            const cat = t.category || "雑談";
            const fav = !!t.favorite;
            const favClass = fav ? "fav-true" : "fav-false";
            const favSymbol = fav ? "★" : "☆";

            html += `
                <div class="thread-item">
                    <span class="fav-toggle ${favClass}" data-thread-id="${t.id}" title="お気に入り切り替え">${favSymbol}</span>
                    <div class="thread-main">
                        <span class="thread-title">
                            <span class="thread-category">[${escapeHtml(cat)}]</span>
                            <a data-thread-id="${t.id}" class="thread-link">
                                ${escapeHtml(t.title)}
                            </a>
                        </span><br>
                        <span class="meta">
                            No.${t.id} / 作成:${escapeHtml(t.createdAt)} /
                            最終更新:${escapeHtml(t.updatedAt)} /
                            レス:${t.posts ? t.posts.length : 0}
                        </span>
                    </div>
                </div>
            `;
        });
        threadListEl.innerHTML = html;

        // スレッドリンクイベント
        const links = threadListEl.querySelectorAll(".thread-link");
        links.forEach(function(link) {
            link.addEventListener("click", function() {
                const id = parseInt(this.getAttribute("data-thread-id"), 10);
                openThread(id);
            });
        });

        // お気に入り切り替えイベント
        const favToggles = threadListEl.querySelectorAll(".fav-toggle");
        favToggles.forEach(function(btn) {
            btn.addEventListener("click", function() {
                const id = parseInt(this.getAttribute("data-thread-id"), 10);
                toggleFavorite(id, this);
            });
        });
    }

    // --------------------
    // レンダリング: 個別スレ
    // --------------------
    function renderThread(thread) {
        currentThreadId = thread.id;

        const cat = thread.category || "雑談";
        threadTitleEl.textContent = "[" + cat + "] " + thread.title;
        threadMetaEl.textContent =
            "スレ番号:" + thread.id +
            " 作成:" + thread.createdAt +
            " 最終更新:" + thread.updatedAt +
            " レス数:" + (thread.posts ? thread.posts.length : 0);

        if (!thread.posts || !thread.posts.length) {
            postsEl.innerHTML = "<p>まだレスはありません。</p>";
            return;
        }

        let html = "";
        thread.posts.forEach(function(p) {
            const uid = p.uid || generateUid(thread.id, p.id, p.name, p.body, p.createdAt);
            html += `
                <div class="post">
                    <div class="post-header">
                        <a href="#" class="post-no-link" data-no="${p.id}">No.${p.id}</a>
                        名前:${escapeHtml(p.name)} 
                        投稿日:${escapeHtml(p.createdAt)} 
                        ID:<span class="post-id">${escapeHtml(uid)}</span>
                    </div>
                    <div class="post-body">
                        ${escapeHtml(p.body).replace(/\n/g, "<br>")}
                    </div>
                </div>
            `;
        });
        postsEl.innerHTML = html;

        // 自動名前反映
        applyAutoNameToReply();
    }

    function openThread(id) {
        const thread = threads.find(t => t.id === id);
        if (!thread) {
            alert("スレッドが見つかりません。");
            return;
        }
        renderThread(thread);
        showThreadView();
    }

    // --------------------
    // お気に入り切り替え
    // --------------------
    function toggleFavorite(threadId, element) {
        const thread = threads.find(t => t.id === threadId);
        if (!thread) return;
        thread.favorite = !thread.favorite;
        saveThreads(threads);

        const fav = !!thread.favorite;
        element.textContent = fav ? "★" : "☆";
        element.classList.toggle("fav-true", fav);
        element.classList.toggle("fav-false", !fav);
    }

    // --------------------
    // ストレージ操作
    // --------------------
    function loadThreads() {
        try {
            const raw = localStorage.getItem(STORAGE_KEY);
            if (!raw) return [];
            const data = JSON.parse(raw);
            if (!Array.isArray(data)) return [];
            return data;
        } catch (e) {
            console.warn("failed to load threads:", e);
            return [];
        }
    }

    function saveThreads(data) {
        try {
            localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
        } catch (e) {
            console.warn("failed to save threads:", e);
            alert("保存に失敗しました。localStorage容量オーバーの可能性があります。");
        }
    }

    function loadSettings() {
        try {
            const raw = localStorage.getItem(SETTINGS_KEY);
            if (!raw) {
                return {
                    autoName: false,
                    lastName: "名無し"
                };
            }
            const data = JSON.parse(raw);
            if (!data || typeof data !== "object") {
                return {
                    autoName: false,
                    lastName: "名無し"
                };
            }
            if (typeof data.autoName !== "boolean") {
                data.autoName = false;
            }
            if (typeof data.lastName !== "string") {
                data.lastName = "名無し";
            }
            return data;
        } catch (e) {
            console.warn("failed to load settings:", e);
            return {
                autoName: false,
                lastName: "名無し"
            };
        }
    }

    function saveSettings(data) {
        try {
            localStorage.setItem(SETTINGS_KEY, JSON.stringify(data));
        } catch (e) {
            console.warn("failed to save settings:", e);
        }
    }

    // --------------------
    // 統計更新
    // --------------------
    function updateStats() {
        statThreadsEl.textContent = threads.length;
        let totalPosts = 0;
        threads.forEach(function(t) {
            if (Array.isArray(t.posts)) {
                totalPosts += t.posts.length;
            }
        });
        statPostsEl.textContent = totalPosts;
    }

    // --------------------
    // ID・日付・エスケープ
    // --------------------
    function getNewThreadId() {
        if (!threads.length) return 1;
        const ids = threads.map(function(t) { return t.id; });
        return Math.max.apply(null, ids) + 1;
    }

    function getNewPostId(thread) {
        if (!thread.posts || !thread.posts.length) return 1;
        const ids = thread.posts.map(function(p) { return p.id; });
        return Math.max.apply(null, ids) + 1;
    }

    function getNowStr() {
        const d = new Date();
        const y = d.getFullYear();
        const m = ("0" + (d.getMonth() + 1)).slice(-2);
        const day = ("0" + d.getDate()).slice(-2);
        const h = ("0" + d.getHours()).slice(-2);
        const min = ("0" + d.getMinutes()).slice(-2);
        const s = ("0" + d.getSeconds()).slice(-2);
        return y + "/" + m + "/" + day + " " + h + ":" + min + ":" + s;
    }

    function escapeHtml(str) {
        return String(str)
            .replace(/&/g, "&amp;")
            .replace(/</g, "&lt;")
            .replace(/>/g, "&gt;")
            .replace(/"/g, "&quot;")
            .replace(/'/g, "&#39;");
    }

    // 簡易ハッシュでUIDを生成(8桁16進)
    function generateUid(threadId, postId, name, body, createdAt) {
        const src = String(threadId) + ":" + String(postId) + ":" +
                    String(name) + ":" + String(body) + ":" + String(createdAt);
        let hash = 0;
        for (let i = 0; i < src.length; i++) {
            hash = ((hash << 5) - hash) + src.charCodeAt(i);
            hash |= 0; // 32bit
        }
        // 符号を外す
        if (hash < 0) {
            hash = ~hash + 1;
        }
        let hex = hash.toString(16);
        if (hex.length < 8) {
            hex = ("00000000" + hex).slice(-8);
        } else if (hex.length > 8) {
            hex = hex.slice(-8);
        }
        return hex;
    }

    // --------------------
    // ソート
    // --------------------
    function sortThreadsByUpdated() {
        threads.sort(function(a, b) {
            if (a.updatedAt < b.updatedAt) return 1;
            if (a.updatedAt > b.updatedAt) return -1;
            return 0;
        });
    }

    // --------------------
    // 設定適用
    // --------------------
    function applySettingsToUI() {
        optAutoname.checked = !!settings.autoName;
        settingLastName.value = settings.lastName || "名無し";

        if (settings.autoName) {
            if (newNameInput) newNameInput.value = settings.lastName || "名無し";
            if (replyNameInput) replyNameInput.value = settings.lastName || "名無し";
        }
    }

    function applyAutoNameToReply() {
        if (settings.autoName) {
            replyNameInput.value = settings.lastName || "名無し";
        }
    }

    function updateLastNameSetting(name) {
        settings.lastName = name || "名無し";
        if (settings.autoName) {
            // UIへ即時反映
            settingLastName.value = settings.lastName;
            if (newNameInput) newNameInput.value = settings.lastName;
            if (replyNameInput) replyNameInput.value = settings.lastName;
        }
        saveSettings(settings);
    }

    // --------------------
    // レス番アンカー挿入
    // --------------------
    function insertAnchorToReply(text) {
        replyBodyTextarea.focus();
        const start = replyBodyTextarea.selectionStart;
        const end = replyBodyTextarea.selectionEnd;
        const value = replyBodyTextarea.value;
        replyBodyTextarea.value = value.slice(0, start) + text + value.slice(end);
        // キャレット位置を挿入したテキストの後ろに
        const pos = start + text.length;
        replyBodyTextarea.selectionStart = replyBodyTextarea.selectionEnd = pos;
    }

    // --------------------
    // 既存データのマイグレーション
    // --------------------
    function migrateThreads() {
        threads.forEach(function(t) {
            if (!t.category) {
                t.category = "雑談";
            }
            if (typeof t.favorite !== "boolean") {
                t.favorite = false;
            }
            if (!Array.isArray(t.posts)) {
                t.posts = [];
            }
            t.posts.forEach(function(p) {
                if (!p.uid) {
                    p.uid = generateUid(t.id, p.id, p.name || "名無し", p.body || "", p.createdAt || "");
                }
            });
        });
        saveThreads(threads);
    }
})();
</script>
</body>
</html>

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">
          次世代ソーシャルネットワーク &bull; 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 => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;','\'':'&#39;'}[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>

ゼノギアスリメイク企画書

1. プロジェクト概要

  • 企画名(仮):『Xenogears Rebirth(ゼノギアス リバース)(仮)』
  • ジャンル:ドラマティックRPG / ロボット(ギア)バトルRPG
  • 対象ハード
    • PlayStation 5 / PC(Steam) / 次世代Nintendoプラットフォーム
  • プレイ人数:1人
  • 想定プレイ時間:メインストーリー 60〜80時間+サブ要素 20〜40時間
  • 企画区分:既存IPリメイク(※本書は非公式ファン企画書)

2. 企画意図・コンセプト

2-1. 企画意図

  • 1990年代に発売された原作は、重厚なストーリー・哲学的テーマ・ギア(巨大ロボ)×人間ドラマが高く評価されている。
  • 一方で、
    • ハード性能制約による表現不足
    • 終盤の駆け足展開
    • 現代基準では不便なUI・テンポ
      といった課題も指摘されている。
  • 本リメイクでは、
    **「原作の本質と感情の揺さぶりを守りつつ、現代RPGとしての遊びやすさ・映像・テンポを刷新」**することを目標とする。

2-2. メインコンセプト

  1. 原作のドラマ性・テーマ性を忠実に再現
    • キャラクター性・世界観・大きな物語の流れ・名シーンの構造は尊重。
  2. ゲーム体験の再構築
    • 戦闘テンポ、探索、UI、カメラワークを現代基準に最適化。
  3. 後半シナリオの再表現
    • 当時表現しきれなかったシーンや背景を、プレイアブルパートやイベントとして補完。
  4. ギアバトルの“憧れ”を実現
    • 重厚さとスピード感の両立した、アクション性の高いハイブリッドバトル。

3. ターゲットユーザー

  1. 原作ファン層(30〜40代中心)
    • 当時プレイしたコアファン。
    • シナリオ重視・キャラ重視のRPGを好むユーザー。
  2. 現代のJRPGファン(10〜20代)
    • スタイリッシュなビジュアル、手触りの良いアクションRPGに慣れているユーザー。
    • 他社の大型RPG(FF・テイルズ・ペルソナなど)を遊ぶ層。
  3. ロボット・メカファン
    • アニメ調のロボットバトル、巨大兵器のスケール感に魅力を感じる層。

4. 世界観・ストーリーテーマ(高レベル)

※具体的なネタバレの細部ではなく、「リメイク方向性」を示すレベルに留める。

  • 核となるテーマ
    • 「自己との対話」「存在理由」「因果・輪廻」「人と機械・神話の境界」
  • トーン&ムード
    • 一見牧歌的な日常から、徐々に世界の根源的な闇へ降りていく構成。
    • シリアス中心だが、仲間同士の掛け合いや日常シーンで緩急を付ける。
  • リメイクでの方針
    • 後半の出来事を**ナレーション主体ではなく、可能な限り“実際に操作して体験させる”**形へ再構成。
    • 補完要素として、
      • 過去編の短いプレイアブルセクション
      • サブクエストで描かれる脇役の視点
        などを追加し、世界の厚み・説得力を強化。

5. ゲームシステム全体像

5-1. バトルシステム(人間戦 / ギア戦)

共通方針

  • ベースはコマンドRPG+コンボ(連携)システムを継承しつつ、
    入力タイミングや位置取りを取り入れたアクション性のあるハイブリッドにする。
  • 行動順やバフ・デバフなど、戦術RPGとしての深みを維持。

人間戦(オンフット)

  • 三ボタン程度の「弱・中・強」攻撃の組み合わせで、コンボ技を発動。
  • AP(アクションポイント)制で、行動配分・温存の駆け引きを残す。
  • 回避・ガード・距離管理など、ライトなアクション要素を追加。
  • 必殺技発動時はカットイン+カメラワーク強化。

ギア戦

  • 重量感・速度感を感じるカメラとSEで「巨大兵器を操る感覚」を強調。
  • 燃料・装甲・ブースト管理を要素にした中量級“シミュレーションアクション”寄りの手触り。
  • ギアごとにロール(格闘型 / 射撃型 / 支援型)を明確化し、パーティ編成の意味を強化。
  • 合体技・連携技は演出を刷新し、ボス戦のハイライトに。

5-2. フィールド・ダンジョン

  • シームレス3Dフィールド:ローディングを極力隠し、没入感を重視。
  • キャラアクション:
    • ダッシュ・ジャンプ・よじ登り・壁キックなど、原作の立体的マップを現代仕様にアップグレード。
  • ダンジョンデザイン:
    • ただ複雑にするのではなく、「ギミック+物語的意味」のある構造へ再調整。
    • ショートカット・簡易マップなどで迷いすぎストレスを軽減。

5-3. 町・拠点要素

  • 町では
    • NPCとの会話
    • ショップ
    • サブクエスト受注
    • 仲間との会話イベント(好感度イベントではなく、あくまでストーリー深堀り)
      を配置。
  • 宿屋・酒場などで**仲間同士の会話イベント(「キャンプトーク」的なもの)**を追加し、
    心情描写を補強。

6. キャラクターデザイン・ビジュアル

  • ビジュアルスタイル
    • “ハイエンドなアニメ調3D”を目指し、トゥーンシェーダ+物理ベースレンダリングを両立。
    • オリジナルの印象的なデザインを尊重し、細部・質感・装飾を現代的にアップデート。
  • ギアデザイン
    • シルエット・武装は原作のイメージを維持しつつ、関節ディテール・マーキング・発光表現で情報量を増加。
    • バトル中の変形・形態変化を一部ギアに追加し、差別化と盛り上げに利用。

7. サウンド・音楽

  • BGM
    • 原作楽曲をフルオーケストラ・バンド・民族楽器などで再アレンジ。
    • 要所では原曲アレンジを使用し、ファン心理を刺激。
  • ボイス
    • メインストーリーはフルボイスを基本とし、一部サブイベントもボイス対応。
  • SE
    • ギアの駆動音・ブースト音・衝撃音をハイレゾ化し、“質量感”を音でも表現。

8. UI/UX・快適さ

  • 戦闘速度は**3段階調整(標準 / 早い / 非常に早い)**を用意。
  • オートバトル・オートコンボ機能(難易度EASY向け)を搭載。
  • ログ機能、イベントリプレイ機能で、物語の振り返りをサポート。
  • ミニマップ&クエストガイド(オン/オフ可能)で迷子ストレスを軽減。

9. 新規要素(リメイクオリジナル)

  1. サブクエスト強化
    • 主要都市・村ごとに「世界観を深めるサブストーリー」を用意。
    • 一部はギア専用サブクエスト(闘技場・模擬戦・試作機テストなど)。
  2. ギアカスタマイズ拡張
    • 外見は原作イメージを壊さない範囲で、カラー・マーキング・エンブレム変更を許可。
    • パーツ・チップによる能力カスタム要素を拡張。
  3. フォトモード
    • ギア・キャラクターを撮影できるフォトモードを実装し、SNS映えを意識。
  4. ギャラリーモード
    • イラスト・3Dモデル・BGM・過去PVなどを閲覧可能なモードを搭載。
    • リメイク制作のメイキング要素を一部公開し、ファン満足度を高める。

10. 開発体制(想定)

  • 開発期間:36〜42ヶ月
  • チーム規模:メイン開発 80〜120名規模
    • ディレクター:1名
    • プロデューサー:1〜2名
    • シナリオ・演出:数名(原作関係者+新規ライター)
    • プログラマー:15〜25名
    • アーティスト(キャラ・ギア・背景):25〜35名
    • UI/UX:3〜5名
    • サウンド:3〜5名
    • QA:外部含め多数

11. 開発スケジュール(ラフ)

  1. プリプロダクション(〜6ヶ月)
    • リメイク範囲・シナリオ再構成方針の決定
    • 映像コンセプト・バトルプロトタイプ作成
  2. プロダクション前半(〜18ヶ月)
    • メインストーリー実装
    • バトルシステム・ギアシステムの実装と調整
  3. プロダクション後半(〜30ヶ月)
    • サブクエスト・追加要素・UI/UX実装
    • バランス調整・最適化
  4. ポリッシュ・QA(〜36〜42ヶ月)
    • デバッグ・チューニング・ローカライズ
    • マーケティング連動施策準備

12. ビジネス・収益計画(案)

  • 販売形態:フルプライスパッケージ+DL版(DLCは最小限)
  • DLC案
    • サウンドトラック拡張
    • ビジュアルアートブック(デジタル)
    • ギャラリー用メイキング映像など、世界観を壊さない“資料系DLC”
  • 訴求ポイント
    • 「伝説的RPGの決定版リメイク」
    • 「当時の未完感を、現代の技術と表現で再構築」
    • 「ギアバトル×重厚ストーリーの最高峰を、最新ハードで」

13. 注意事項(IP・ブランド)

  • 本企画書は非公式ファンメイドの仮想リメイク案であり、
    実際の権利は原作の権利元(スクウェア・エニックス等)に帰属する。
  • 実際に商業利用を行う場合は、
    • 権利元への企画提案
    • 商標・著作権の正式なライセンス契約
      が必須となる。

AIR2企画書

1. タイトル案

  • 仮タイトル:「AIR2 – 空に還る約束 -」
    • 開発中の呼称として「AIR2」、権利上はサブタイトル部分で展開する想定。

2. 企画概要

  • ジャンル:ビジュアルノベル / 恋愛アドベンチャー
  • プレイ時間:1ルート 8〜10時間、フルコンプ 30〜40時間想定
  • 対応プラットフォーム:PC(Windows)、後発でコンシューマ / Steam 展開
  • ターゲット層
    • 10代後半〜30代のアニメ・ギャルゲー・泣きゲー ファン
    • 過去の「夏×感動系」作品が好きなユーザー
  • キーワード
    • 「夏の終わり」「空」「輪廻」「失われた時間」「叶わなかった約束」

3. コンセプト

  1. “空に還る”感動体験
    • 夏の終わりに訪れる「別れ」と「救い」を、プレイヤー自身の選択で体感させる。
  2. 複数ルートで一つの真実に収束
    • 各ヒロインルートで世界の断片が見え、すべてをクリアすることで“真相ルート”が解禁。
  3. 音楽と背景で“空気”を売るゲーム
    • BGM・背景・SE で、「蝉の声が聞こえてきそうな夏」の空気感を最大限に演出。

4. 世界観・舞台

  • 舞台:海沿いの小さな港町「空波町(そらなみちょう)」
    • 古くから「空へ還る人々」の伝承が残る町。
    • 夏になると観光客で賑わうが、どこか“取り残された時間”の気配が漂う。
  • 伝承
    • 「願いを叶えられなかった魂は、夏の終わりに空へ還り、もう一度だけこの町に戻ってくる」
    • 子どもたちの間では“空見様(そらみさま)”と呼ばれる謎の存在の噂がある。

5. 物語の骨子(大枠)

基本プロット

  • 主人公は、都会から空波町へとやってきた青年(or 高校生)。
  • 過去にこの町で過ごした記憶があるような“既視感”に悩まされている。
  • 町で出会うヒロインたちは、それぞれ「時間」や「記憶」に歪みを抱えている。
  • 各ルートで、主人公はヒロインの“未練”や“叶わなかった約束”に向き合い、
    最後に“空見様”と伝承の真実へ辿り着く。

章構成(イメージ)

  1. 序章:夏、空波町へ
    • 主人公が町に到着。ヒロインたちとの出会い。
  2. 日常パート
    • 学校・海辺・神社・古い灯台などで日常を積み重ねる。選択肢で好感度とルート分岐。
  3. 違和感の顕在化
    • ヒロインの周囲で時間のズレ・記憶の食い違い・デジャヴが増えていく。
  4. クライマックス
    • ヒロインごとの「過去の約束」や「失われた時間」の真相へ。
  5. エンディング
    • ルートごとに、ヒロインを“空へ還す”のか、“この世界につなぎとめる”のかが分岐。
  6. 真相ルート
    • 全ルートクリアで解禁。主人公自身の正体、空見様の真実、輪廻の構造が明かされる。

6. キャラクター案

■ 主人公(名前変更可)

  • 性別:男性
  • 設定:都会育ち。心にぽっかり穴が空いたような虚無感を抱え、療養も兼ねて空波町へ。
  • 特徴:時々“見たことのないはずの光景”を夢に見る。
  • 役割:プレイヤーの視点でありつつ、物語の“鍵”そのもの。

■ ヒロインA:海咲(みさき)

  • モチーフ:海 / ひまわり / 素直になれない
  • 性格:明るく元気だが、時折ふっと寂しそうな表情を見せる幼馴染ポジション。
  • 秘密:
    • 主人公とは「忘れているだけ」で深い過去のつながりがある。
    • ある“約束”のために、この町に“留まっている”存在かもしれない。

■ ヒロインB:静流(しずる)

  • モチーフ:神社 / 風鈴 / 祈り
  • 性格:穏やかで神秘的。少し天然で夢見がちな巫女。
  • 秘密:
    • 町に伝わる「空見様」の儀式を受け継ぐ家系。
    • 見えるはずのない“空の向こう側”を時々見てしまう。

■ ヒロインC:遥(はるか)

  • モチーフ:列車 / カメラ / 旅人
  • 性格:クールで現実主義だが、内に強い情熱を秘める転校生。
  • 秘密:
    • この町の“時間の歪み”を調査するために訪れている。
    • 主人公たちとは別の立場から、「輪廻のルール」を変えようとしている。

7. ゲームシステム・UI

■ システム

  • マルチルート型ビジュアルノベル
    • 選択肢によってヒロインルートへ分岐。
    • BAD / NORMAL / GOOD エンド+各TRUEエンドを想定。
  • 「空の記憶」システム
    • 物語中に散らばる“断片的な記憶”を収集。
    • メニュー画面で、集めた記憶のピースが「空のパネル」として並び、
      真相ルート解禁の鍵になる。
  • 日付・時間帯による演出
    • 朝・昼・夕方・夜で背景とBGMを変化させ、
      夏の一日がゆっくり終わっていく感覚を強調。

■ UI・ビジュアル

  • 画面構成
    • 16:9 フルHDを基本。
    • 下部に半透明のテキストウィンドウ、上部に広く空と背景が見えるレイアウト。
  • アートスタイル
    • 柔らかい線と、淡いパステル調の色彩。
    • 太陽光・逆光・空のグラデーションを重視した背景美術。
  • BGM・SE
    • ピアノ・アコースティックギター・ストリングスを中心にした切ないメロディ。
    • 波の音、蝉の声、風鈴など、夏の音を随所に入れて没入感を高める。

8. 開発体制・スケジュール(イメージ)

  • 開発規模
    • シナリオライター:1〜2名
    • 原画:1〜2名(キャラ / イベントCG)
    • 背景:1名 or 外注
    • BGM:1名
    • プログラマ / スクリプター:1〜2名
    • ディレクター / スクリプト管理:1名
  • スケジュール案(約18〜24ヶ月)
    1. 企画・プロット固め:3ヶ月
    2. シナリオ執筆・グラフィック発注:8〜10ヶ月
    3. 実装・デバッグ:4〜6ヶ月
    4. マスターアップ・宣伝準備:2〜3ヶ月

9. ビジネス・展開案

  • 販売形態
    • ダウンロード版 メイン
    • パッケージ版(限定版)でアートブック・サントラを同梱。
  • プロモーション
    • ティザーサイト公開 → キービジュアル・コンセプトムービー。
    • 主題歌MVを先行公開し、YouTube・SNSで話題づくり。
    • 体験版配布&配信者向けプレイ許諾で露出を拡大。

リアルワールド・リビルド ONLINE

① タイトル案

タイトル:リアルワールド・リビルド ONLINE
副題:「壊れた世界を“編集”するマルチワールドRPG」


② コンセプト

「プレイヤー全員がゲームマスター。世界のルールやマップを“編集”して遊ぶRPG」

  • ただのオープンワールドRPGではなく、
    プレイヤーがクエスト、街、ダンジョン、さらには“物理ルール”まで書き換えていく。
  • 他プレイヤーの作った世界に遠征し、評価・攻略・破壊・改造していく
    “マルチバース系オンラインRPG”。

③ ゲーム概要

  • ジャンル:オンラインアクションRPG / サンドボックス / クリエイト
  • 対応プラットフォーム:PC / コンソール(将来的にVR対応想定)
  • プレイ人数:ソロ~4人協力 / ワールド単位で最大数十人が同時接続

④ 世界観

  • かつて世界は「原初コード」と呼ばれる法則で完璧に保たれていた。
  • しかし何者かによって「デバッグ中のテスト世界」が流出。
    世界はテスト用の仮想ワールドと現実に似た本編ワールドが混ざり合った状態に。
  • プレイヤーは「リビルダー」と呼ばれる特異な存在。
    世界の“設定ファイル”に直接アクセスし、
    ・重力の向き
    ・時間の流れ
    ・魔法の属性
    ・モンスターの行動AI
    などを編集できる。

⑤ 物語のフック

  • チュートリアルの最後、
    「この世界はゲームではない。あなたが壊したら、本当に壊れる
    という謎のメッセージ。
  • メインストーリーでは、プレイヤーが改造してきたログが
    すべて“物語”として記録され、
    ラスボスは「プレイヤーが作ってきた世界の矛盾」が具現化した存在になる。

⑥ コアゲームシステム

1. リビルド・エディットシステム

  • 各エリアには「世界エディタ」があり、以下を編集可能:
    • 地形(崖・島・浮遊大陸など)
    • 罠・ギミック(動く床、即死トラップ、謎解き装置)
    • 敵の配置・行動パターン
    • 天候・時間・重力方向
  • 編集には「世界ポイント(WP)」が必要。
    → クエストクリアや他プレイヤーからの高評価で入手。

2. 他プレイヤーワールドへの侵攻&評価

  • 他プレイヤーが作ったダンジョンに挑戦できる。
  • クリア後に5段階評価+コメントでフィードバック。
    • 面白かった:WPボーナス
    • 鬼畜すぎた:ペナルティ or 別枠ランキング(鬼畜ランキング)

3. 自分のワールドが「コンテンツ」になる

  • 自分のダンジョンや街が「ポータル一覧」に登録される。
  • 人気ワールドはハブエリアに巨大ポータルとして常設。
  • 定期的に「今週の人気ワールド」「今月の狂気ワールド」などランキング表示。

⑦ キャラクターと成長要素

プレイヤー

  • クラスは存在せず、“編集スタイル”でビルドが変わる
    • ビルダー系:建築・罠・地形変化が得意
    • ハッカー系:敵AI・ギミックの論理を書き換えるのが得意
    • アバター系:外見や演出、演劇系イベント作りが得意
  • 戦闘スキルも、
    「自分が作った世界のルール」によって強化される
    (例:低重力世界で鍛えたプレイヤーはジャンプスキルが異常に強い)。

⑧ マルチプレイ要素

  • 共創ダンジョン制作
    • 2~4人で同時に1つのダンジョンを共同編集。
    • 役割分担:地形班、敵AI班、演出班など。
  • ワールドレイドバトル
    • コミュニティで巨大ボス戦イベント開催。
    • ボスの行動パターン自体もプレイヤーが編集し、「視聴型レイド」も可能。

⑨ 継続プレイの動機付け

  • 自分のワールドが遊ばれるほど、
    → ログが「世界の歴史」として本編ストーリーに反映される。
  • ワールドごとに
    • 閲覧数
    • クリア率
    • 平均死亡回数
    • 平均クリア時間
      などが可視化され、「ゲーム内アナリティクス」が見られる。

⑩ 企画のポイント(売り)

  1. 「遊ぶ」と「作る」がシームレス
    • マリオメーカー系の楽しさ + オープンワールドRPGの没入感。
  2. 毎回違う世界体験
    • 公式ストーリー+プレイヤー生成コンテンツで、寿命の長いタイトルを目指せる。
  3. “世界編集”というフック
    • 単なるステージエディタじゃなく、
      物理・ルール・AIまで触れることで、他と差別化。

光の歩み:パウロの航路

タイトル(仮)

「光の歩み:パウロの航路」
― 初期教会の旅路を体験し、希望・信頼・愛をゲームとして学ぶアドベンチャーRPG

1. 企画概要

  • ジャンル:物語主導アドベンチャーRPG+探索+対話パズル(非殺傷系)
  • 対象:中高生〜大人。信仰の有無を問わず、歴史物語や倫理的選択を楽しみたい人
  • プラットフォーム:PC/コンソール(Switch/PS/Xbox)+後発でモバイル簡易版
  • プレイ時間:本編10〜15時間+周回要素
  • レーティング想定:CERO B(暴力表現なし、差別・扇動なし)

2. 企画意図・学習目標

  • 初期キリスト教の歴史的背景(1世紀ローマ世界・ディアスポラ都市)を偏りなく紹介
  • 価値観(信仰=Faith/希望=Hope/愛=Charity)を、選択と結果で体験的に理解
  • 実在地・史料に基づく考証のもと、**布教=説得ではなく「対話と理解」**を主眼に

3. コア体験(Game Pillars)

  1. 対話と調停:噂・誤解・利害が交差する街で、中立的に聞き取り、誠実に橋渡し
  2. 旅と共同体:船旅で各都市へ。資源(時間・食糧・寄付)と仲間の士気を管理
  3. 徳の選択:状況に応じて「信仰/希望/愛」を強化する行動を選ぶ(暴力回避)
  4. 史的没入:当時の市場、会堂、家屋教会、ローマ法廷、港湾などを探索し資料収集

4. ゲームループ

  • 街に到着 → 情報収集(傾聴) → 対話パズル(調停/説得/証言の整合) → 共同体支援(施し/労働/手紙) → 船旅準備(資源管理) → 次の地へ
  • 章ごとに手紙(Epistle)が解禁され、読了で小さな洞察ブースト(対話オプション解放)

5. メカニクス

  • 徳メーター:Faith / Hope / Charity(数値でなく「状態」重視。偏らず三徳のバランスが良いと特殊イベント)
  • 祈り(クールダウン制):混乱時に視点を整える。NPCの本心“キーワード”が一瞬だけ可視化
  • 証言カード:聞き取った事実・比喩・譬えの断片。論理矛盾がない並べ方で対話パズルを解く
  • 共同体ボード:困窮、病、孤立、迫害の度合いを可視化。資源配分で街の状態が改善しクエストが変化
  • 非殺傷コンバット:逃走・隠密・法廷弁明・保証人立て等で危機回避(QTE最小)

6. 物語構成(例)

  • プロローグ:ダマスコ(出会いと転機)
  • 第1章:アンティオキア(多文化の街での誤解解消)
  • 第2章:ピシディアのアンティオキア〜リストラ(迫害と連帯)
  • 第3章:コリント(商人ギルドと信仰共同体の摩擦を調停)
  • 第4章:エフェソス(職人組合と偶像問題、平和的収束)
  • 最終章:ローマ(法と良心、希望の証言。エンディング分岐=三徳の在り方)

7. 美術・音響

  • アート:温かい筆致のセミリアル。衣装・建築は史料参照、象徴表現はやさしく
  • UI:巻物/羊皮紙モチーフ。徳メーターは三つの小さな灯火
  • 音楽:弦・笛・竪琴+現代的サウンドスケープ。祈り時は環境音がやわらぐ

8. アクセシビリティ

  • 朗読モード(本文読み上げ)、字幕・フォントサイズ調整、色弱配慮配色
  • 難易度:ストーリーフォーカス/スタンダード/考証チャレンジ(史料クイズ追加)

9. リサーチと表現ガイドライン

  • 宗派横断の監修委員会(神学・歴史・考古の3名以上)を設置
  • 固有名詞・儀礼の描写は史料脚注に準拠、論争点は断定せず複数説を匂わせる
  • 勧誘的・攻撃的表現は避ける。他宗教・文化への敬意を必須要件に

10. 進行・成長

  • 徳の熟達で新しい対話フレーズ、比喩(譬え話)カードが解禁
  • 街レベルが上がると共同体ボードの課題が高度化(教育、孤児支援、介護など)

11. マネタイズ

  • 基本買い切り(DLCで「ガリラヤの物語集」「詩編黙想パック」など追加コンテンツ)
  • 収益の一部を現実のチャリティに寄付する連動企画(透明性レポートをゲーム内掲示板で公開)

12. 競合・差別化

  • 既存の宗教題材ゲームは説話紹介が中心。
    → 本作は**“非対立のデザイン”**(調停パズル×徳システム)で能動的体験を提供。

13. 技術スタック

  • Unity(ユーザーの希望に合致):URP、Yamlローカライズ、Ink/Dialogue System、Timeline
  • セーブはJSON+暗号化、PCとSwitchを初期ターゲット
  • 字幕・スクリプトは外部CSVで管理し監修者が直接校正可能

14. 開発体制・期間(目安)

  • チーム:10〜14名(Pd/PM1、リードデザ1、レベルデザ2、プログラマ3、美術3、UI/UX1、サウンド1、QA1、歴史/神学監修2)
  • 期間:プリプロ3ヶ月/プロダクション10ヶ月/QA・ローカライズ3ヶ月(計16ヶ月)
  • 概算:8,000万〜1.4億円規模(外注とキャスト有無で変動)

15. KPI / 品質目標

  • ストーリー完走率60%以上、レビューで「尊重/配慮」に関する肯定的言及比率70%以上
  • 学習要素:章末クイズ正答率の上昇、資料室閲覧率40%以上

16. リスクと対応

  • 神学表現の誤解:監修フローと用語脚注で軽減
  • 暴力の回避が地味化:対話パズルに“緊張の演出”(時間制限・人だかり・法廷)を導入
  • 文化的感受性:外部レビュー(多宗教・無宗教含む)でプレイテスト

サンプルクエスト(第3章:コリント)

  • 状況:商人ギルドが安息日の取引巡りで共同体を批判。街の不満が高まる
  • 目的:両者の誤解を解き、市場規約の妥協案を提案
  • 手順
    1. 市場で聞き取り→証言カード「規約草案」「祭礼日」「巡礼需要」を入手
    2. 祈りでキーワード「家族」「信用」を取得
    3. 対話パズルで並べ替え:信用>祭礼日>需要調整
    4. 結果:ギルドは早朝市を追加、共同体は夕刻会合に変更→街ボード好転

代替案(スピンオフ)

A. 「たとえの箱庭(Parables Builder)」

  • ジャンル:箱庭パズル+教育
  • イエスのたとえ話を抽象化オブジェクトで組み、他者に伝わるかを評価
  • マップ共有・評価でコミュニティ生成、学校向け教材モード有り

B. 「詩編の旋律」

  • ジャンル:没入型音×言葉アドベンチャー
  • 詩編のテキストを手掛かりに、風景・音景を“調律”して心情を可視化
  • メンタルヘルス配慮の瞑想モード、読み上げ+多言語字幕

必要なら、この企画をピッチ用1枚詳細GDD予算&スケジュール表に分解してドキュメント化します。どの案を推したい? あるいは「現代日本舞台」「子ども向け」などの方向性も出せます。

DNAのダウンロードとアップロード方法

ざっくり言うと「DNAのダウンロード=配列データを公式DBから取得」「アップロード=公式DBへ自分の配列やリードを登録」です。実物のDNAを扱う手順ではなく、デジタル配列データの話として安全に説明します。

1) ダウンロード(取得)

代表的な取り先は NCBI・Ensembl・ENA です。

  • NCBI Datasets(ゲノム/遺伝子)
    ブラウザ or CLI で目的の生物やアセンブリをまとめて落とせます(FASTA、GFF/GTF、注釈一式)。CLI は datasetsdataformat を使用。使い方は公式の「Download a genome data package」と「Download and Install」に詳しいです。 NCBI+2NCBI+2
    • 例(CLI の雰囲気):
      datasets download genome taxon "Homo sapiens" --include genome,annotation
      dataformat tsv genome --input-file ncbi_dataset.zip > report.tsv
  • Ensembl(参考配列・遺伝子セット)
    ブラウザの「Export data」から FASTA/GTF/GFF を取得できます。FTP/rsync でもまとめ取り可能。最新リリース情報も公開されています。 ensembl.org+2ensembl.org+2
  • ENA(European Nucleotide Archive)
    原則として NCBI/ENA/DDBJ はデータ連携します。ENA でも配布・ダウンロードが可能です。 OUP Academic

補足:GenBank 全体の最新リリース規模(配列数・塩基数)などは「Current GenBank Release Notes」で確認できます。 NCBI

2) アップロード(登録)

自分の配列やシーケンスデータを公式リポジトリに登録してアクセッション番号を取得します。代表ルートは次のとおり。

  • NCBI Submission Portal(総合窓口)
    GenBank(配列)、SRA(生リード)、GEO(発現)、dbSNP 等へ Web から提出できます。アカウント作成 → フォームにメタデータ入力 → FASTA/FASTQ などをアップロード → 審査 → アクセッション付与、という流れです。 submit.ncbi.nlm.nih.gov
  • GenBank への配列提出(ゲノム含む)
    近年は Submission Portal 経由が基本。単一ゲノムの提出等のガイドも公式にまとまっています。 NCBI+1
  • ENA(Webin)
    ENA へは Webin(Web/REST API)で提出できます。プログラマブルに投げたい場合は Webin REST v2 のドキュメントが便利です。 ena-docs.readthedocs.io

提出の一般的な準備物(高レベル)

  • 配列ファイル:FASTA(配列)、FASTQ(リード)など
  • メタデータ:生物種、由来、ライブラリ情報、アセンブリ法、文献情報 等
  • 連絡情報:提出者・所属・ORCID 等
    (いずれも各ポータルの入力フォームに沿って用意します。具体的なフィールド名はポータルの最新ガイドに従ってください。) submit.ncbi.nlm.nih.gov+1

倫理・公開に関する注意(重要)

  • ヒト由来データ:個人特定情報や同意に関わる要件があります。適切なリポジトリ(例:制限付きアクセスのデータベース)や同意状況の確認が必要です。
  • 公開タイミング:多くのリポジトリで**エンバーゴ(公開保留)**設定が可能です。論文受理まで非公開にしてアクセッションだけ取得、といった運用が一般的です。
    (具体仕様は提出先ポータルのポリシーを必ず確認してください。) submit.ncbi.nlm.nih.gov

必要なら、あなたの目的(例:「人間の遺伝子の特定領域を取りたい」「自前のアセンブリを GenBank に出したい」/**「リードを SRA に出したい」**など)に合わせて、実際のダウンロード/提出手順(検索クエリ、必要な最小メタデータ、CLI コマンド例)をあなたのケースに即して書き起こします。どの用途ですか?