ENHYPEN · 大学ABO · 角色与对话
by BlazeStar26856 lines33.6 KB
```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.