0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

タスクを放置するとキャラクターが傷ついていき死に絶えるアプリを作った話

0
Last updated at Posted at 2026-01-18

はじめに - なぜ作ったのか

ADHDの自分には普通のToDoリストが効かない。

「やらなきゃ」という曖昧な義務感では動けない。「やらないと誰かが死ぬ」という明確な損失が必要だった。

そこで作ったのがHOSTAGE

"Your negligence kills."(あなたの怠慢が、誰かを殺す)

培養槽の中で眠るバーチャルキャラクター。彼・彼女の命はあなたのタスク消化状況に直結している。期限を破ればそれは衰弱し、画面は血のように赤く染まり、システムエラーが走る。習慣を完了すれば回復する。

これは生産性ツールではない。心理戦だ。

デモ・ソースコード

コンセプトと心理学的背景

なぜ「ホラー」なのか?

行動経済学の研究によれば、人間の損失回避の動機は利益追求の約2倍強い(プロスペクト理論)。

従来のタスク管理アプリは「タスクを完了すると嬉しい」という利益追求型。しかし、ADHDや先延ばし癖がある人間にとって、この程度の動機では不十分。

HOSTAGEは逆のアプローチ:

  • タスクを放置するとキャラクターが死ぬ(損失)
  • UIが崩壊し、視覚的に不快になる(損失)
  • 習慣を完了すると回復する(損失回避)

キャラクターの死は、脳が「自分の責任による損失」として認識する。これが罪悪感を生み、強力な行動動機となる。

主要機能

🎮 ゲームメカニクス

タスク完了 → HP回復(優先度連動)

優先度 回復量
Low +3 HP
Medium +5 HP
High +8 HP
Critical +12 HP

習慣完了 → HP回復 + ストリーク

  • 固定 +5 HP
  • 連続達成日数をトラッキング
  • 「🔥 7日連続達成!」のようなフィードバック

時間減衰 → 放置するほど加速的にダメージ

ダメージ = (経過時間²) × 0.5

1時間放置  → 0.5 ダメージ
10時間放置 → 50 ダメージ
24時間放置 → 288 ダメージ

💀 Death & Purge(死と廃棄)

HPが0になると被験体は死亡。ユーザーには2つの選択肢が提示される:

  • Reboot(蘇生): HP50で復活、同じ被験体を継続
  • Purge(検体廃棄): 完全削除し、新規被験体を作成

「失敗作を消して最初からやり直す」という選択肢が、心理的なリセット効果を生む。

📱 PWA対応(ネイティブアプリ体験)

スマートフォンの「ホーム画面に追加」で、アドレスバーのない全画面表示が可能。

  • オフライン対応(Service Worker)
  • プッシュ通知対応(将来実装予定)
  • インストール不要、ブラウザから直接追加

🌐 多言語対応(i18n)

英語・日本語の完全対応。ブラウザ言語を自動判定し、手動切り替えも可能。

翻訳の設計思想: 直訳ではなく、SF/サイバーパンクの世界観を維持した意訳を採用。

English 直訳(NG) SF調意訳(採用)
Sync Tasks タスク同期 外部記憶同期
Subject 対象 被験体
Signal Lost 信号喪失 反応消失
Your negligence has consequences あなたの怠慢には結果がある 怠惰には代償が伴う

技術スタック

┌─────────────────────────────────────┐
│         Frontend (Vercel)           │
│     Next.js 14 / App Router / PWA   │
└──────────────┬──────────────────────┘
               │ HTTPS
               ▼
┌─────────────────────────────────────┐
│         Backend (Railway)           │
│        FastAPI / Python 3.11        │
└──────────────┬──────────────────────┘
               │
               ▼
┌─────────────────────────────────────┐
│        Database (Supabase)          │
│    PostgreSQL / Auth / Realtime     │
└─────────────────────────────────────┘

選定理由

技術 選定理由
Next.js 14 App RouterでSSR/CSRの最適化、PWA対応が容易
FastAPI 型安全(Pydantic v2)、非同期処理、自動ドキュメント生成
Supabase PostgreSQL + 認証 + リアルタイム購読を一括提供
Vercel Next.jsとの親和性、Edge Functionsでグローバル配信
Railway Pythonバックエンドのデプロイが簡単、自動スケール

アーキテクチャの変遷

当初はNotion APIと連携し、ユーザーの既存タスクを読み取る設計だった。しかし以下の理由でSupabase完結型に移行:

  1. 仕様の不確定性: 「Notionのどのプロパティをどう同期するか」が未定義のまま肥大化
  2. 外部依存リスク: APIレート制限、トークン期限切れ、仕様変更に振り回される
  3. ゲームバランス制御: 自前DBなら「タスク完了 = +X HP」を明確に定義可能

結果として、シンプルで堅牢なシステムが完成した。

CI/CDパイプライン

GitHub Actionsによる自動品質チェックを実装。

# .github/workflows/ci.yml
jobs:
  backend-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.11'
      - run: pip install -r requirements.txt
      - run: python -m compileall app/

  frontend-build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: cd frontend && npm ci
      - run: cd frontend && npm run build
        env:
          NEXT_PUBLIC_API_BASE: "http://localhost:8000"
          NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
          NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.SUPABASE_ANON_KEY }}

効果:

  • デプロイ前にビルドエラーを検知
  • 依存関係の不整合(存在しないバージョン指定など)を事前発見
  • 導入後のデプロイ失敗率: 0%

実装で最も苦労した点

1. Horror UI実装:「不快だけど目を離せない」絶妙なライン

