🎮ArcadeLab

ENHYPEN · 大学ABO · 角色与对话

by BlazeStar26
856 lines33.6 KB
▶ Play
```html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ENHYPEN · 大学ABO · 角色与对话</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        body {
            font-family: 'Segoe UI', 'PingFang SC', Roboto, sans-serif;
            background: #faf6f0;
            color: #2c3e50;
            padding: 24px 20px 60px;
            max-width: 1300px;
            margin: 0 auto;
        }
        h1 {
            font-size: 28px;
            font-weight: 600;
            letter-spacing: 2px;
            display: flex;
            align-items: center;
            gap: 12px;
            flex-wrap: wrap;
            border-bottom: 2px solid #e8ddd0;
            padding-bottom: 16px;
            margin-bottom: 28px;
        }
        h1 small {
            font-size: 15px;
            font-weight: 400;
            color: #8a7a6a;
            letter-spacing: 0;
        }
        .sub {
            color: #8a7a6a;
            font-size: 14px;
            margin-top: -12px;
            margin-bottom: 24px;
        }

        /* 角色卡片 */
        .card-grid {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
            gap: 18px;
            margin-bottom: 40px;
        }
        .card {
            background: #fff;
            border-radius: 18px;
            padding: 18px 14px 16px;
            box-shadow: 0 4px 16px rgba(44, 62, 80, 0.06);
            transition: all 0.2s;
            cursor: pointer;
            border: 2px solid transparent;
            text-align: center;
            position: relative;
        }
        .card:hover {
            transform: translateY(-4px);
            box-shadow: 0 8px 28px rgba(44, 62, 80, 0.10);
            border-color: #dccfc2;
        }
        .card .avatar {
            width: 70px;
            height: 70px;
            border-radius: 50%;
            margin: 0 auto 10px;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 32px;
            font-weight: 600;
            color: #fff;
            background: #b8a99a;
        }
        .card .name {
            font-weight: 700;
            font-size: 18px;
            letter-spacing: 1px;
        }
        .card .tag {
            font-size: 12px;
            background: #f0ebe4;
            padding: 2px 12px;
            border-radius: 20px;
            display: inline-block;
            margin-top: 4px;
            color: #5f4f3f;
        }
        .card .subinfo {
            font-size: 13px;
            color: #6f5f4f;
            margin-top: 6px;
            line-height: 1.5;
        }
        .card .relation-badge {
            font-size: 11px;
            background: #e8ddd0;
            padding: 2px 10px;
            border-radius: 12px;
            display: inline-block;
            margin-top: 6px;
            color: #4a3f35;
            font-weight: 500;
        }
        .card.selected {
            border-color: #b8a99a;
            background: #fcf8f3;
            box-shadow: 0 4px 20px rgba(44, 62, 80, 0.12);
        }

        /* 详情弹窗 */
        .modal-overlay {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0, 0, 0, 0.35);
            display: none;
            align-items: center;
            justify-content: center;
            z-index: 1000;
            backdrop-filter: blur(4px);
            padding: 20px;
        }
        .modal-overlay.active {
            display: flex;
        }
        .modal {
            background: #fff;
            max-width: 520px;
            width: 100%;
            border-radius: 28px;
            padding: 32px 30px 28px;
            box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2);
            position: relative;
            max-height: 90vh;
            overflow-y: auto;
        }
        .modal .close {
            position: absolute;
            top: 16px;
            right: 22px;
            font-size: 26px;
            cursor: pointer;
            color: #8a7a6a;
            background: none;
            border: none;
        }
        .modal .close:hover {
            color: #2c3e50;
        }
        .modal .m-avatar {
            font-size: 48px;
            width: 80px;
            height: 80px;
            border-radius: 50%;
            display: flex;
            align-items: center;
            justify-content: center;
            color: #fff;
            margin: 0 auto 12px;
        }
        .modal .m-name {
            font-size: 26px;
            font-weight: 700;
            text-align: center;
        }
        .modal .m-badge {
            text-align: center;
            font-size: 14px;
            color: #6f5f4f;
            margin: 2px 0 10px;
        }
        .modal .m-detail {
            border-top: 1px solid #eee7df;
            padding-top: 16px;
            margin-top: 10px;
            font-size: 15px;
            line-height: 1.8;
        }
        .modal .m-detail strong {
            color: #4a3f35;
            display: inline-block;
            min-width: 70px;
        }
        .modal .m-detail .section {
            margin-bottom: 10px;
        }

        /* 对话生成器 */
        .dialogue-section {
            background: #fff;
            border-radius: 24px;
            padding: 28px 26px 32px;
            box-shadow: 0 4px 20px rgba(44, 62, 80, 0.06);
            margin-top: 12px;
        }
        .dialogue-section h2 {
            font-size: 20px;
            font-weight: 600;
            margin-bottom: 16px;
            display: flex;
            align-items: center;
            gap: 10px;
        }
        .dialogue-section h2 span {
            font-size: 14px;
            font-weight: 400;
            color: #8a7a6a;
        }
        .selector-row {
            display: flex;
            flex-wrap: wrap;
            gap: 14px 20px;
            align-items: center;
            margin-bottom: 18px;
        }
        .selector-row label {
            font-weight: 500;
            font-size: 14px;
            color: #4a3f35;
            display: flex;
            align-items: center;
            gap: 8px;
        }
        .selector-row select {
            padding: 8px 14px;
            border-radius: 12px;
            border: 1.5px solid #ddd2c6;
            background: #fcf8f3;
            font-size: 15px;
            font-family: inherit;
            color: #2c3e50;
            min-width: 140px;
            outline: none;
            transition: border 0.2s;
        }
        .selector-row select:focus {
            border-color: #b8a99a;
        }
        .btn {
            background: #2c3e50;
            color: #fff;
            border: none;
            padding: 9px 28px;
            border-radius: 30px;
            font-size: 15px;
            font-weight: 600;
            cursor: pointer;
            transition: 0.2s;
            font-family: inherit;
        }
        .btn:hover {
            background: #1f2e3d;
            transform: scale(1.01);
        }
        .btn:active {
            transform: scale(0.97);
        }
        .btn-outline {
            background: transparent;
            color: #2c3e50;
            border: 1.5px solid #ddd2c6;
        }
        .btn-outline:hover {
            background: #f0ebe4;
            border-color: #b8a99a;
        }

        .dialogue-box {
            background: #f8f3ed;
            border-radius: 18px;
            padding: 22px 26px;
            min-height: 110px;
            margin-top: 10px;
            display: flex;
            flex-direction: column;
            justify-content: center;
            position: relative;
            border-left: 4px solid #dccfc2;
        }
        .dialogue-box .speaker {
            font-weight: 700;
            font-size: 17px;
            color: #3d332a;
        }
        .dialogue-box .speaker small {
            font-weight: 400;
            color: #8a7a6a;
            font-size: 14px;
            margin-left: 8px;
        }
        .dialogue-box .line {
            font-size: 17px;
            line-height: 1.7;
            margin-top: 6px;
            color: #2c3e50;
        }
        .dialogue-box .line em {
            color: #6f5f4f;
            font-style: italic;
        }
        .dialogue-box .empty {
            color: #b0a090;
            font-style: italic;
            text-align: center;
            padding: 16px 0;
        }
        .dialogue-box .context {
            font-size: 13px;
            color: #8a7a6a;
            margin-bottom: 6px;
            background: #eee7df;
            padding: 2px 12px;
            border-radius: 20px;
            display: inline-block;
            align-self: flex-start;
        }

        /* 历史记录 */
        .history {
            margin-top: 16px;
            border-top: 1px solid #eee7df;
            padding-top: 16px;
        }
        .history .h-item {
            font-size: 14px;
            color: #5f4f3f;
            padding: 6px 0;
            border-bottom: 1px dashed #eee7df;
            display: flex;
            gap: 12px;
        }
        .history .h-item .h-speaker {
            font-weight: 600;
            min-width: 70px;
        }
        .history .h-item .h-text {
            color: #3d332a;
        }
        .history-toggle {
            font-size: 13px;
            color: #8a7a6a;
            cursor: pointer;
            background: none;
            border: none;
            text-decoration: underline dotted;
            padding: 4px 0;
            font-family: inherit;
        }
        .history-toggle:hover {
            color: #2c3e50;
        }

        @media (max-width: 600px) {
            .card-grid {
                grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
                gap: 12px;
            }
            .modal {
                padding: 24px 18px;
            }
            .selector-row {
                flex-direction: column;
                align-items: stretch;
            }
            .selector-row select {
                min-width: unset;
            }
            .dialogue-section {
                padding: 18px 16px 24px;
            }
            h1 {
                font-size: 22px;
            }
        }
    </style>
</head>
<body>

    <h1>
        🎓 ENHYPEN · 大学ABO
        <small>大二 · 结缘 · 寻雨 · 双向暗恋</small>
    </h1>
    <div class="sub">✦ 点击角色卡片查看详细设定 · 下方生成CP对话</div>

    <!-- 角色卡片 -->
    <div class="card-grid" id="cardGrid"></div>

    <!-- 对话生成器 -->
    <div class="dialogue-section">
        <h2>💬 对话文字游戏 <span>— 选择两位角色,生成专属互动</span></h2>
        <div class="selector-row">
            <label>角色①
                <select id="char1"></select>
            </label>
            <label>角色②
                <select id="char2"></select>
            </label>
            <button class="btn" id="genBtn">✨ 生成对话</button>
            <button class="btn btn-outline" id="randomBtn">🎲 随机一对</button>
        </div>

        <div class="dialogue-box" id="dialogueBox">
            <div class="empty">👆 选择两位角色,点击「生成对话」</div>
        </div>

        <div class="history">
            <button class="history-toggle" id="historyToggle">📜 显示历史记录</button>
            <div id="historyList" style="display:none; margin-top:10px;"></div>
        </div>
    </div>

    <!-- 模态框 -->
    <div class="modal-overlay" id="modalOverlay">
        <div class="modal">
            <button class="close" id="modalClose">✕</button>
            <div id="modalContent"></div>
        </div>
    </div>

    <script>
        // ======================== 角色数据 ========================
        const characters = [{
            id: 'heeseung',
            name: '李羲承',
            eng: 'HEESEUNG',
            gender: 'Omega',
            age: 20,
            major: '实用音乐系 大二',
            info: '旧书页与麝香 · 干燥温和',
            emoji: '📖',
            color: '#7a6a5a',
            relation: '破镜重圆 · 带球跑',
            detail: {
                height: '178cm',
                mark: '被沈载伦终身标记',
                personality: '表面冷静疏离,内心坚韧固执。一个人扛了两年,已经习惯了"不需要任何人"。',
                background: '高中和沈载伦是恋人,毕业前因听到载伦父母对话而主动提分手,分手当天被终身标记。后发现自己怀孕,独自生下女儿李知妤,休学一年后考上大学边带娃边读书。',
                weakness: '长期睡眠不足导致偏头痛;终身标记带来的生理依赖;最怕载伦发现知妤、抢走知妤。'
            }
        }, {
            id: 'jake',
            name: '沈载伦',
            eng: 'JAKE',
            gender: 'Alpha',
            age: 20,
            major: '英语英文系 大二 (澳洲转学)',
            info: '海盐与阳光晒过的棉 · 温暖开阔',
            emoji: '🌊',
            color: '#4f8a8a',
            relation: '破镜重圆 · 前任变家人',
            detail: {
                height: '180cm',
                mark: '终身标记了李羲承',
                personality: '表面开朗温和,内里偏执执着。两年都在找羲承,来韩国前已决定"找到他,不放手"。',
                background: '澳洲出生,高中交换到韩国和羲承在一起。毕业前被羲承提分手,易感期爆发中终身标记了羲承。之后羲承消失,他找遍所有方式无果,拒绝澳洲的一切申请了韩国的大学。转学第一天在校园重遇羲承,并发现女儿的存在。',
                weakness: '被抛弃的创伤;对终身标记的复杂情绪;最怕羲承不需要他、女儿不认识他。'
            }
        }, {
            id: 'jay',
            name: '朴综星',
            eng: 'JAY',
            gender: 'Alpha',
            age: 20,
            major: '经营学系 大二',
            info: '燃烧的檀木与焦糖 · 沉稳微苦带甜',
            emoji: '🔥',
            color: '#8a6e4b',
            relation: '双向暗恋 · 纯情推拉',
            detail: {
                height: '181cm',
                mark: '未标记',
                personality: '表面酷飒直接,内心细腻温柔。做了十分只说三分,默默对祯元好却不擅长表达。',
                background: '经营学成绩优异,多国语言能力。从第一眼就对祯元心动,但觉得"祯元怎么可能喜欢我",一直用行动代替语言。所有人都看得出来他在暗恋,只有他自己觉得藏得很好。',
                weakness: '嘴硬心软;和祯元在一起时脑子会短路;重度恋爱脑但死不承认。'
            }
        }, {
            id: 'jungwon',
            name: '梁祯元',
            eng: 'JUNGWON',
            gender: 'Omega',
            age: 19,
            major: '实用舞蹈系 大一',
            info: '冷掉的雨与棉布 · 干净清冽',
            emoji: '🐈',
            color: '#6a7a8a',
            relation: '双向暗恋 · 纯情推拉',
            detail: {
                height: '174cm',
                mark: '未标记',
                personality: '表面沉稳有主见,内里敏感细腻。对综星的心意小心翼翼藏着,总是找各种理由出现在综星身边。',
                background: '入学第一天就注意到综星,但觉得"jay哥那么酷怎么可能喜欢我"。会记住综星说的每一句话,综星不在时会不自觉地提到他。所有人都看得出来他在暗恋,只有他自己觉得藏得很好。',
                weakness: '容易想多;会因为综星的冷淡而沮丧;等对方先开口却不敢自己先说。'
            }
        }, {
            id: 'sunghoon',
            name: '朴成训',
            eng: 'SUNGHOON',
            gender: 'Alpha',
            age: 20,
            major: '体育系 大二 (花滑特长生)',
            info: '冰雪与冷杉木 · 清冷干净',
            emoji: '⛸️',
            color: '#5a7a8a',
            relation: '欢喜冤家 · 表面互怼私下护短',
            detail: {
                height: '181cm',
                mark: '未标记',
                personality: '表面高冷话少,内心柔软。嘴上嫌弃善禹,心里在意得要死。喜欢逗善禹看他炸毛。',
                background: '花滑国家队预备役级别。觉得善禹"太吵了",但善禹不在会不自在。如果别人欺负善禹,第一个翻脸。所有人都知道他对善禹不一样,只有他自己不承认。',
                weakness: '不擅长表达感情;易感期会变得湿重;嘴硬心软。'
            }
        }, {
            id: 'sunoo',
            name: '金善禹',
            eng: 'SUNOO',
            gender: 'Omega',
            age: 19,
            major: '实用音乐系 大一',
            info: '蜜桃与棉花糖 · 甜而不腻',
            emoji: '🍑',
            color: '#d4a08a',
            relation: '欢喜冤家 · 表面互怼私下护短',
            detail: {
                height: '175cm',
                mark: '未标记',
                personality: '表面开朗爱笑,内里敏感细腻。笑容是武器也是保护色,其实很在意成训的看法。',
                background: '主唱级别vocal,舞台表现力强。觉得成训"面恶心善",喜欢逗他看冰山脸崩掉。被成训逗时会炸毛但不会真生气。生病或难受时第一个找的是成训。',
                weakness: '笑容是保护色;在意成训的看法却嘴硬;发情期会变得粘人。'
            }
        }, {
            id: 'niki',
            name: '西村力',
            eng: 'NI-KI',
            gender: 'Beta',
            age: 18,
            major: '实用舞蹈系 大一 (日本留学生)',
            info: 'Beta · 信息素极淡不可感知',
            emoji: '🎯',
            color: '#6a7a7a',
            relation: '旁观者清 · 稳定器',
            detail: {
                height: '183cm+',
                mark: '无',
                personality: '表面话少冷感,内里专注有野心。毒舌但心软,吐槽精准行动力强。作为Beta不被ABO规则束缚。',
                background: '从小学习舞蹈,偶像是迈克尔杰克逊。看哥哥们的情感推拉觉得"你们不累吗",但会默默帮忙制造机会。是团队里看得最清楚的人。',
                weakness: '韩语还在进步中;有时词不达意;被哥哥们的情感问题折腾。'
            }
        }];

        // ======================== 对话库 ========================
        const dialogues = {
            // 李羲承 × 沈载伦 (破镜重圆)
            'heeseung-jake': [
                { speaker: '沈载伦', line: '"你问过我吗?你问过我愿不愿意吗?"', context: '重逢·质问' },
                { speaker: '李羲承', line: '"我不后悔留下她。我只是……不后悔而已。"', context: '关于知妤' },
                { speaker: '沈载伦', line: '"这两年你去哪了?"', context: '重逢·追问' },
                { speaker: '李羲承', line: '"我在等你找到我。"', context: '重逢·坦白' },
                { speaker: '沈载伦', line: '"知妤……她叫知妤。"', context: '第一次见女儿' },
                { speaker: '李羲承', line: '"她长得像你。笑得像你,眼睛也像你。"', context: '关于女儿' },
                { speaker: '沈载伦', line: '"为什么不告诉我?你一个人……怎么扛的?"', context: '心疼' },
                { speaker: '李羲承', line: '"我习惯了。真的,已经习惯了。"', context: '逞强' },
                { speaker: '沈载伦', line: '"以后不用再习惯了。我回来了。"', context: '承诺' },
                { speaker: '李羲承', line: '"你回来得太晚了。"', context: '带着哭腔' },
                { speaker: '沈载伦', line: '"那我来弥补。一辈子够不够?"', context: '告白' },
                { speaker: '李羲承', line: '"……不够。但你可以试试。"', context: '松动' },
            ],
            // 朴综星 × 梁祯元 (双向暗恋)
            'jay-jungwon': [
                { speaker: '梁祯元', line: '"哥到底知不知道……我在看他啊。"', context: '内心独白' },
                { speaker: '朴综星', line: '"我不是对谁都这样的。你能不能……稍微察觉一下?"', context: '嘴硬' },
                { speaker: '梁祯元', line: '"jay哥上次说喜欢这个口味……所以我买了。"', context: '默默记住' },
                { speaker: '朴综星', line: '"你又提我干嘛。……不过谢了。"', context: '掩饰开心' },
                { speaker: '梁祯元', line: '"哥,一起去吃饭吗?"', context: '主动约' },
                { speaker: '朴综星', line: '"……好。刚好我也饿了。"', context: '明明在等' },
                { speaker: '梁祯元', line: '"jay哥穿白色真的很好看。"', context: '真心夸奖' },
                { speaker: '朴综星', line: '"……你穿什么都好看。"', context: '不小心说漏' },
                { speaker: '梁祯元', line: '"什么?"', context: '心跳加速' },
                { speaker: '朴综星', line: '"没什么。吃饭。"', context: '慌乱转移' },
                { speaker: '梁祯元', line: '"哥,初雪那天……你想和谁一起看?"', context: '试探' },
                { speaker: '朴综星', line: '"……你。"', context: '终于承认' },
            ],
            // 朴成训 × 金善禹 (欢喜冤家)
            'sunghoon-sunoo': [
                { speaker: '朴成训', line: '"金善禹你烦不烦……"', context: '日常嫌弃' },
                { speaker: '金善禹', line: '"那你别看啊!"', context: '日常回怼' },
                { speaker: '朴成训', line: '"你是不是又偷偷看我跳舞了。"', context: '被抓包' },
                { speaker: '金善禹', line: '"谁看你了!自恋狂!"', context: '死不承认' },
                { speaker: '朴成训', line: '"别来这套。"', context: '嘴硬' },
                { speaker: '金善禹', line: '"哼,我才没有撒娇。"', context: '傲娇' },
                { speaker: '朴成训', line: '"你……真的没事吧?"', context: '担心' },
                { speaker: '金善禹', line: '"你这是在关心我?"', context: '抓住把柄' },
                { speaker: '朴成训', line: '"……当我没问。"', context: '退缩' },
                { speaker: '金善禹', line: '"朴成训,我好像有点喜欢你了。"', context: '突然告白' },
                { speaker: '朴成训', line: '"……你再说一遍。"', context: '愣住' },
                { speaker: '金善禹', line: '"我说,我好像……算了,没听到就算了。"', context: '害羞逃跑' },
                { speaker: '朴成训', line: '"我听到了。……我也是。"', context: '终于回应' },
            ],
            // 西村力作为旁观者 (单独角色互动)
            'niki': [
                { speaker: '西村力', line: '"哥,你们不累吗……算了当我没问。"', context: '旁观' },
                { speaker: '西村力', line: '"你们到底什么时候在一起。"', context: '吐槽' },
                { speaker: '西村力', line: '"为什么是我要操心这些……"', context: '无奈' },
                { speaker: '西村力', line: '"我看得都累了。"', context: '冷漠脸' },
                { speaker: '西村力', line: '"哥,你嘴角都翘到耳朵了。"', context: '拆穿' },
            ]
        };

        // ======================== 工具 ========================
        function getChar(id) { return characters.find(c => c.id === id); }

        function getRelationPair(id1, id2) {
            const pairs = [
                ['heeseung', 'jake'],
                ['jay', 'jungwon'],
                ['sunghoon', 'sunoo']
            ];
            const sorted = [id1, id2].sort();
            for (let p of pairs) {
                const sp = [...p].sort();
                if (sorted[0] === sp[0] && sorted[1] === sp[1]) return p.join('-');
            }
            return null;
        }

        function pickRandom(arr) { return arr[Math.floor(Math.random() * arr.length)]; }

        // ======================== 渲染角色卡片 ========================
        const grid = document.getElementById('cardGrid');
        characters.forEach(c => {
            const div = document.createElement('div');
            div.className = 'card';
            div.dataset.id = c.id;
            div.innerHTML = `
                <div class="avatar" style="background:${c.color};">${c.emoji}</div>
                <div class="name">${c.name}</div>
                <div class="tag">${c.gender} · ${c.age}岁</div>
                <div class="subinfo">${c.major}</div>
                <div class="relation-badge">${c.relation}</div>
            `;
            div.addEventListener('click', () => openModal(c.id));
            grid.appendChild(div);
        });

        // ======================== 模态框 ========================
        const overlay = document.getElementById('modalOverlay');
        const modalContent = document.getElementById('modalContent');
        const modalClose = document.getElementById('modalClose');

        function openModal(id) {
            const c = getChar(id);
            if (!c) return;
            modalContent.innerHTML = `
                <div class="m-avatar" style="background:${c.color};">${c.emoji}</div>
                <div class="m-name">${c.name} <span style="font-weight:400;font-size:18px;color:#8a7a6a;">${c.eng}</span></div>
                <div class="m-badge">${c.gender} · ${c.age}岁 · ${c.major}</div>
                <div class="m-detail">
                    <div class="section"><strong>信息素</strong> ${c.info}</div>
                    <div class="section"><strong>身高</strong> ${c.detail.height}</div>
                    <div class="section"><strong>标记状态</strong> ${c.detail.mark}</div>
                    <div class="section"><strong>性格</strong> ${c.detail.personality}</div>
                    <div class="section"><strong>背景</strong> ${c.detail.background}</div>
                    <div class="section"><strong>弱点</strong> ${c.detail.weakness}</div>
                    <div style="margin-top:8px;font-size:13px;color:#8a7a6a;border-top:1px solid #eee7df;padding-top:10px;">关系 · ${c.relation}</div>
                </div>
            `;
            overlay.classList.add('active');
            document.body.style.overflow = 'hidden';
        }

        function closeModal() {
            overlay.classList.remove('active');
            document.body.style.overflow = '';
        }
        modalClose.addEventListener('click', closeModal);
        overlay.addEventListener('click', (e) => { if (e.target === overlay) closeModal(); });
        document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeModal(); });

        // ======================== 对话生成器 ========================
        const sel1 = document.getElementById('char1');
        const sel2 = document.getElementById('char2');
        const genBtn = document.getElementById('genBtn');
        const randomBtn = document.getElementById('randomBtn');
        const dialogueBox = document.getElementById('dialogueBox');
        const historyList = document.getElementById('historyList');
        const historyToggle = document.getElementById('historyToggle');
        let history = [];

        // 填充下拉
        characters.forEach(c => {
            const o1 = document.createElement('option');
            o1.value = c.id;
            o1.textContent = `${c.name} (${c.eng})`;
            sel1.appendChild(o1);
            const o2 = document.createElement('option');
            o2.value = c.id;
            o2.textContent = `${c.name} (${c.eng})`;
            sel2.appendChild(o2);
        });
        // 默认选一对
        sel1.value = 'heeseung';
        sel2.value = 'jake';

        function generateDialogue(id1, id2) {
            if (id1 === id2) {
                // 单角色:显示个人台词或旁观者台词
                if (id1 === 'niki') {
                    const lines = dialogues['niki'];
                    return pickRandom(lines);
                }
                const c = getChar(id1);
                return { speaker: c.name, line: `"……"  ${c.name}陷入了沉思。`, context: '独处' };
            }

            const key = getRelationPair(id1, id2);
            if (key && dialogues[key]) {
                return pickRandom(dialogues[key]);
            }

            // 非CP组合:生成日常互动
            const c1 = getChar(id1);
            const c2 = getChar(id2);
            const generic = [
                { speaker: c1.name, line: `"${c2.name},一起吃饭吗?"`, context: '日常' },
                { speaker: c2.name, line: `"好啊。正好我也饿了。"`, context: '日常' },
                { speaker: c1.name, line: `"你今天看起来心情不错。"`, context: '日常' },
                { speaker: c2.name, line: `"被你发现了。"`, context: '日常' },
                { speaker: c1.name, line: `"……"`, context: '沉默' },
                { speaker: c2.name, line: `"……你倒是说句话啊。"`, context: '日常' },
            ];
            return pickRandom(generic);
        }

        function renderDialogue(result, id1, id2) {
            const c1 = getChar(id1);
            const c2 = getChar(id2);
            const isPair = getRelationPair(id1, id2) !== null;

            let html = '';
            if (result.context) {
                html += `<span class="context">${result.context}</span>`;
            }
            const speakerLabel = result.speaker === c1.name ? `${c1.name} (${c1.gender})` :
                result.speaker === c2.name ? `${c2.name} (${c2.gender})` :
                result.speaker;
            html += `<div class="speaker">${speakerLabel} <small>${isPair ? '❤️' : '💬'}</small></div>`;
            html += `<div class="line">${result.line}</div>`;
            dialogueBox.innerHTML = html;

            // 存入历史
            history.push({
                id1,
                id2,
                speaker: result.speaker,
                line: result.line,
                context: result.context || ''
            });
            if (history.length > 50) history.shift();
            renderHistory();
        }

        function renderHistory() {
            if (history.length === 0) {
                historyList.innerHTML = '<div style="color:#b0a090;font-style:italic;padding:8px 0;">暂无记录</div>';
                return;
            }
            let html = '';
            const show = history.slice(-10).reverse();
            show.forEach(h => {
                const c1 = getChar(h.id1);
                const c2 = getChar(h.id2);
                const label = h.speaker + (h.context ? ` · ${h.context}` : '');
                html += `<div class="h-item">
                    <span class="h-speaker">${label}</span>
                    <span class="h-text">${h.line}</span>
                </div>`;
            });
            historyList.innerHTML = html;
        }

        function doGenerate() {
            const id1 = sel1.value;
            const id2 = sel2.value;
            if (!id1 || !id2) {
                dialogueBox.innerHTML = `<div class="empty">⚠️ 请选择两位角色</div>`;
                return;
            }
            const result = generateDialogue(id1, id2);
            renderDialogue(result, id1, id2);
        }

        function doRandom() {
            const ids = characters.map(c => c.id);
            const shuffled = ids.sort(() => Math.random() - 0.5);
            const a = shuffled[0];
            let b = shuffled[1];
            // 保证不选同一个
            while (b === a && ids.length > 1) {
                b = shuffled[2] || ids.find(id => id !== a);
            }
            sel1.value = a;
            sel2.value = b;
            doGenerate();
        }

        genBtn.addEventListener('click', doGenerate);
        randomBtn.addEventListener('click', doRandom);

        // 回车触发
        document.addEventListener('keydown', (e) => {
            if (e.key === 'Enter' && (e.target === sel1 || e.target === sel2)) {
                doGenerate();
            }
        });

        // 历史折叠
        let historyVisible = false;
        historyToggle.addEventListener('click', () => {
            historyVisible = !historyVisible;
            historyList.style.display = historyVisible ? 'block' : 'none';
            historyToggle.textContent = historyVisible ? '📜 隐藏历史记录' : '📜 显示历史记录';
            if (historyVisible) renderHistory();
        });

        // 首次加载生成一组
        setTimeout(doGenerate, 300);

        // 选中的卡片高亮 - 联动下拉
        function highlightCards() {
            const v1 = sel1.value,
                v2 = sel2.value;
            document.querySelectorAll('.card').forEach(el => {
                const id = el.dataset.id;
                el.classList.toggle('selected', id === v1 || id === v2);
            });
        }
        sel1.addEventListener('change', highlightCards);
        sel2.addEventListener('change', highlightCards);
        highlightCards();

        console.log('✨ ENHYPEN · 大学ABO 文字游戏已启动!');
        console.log('💡 点击角色卡片查看详情,下拉选择角色生成对话。');
    </script>
</body>
</html>
```

Game Source: ENHYPEN · 大学ABO · 角色与对话

Creator: BlazeStar26

Libraries: none

Complexity: complex (856 lines, 33.6 KB)

The full source code is displayed above on this page.

Remix Instructions

To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: enhypen-abo-blazestar26" to link back to the original. Then publish at arcadelab.ai/publish.