一番こだわったのは**「ギリ不快で、ギリ目をそらさないホラー演出」**の調整。

CRTスキャンライン

.scanlines::before {
  content: " ";
  display: block;
  position: fixed;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
  background: linear-gradient(
    to bottom,
    rgba(18, 16, 16, 0) 50%,
    rgba(0, 0, 0, 0.25) 50%
  );
  background-size: 100% 4px;
  z-index: 50;
  pointer-events: none;
}

グリッチテキストアニメーション

@keyframes glitch-anim-1 {
  0% { clip-path: inset(20% 0 80% 0); transform: translate(-2px, 1px); }
  20% { clip-path: inset(60% 0 10% 0); transform: translate(2px, -1px); }
  40% { clip-path: inset(40% 0 50% 0); transform: translate(-2px, 2px); }
  60% { clip-path: inset(80% 0 5% 0); transform: translate(2px, -2px); }
  80% { clip-path: inset(10% 0 70% 0); transform: translate(-1px, 1px); }
  100% { clip-path: inset(30% 0 50% 0); transform: translate(1px, -1px); }
}

.glitch-text::before {
  left: 2px;
  text-shadow: -1px 0 #ff00c1;
  animation: glitch-anim-1 2s infinite linear alternate-reverse;
}

.glitch-text::after {
  left: -2px;
  text-shadow: -1px 0 #00fff9;
  animation: glitch-anim-1 2s infinite linear alternate-reverse;
}

HPに応じた動的UI崩壊

const hpPercent = (pet.hp / pet.max_hp) * 100;
const isDead = pet.status === 'DEAD';

{isDead ? '† SIGNAL LOST †' : pet.name}

調整に時間がかかったポイント:

  • グリッチの頻度(激しすぎると不快、弱すぎると印象に残らない)
  • 色の選択(緑→黄→赤の段階的変化)
  • モバイル端末でのパフォーマンス(60fps維持)

2. HP減衰システム:時間でじわじわ死んでいく

from datetime import datetime, timezone

DECAY_COEFFICIENT = 0.5

def calculate_time_decay(pet: dict) -> dict:
    """
    指数関数的にダメージが増加(放置するほど加速度的に危険)
    """
    if pet['status'] == 'DEAD':
        return pet

    last_checked_str = pet.get('last_checked_at')
    if not last_checked_str:
        return pet

    try:
        last_checked = datetime.fromisoformat(
            last_checked_str.replace('Z', '+00:00')
        )
    except ValueError:
        return pet

    now = datetime.now(timezone.utc)
    hours_passed = (now - last_checked).total_seconds() / 3600.0

    if hours_passed <= 0:
        return pet

    damage = (hours_passed ** 2) * DECAY_COEFFICIENT

    current_hp = float(pet.get('hp', 100))
    new_hp = max(0.0, current_hp - damage)

    updated_pet = pet.copy()
    updated_pet['hp'] = new_hp

    if new_hp <= 0:
        updated_pet['status'] = 'DEAD'

    return updated_pet

ポイント: 単純な線形減衰ではなく、二乗関数を採用。「ちょっとくらい大丈夫」という油断を許さない設計。

3. Next.js App RouterとReact Contextの境界問題

多言語対応(i18n)実装時、Server ComponentとClient Componentの境界でContextが使えずHydrationエラーが頻発。

解決策: LayoutClientラッパーを作成し、境界を明確化。

// components/LayoutClient.tsx
'use client';
import dynamic from 'next/dynamic';
import { LanguageProvider } from '@/lib/i18n/LanguageContext';

const LanguageToggle = dynamic(
  () => import('@/components/ui/LanguageToggle'),
  { ssr: false }
);

export default function LayoutClient({ children }: { children: React.ReactNode }) {
  return (
    <LanguageProvider>
      <LanguageToggle />
      {children}
    </LanguageProvider>
  );
}

Phase 2構想:Solana Dynamic NFTs

現在のHOSTAGEはWeb2アプリだが、Phase 2でSolana統合を予定。

Dynamic NFTとは?

通常のNFTは静的だが、メタデータが変化するNFT

HOSTAGEのキャラクターをDynamic NFTにすることで:

  • キャラクターの状態がNFTに反映(健康時は緑、クリティカル時は赤)
  • 生死記録が永続保存(完全な履歴がブロックチェーンに刻まれる)
  • Achievement NFT(7日生存、30日生存などのバッジ)
{
  "name": "HOSTAGE Subject #1337",
  "description": "A productivity companion whose fate depends on your discipline",
  "attributes": [
    {"trait_type": "Current Status", "value": "Critical"},
    {"trait_type": "Days Survived", "value": 23},
    {"trait_type": "Tasks Completed", "value": 156},
    {"trait_type": "Deaths", "value": 2}
  ]
}

フィードバック募集

聞きたいこと

  1. 心理学的効果: 実際に使ってみて、罪悪感は動機になりましたか?
  2. UI/UX: ホラー演出は「不快」と「興味深い」のバランスが取れていますか?
  3. 技術的改善点: コードレビュー、アーキテクチャ改善の提案があれば
  4. Phase 2構想: Solana統合について、面白そうな機能アイデアはありますか?

おわりに

「タスク管理が続かない」という悩みを持つ方、ぜひHOSTAGEを試してみてください。

そして、「こんな機能があったらもっと怖い(やる気が出る)」というアイデアがあれば、ぜひコメントで教えてください。

"Your negligence kills."

このコンセプトが、生産性ツールの新しい可能性を開くと信じています。


関連リンク:

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?