feat(web): 红果-style homepage + instant-play prebaked first acts

Rewrites all 64 homepage cards (32 男性向 + 32 女性向) as short-drama hook
stories (战神归来 / 重生分手前夜 / 系统选妃 / 穿成乙游男配 / 末世异能 / 民国
谍战 / 修真渡劫 …) and regenerates each cover via FLUX in its assigned art
style (12 styles spread across 64 cards) at 832×1024 ≈4:5.

Click-to-play path: cards now jump straight to /play?card=<name> and hydrate
Session from /home/firstact/<name>.json — the engine pipeline (Architect +
Writer + CharacterDesigner + Painter) has been pre-run for 44/64 cards. The
remaining 20 (m14/m29/f14..f31) are pending an LLM credit top-up; their
clicks fall through to live /api/start for now.

Runware-hosted first-scene images are downloaded into /home/firstscene/
and the JSONs are rewritten to point at the local webp, so click → first
image is bounded by local-disk decode (~100ms) instead of CDN round-trip.

Scripts:
- scripts/generate-home-images.mjs  — rewrites all 64 cover prompts, per-card
  styles baked into prompts, 832×1024 dims to match StoryCard aspect
- scripts/prebake-firstacts.mjs     — POST /api/start × 64 with concurrency
  4, saves StartResponse to public/home/firstact/<name>.json
- scripts/localize-firstact-images.mjs — downloads each prebaked imageUrl
  to public/home/firstscene/<name>.webp (q80, ≤1600px) and rewrites JSON

README: adds Screenshots section (3×3 gallery) to README.md / README.zh-CN.md,
9 in-game shots compressed to docs/screenshots/*.webp (7.5MB → 680KB).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
DESKTOP-I1T6TF3\Q
2026-06-02 17:20:34 +08:00
parent 9ae91dd3ed
commit d93c16d836
168 changed files with 680 additions and 544 deletions
+22
View File
@@ -18,6 +18,28 @@ Free to play, no setup required: [infiplot.com](https://infiplot.com)
---
## 📸 Screenshots
<table>
<tr>
<td><img src="docs/screenshots/4.webp" width="280" alt="School rooftop scene"></td>
<td><img src="docs/screenshots/c3.webp" width="280" alt="Golden hour rooftop dialogue"></td>
<td><img src="docs/screenshots/c5.webp" width="280" alt="Sunset rooftop confession"></td>
</tr>
<tr>
<td><img src="docs/screenshots/c7.webp" width="280" alt="Rooftop close-up choice"></td>
<td><img src="docs/screenshots/a4.webp" width="280" alt="Twilight rooftop drama"></td>
<td><img src="docs/screenshots/5.webp" width="280" alt="Cityscape evening scene"></td>
</tr>
<tr>
<td><img src="docs/screenshots/d2.webp" width="280" alt="Cyberpunk neon alley"></td>
<td><img src="docs/screenshots/f2.webp" width="280" alt="Night street anime scene"></td>
<td><img src="docs/screenshots/f5.webp" width="280" alt="Late-night encounter"></td>
</tr>
</table>
---
## Team & Vision
We're a group of young people from Tsinghua University and other schools.
+22
View File
@@ -18,6 +18,28 @@ InfiPlot是一款AI实时生成内容的互动剧情游戏,这里没有预设
---
## 📸 游戏截图
<table>
<tr>
<td><img src="docs/screenshots/4.webp" width="280" alt="高中天台黄昏对话"></td>
<td><img src="docs/screenshots/c3.webp" width="280" alt="夕阳下的告白"></td>
<td><img src="docs/screenshots/c5.webp" width="280" alt="天台上的选择"></td>
</tr>
<tr>
<td><img src="docs/screenshots/c7.webp" width="280" alt="近景特写镜头"></td>
<td><img src="docs/screenshots/a4.webp" width="280" alt="校园暮色"></td>
<td><img src="docs/screenshots/5.webp" width="280" alt="城市夜景"></td>
</tr>
<tr>
<td><img src="docs/screenshots/d2.webp" width="280" alt="赛博朋克霓虹巷"></td>
<td><img src="docs/screenshots/f2.webp" width="280" alt="日系夜街相遇"></td>
<td><img src="docs/screenshots/f5.webp" width="280" alt="深夜街角邂逅"></td>
</tr>
</table>
---
## 团队与愿景
我们是一群来自清华大学等高校的年轻人。
+103 -83
View File
@@ -60,74 +60,94 @@ const OPTS: Opt[] = [
{ label: "内容节奏", items: ["慢热细腻", "紧凑爽快"], defaultIndex: 1 },
];
type StoryContent = { title: string; outline: string };
type StoryContent = { title: string; outline: string; style: string };
/* 每个性向 30 篇预设剧情,与图片 /home/{m|f}{i}.webp 按索引一一对应。
const STYLE_MAP: Record<string, string> = {
: "唯美二次元动漫插画,日系 galgame 精致质感,柔和温暖的自然光照。",
: "吉卜力工作室风格,手绘动画质感,柔和水彩底色,温暖治愈的氛围。",
: "真实电影感,柔和自然光照,胶片颗粒。",
: "超写实人像与场景,电影级布光,皮肤与材质细节精致。",
: "水彩插画,湿润晕染笔触,纸纹底色。",
: "像素风格,复古游戏 16-bit 调色,方块化几何造型。",
: "现代日系动画 cel-shading,硬光阴影分层,赛璐璐风。",
"3D 渲染": "3D 渲染卡通风格,柔和次表面散射,干净的电影级布光。",
: "蒸汽朋克美学,铜色齿轮与蒸汽,工业革命氛围。",
: "国风玄幻插画,仙气缭绕,群山烟雨与神兽萦绕。",
: "国潮唯美古风插画,水墨微晕渲染,仙侠浪漫色彩,极具东方神韵。",
: "赛博朋克都市,霓虹反射湿润街道,电子义体高光。",
};
/* 每个性向 32 篇预设剧情(红果短视频式开场钩子)。与封面 /home/{m|f}{i}.webp 按索引
一一对应;style 字段决定点卡片进入 /play 时使用的画风(对应 styleMap 的 12 种风格)。
男/女同索引共享画面尺寸,切性向 crossfade 时卡片高度不跳变。 */
const STORIES: Record<Gender, StoryContent[]> = {
: [
{ title: "樱の约定", outline: "樱花纷飞的黄昏,他终于鼓起勇气,向并肩走过六年的青梅竹马说出那句话……" },
{ title: "锈色边境", outline: "漫天黄沙的废土,机械心脏在胸腔中沉重轰鸣。我从钢铁山中挖出一个完好的休眠舱……" },
{ title: "云海仙踪", outline: "凡骨少年偶得神秘残碑,登顶云海仙山,神魔同修之路自此开启。" },
{ title: "六月雨季", outline: "南方县城的多雨六月,转学第一天,注意到那个总在天台读诗的同学。雨水打湿了未送出的伞……" },
{ title: "雨夜霓虹", outline: "2087 年东亚特区的酸雨之夜,丢失了三天记忆的我,手腕终端响起一通匿名警告:「他们来找你了」。" },
{ title: "学院秘闻", outline: "深夜图书馆地下密室,清冷孤僻的班长跪在圆环阵法前,吟诵着不属于人类的咒词。" },
{ title: "异界召唤", outline: "再睁眼,没有班主任,只有昏暗的魔法阵与一位哭得梨花带雨的圣女:「勇者大人,请拯救这个世界。」" },
{ title: "花火之夜", outline: "夏祭的夜空下,浴衣女孩与你约定,今晚最后一发烟火,要一起看完。" },
{ title: "霓虹之外", outline: "漂浮的飞车与古老方块字的全息广告——这是赛博东亚的另一种黎明。" },
{ title: "放学后的车站", outline: "夕阳染红的乡间月台,无人列车迟迟未来,你和她沉默并立。" },
{ title: "星辰咒语", outline: "古老图书馆深处,星纹长袍下的法师女孩低声念出禁咒。" },
{ title: "战姬启动", outline: "紧急警报红光中,少女握紧操纵杆——决战时刻已到。" },
{ title: "街灯之下", outline: "午夜独行的女侦探,雨雾中藏着尚未揭晓的真相。" },
{ title: "全息伞下", outline: "霓虹雨夜,两人共撑全息伞——这一次,是道别还是开始?" },
{ title: "竹林之约", outline: "竹林深处的快意一战,落叶纷飞——谁先收剑?" },
{ title: "暗夜王座", outline: "烛光摇曳的古老王座之上,公主等待着她唯一的回信。" },
{ title: "放学独白", outline: "阳光斜射的空教室,最后一个学生在笔记本上写着什么?" },
{ title: "第七封信", outline: "樱花树下展开的信纸,淡淡的笔迹,字字千钧。" },
{ title: "月神降临", outline: "银发倾泻、极光环绕——传说中的月神,今夜降临凡间。" },
{ title: "血月武士", outline: "血色满月之下,刀光与樱瓣同时落下。" },
{ title: "森林女巫", outline: "烛光摇曳的森林小屋,女巫熬制着能改变命运的魔药。" },
{ title: "夏日海岸", outline: "粉橙色的夕阳,两个挚友坐在海岸边,把秘密轻轻放进海风里。" },
{ title: "屏幕之间", outline: "霓虹青光映在脸上,全屏代码下藏着被遗忘的真相。" },
{ title: "雨夜客栈", outline: "雨夜投宿的破败客栈,邻桌蒙面女子的剑匣里,似乎封着一段江湖恩怨。" },
{ title: "深空警报", outline: "殖民舰舰桥警报骤响,舷窗外那颗未知行星正泛起诡异的红光。" },
{ title: "上海滩暗号", outline: "1936 年的上海滩,留声机旋律里,舞女递来一张写着暗号的牌。" },
{ title: "三长两短", outline: "末世第 173 天,卷帘门外的抓挠声停了,取而代之的是规律的敲门——三长两短。" },
{ title: "正午对决", outline: "正午烈日下的无人小镇,唯一的酒馆门口,一个陌生枪手正等着与我决斗。" },
{ title: "万米之城", outline: "潜水钟沉入万米海沟,探照灯扫过的不是岩壁,而是一座沉睡的远古之城。" },
{ title: "云上海盗", outline: "齿轮轰鸣的飞空艇甲板,云海之上,海盗的黑色气球正逼近舷侧。" },
{ title: "战神归来", outline: "五年前我战死边境,灵柩送回家时她抱着儿子改嫁了。今天我站在他们的婚礼门口,新郎刚要骂人,跪在他面前的二十个保镖喊了我一声「上将」。", style: "真实系" },
{ title: "神医归乡", outline: "在城里被嘲笑成「江湖野医生」的我,回了一趟老家。村口的老人见到我直接哭了:「您终于回来了,您当年的师父…病了。」其实他们不知道,我现在是国手第一。", style: "吉卜力" },
{ title: "赘婿亮剑", outline: "岳父大寿,我端着茶被全场嫌弃,一句「废物」让我滚出去。门外停着九辆悬挂军牌的劳斯莱斯,下来的人朝我深深一鞠躬:「少爷,集团等您回去签字。」", style: "真实系" },
{ title: "送外卖的少主", outline: "你以为我是给你送了三个月外卖的那个小哥?昨晚有人对我说:「少主,您隐姓埋名的三年,到了。」——而你昨天还笑我连一杯咖啡都买不起。", style: "二次元" },
{ title: "兵王食言", outline: "退役那天我答应过队长:「这辈子不再开枪。」但你今天在我面前打了她一巴掌,那我食言一次。", style: "真实系" },
{ title: "重生分手前夜", outline: "凌晨四点,我醒在我们分手的那个夜晚——她正打开门要走。这一次,我先把戒指递了出去:「分手,但戒指你拿好,下个月你会用到它。」", style: "日系动画" },
{ title: "重生回到高考前", outline: "我重生回到高考前一周。这一次,我提前知道了每一道压轴题,也知道了——三天后,她会在天台上跳下去。", style: "吉卜力" },
{ title: "墓前签到", outline: "我每天去亡妻的墓地签到,第七天,系统弹出一行字:「奖励到账:未亡人 × 1。」墓碑后走出一个长得和她一模一样的姑娘:「你是…谁?」", style: "二次元" },
{ title: "凌晨四点抽卡", outline: "凌晨三点,我十连抽 SSR 出货,光柱从屏幕里溢出来。客厅响起脚步声,一个穿着我 T 恤的女人揉着眼睛走出来:「老公,你也太晚了。」", style: "3D 渲染" },
{ title: "系统选妃", outline: "系统给了我七个未婚妻候选,每错一个,地图上就有一座城被抹掉。倒计时 30 秒,她们七个同时朝我看过来。", style: "二次元" },
{ title: "穿成废柴皇子", outline: "睁眼是冷宫废柴皇子,太监正在念赐死圣旨。我笑了——上辈子读的那本《这就是大唐》,是我自己写的。", style: "国风水墨" },
{ title: "穿成乙游男配", outline: "我穿成了乙游里第一章就被处刑的反派男配。倒计时三个月。可女主她…昨天竟然主动来找我了。", style: "二次元" },
{ title: "毒酒之后", outline: "睁眼是 1928 年,我刚被亲弟弟下毒,倒在少帅府的红毯上。门外军靴声逼近——他来确认我是不是真死了。", style: "真实系" },
{ title: "九重雷劫", outline: "修了三百年,今夜九重雷劫降下。第八道劫雷劈开时,我看见劫云之上,那个一直在偷偷护我的人,竟是她。", style: "玄幻" },
{ title: "山门扫地僧", outline: "我在山门扫地三十年,谁都看不起我。今日魔尊踏破山门,宗主跪地求饶。我抬头:「让一让,我去扫他。」", style: "国风水墨" },
{ title: "末世第一夜", outline: "同寝的兄弟开始啃我的脖子。我抬手将他甩开——指尖滴下的血珠悬在半空,凝结成了一柄银白小剑。", style: "真实系" },
{ title: "雷霆觉醒", outline: "雷劈不死的第七天,我握紧了拳头。掌心炸开一道闪电,把面前的丧尸群一齐劈成了灰。", style: "赛博朋克" },
{ title: "家宴镇压", outline: "家宴上岳父冷笑:「你也敢上桌?」我手机震了一下,是父亲发来的:「儿,神州七大家主,已到楼下。」", style: "真实系" },
{ title: "买葱归来", outline: "二十年前那场天工大会上消失的人——今天回菜市场买葱,被小贩多收了两毛。他笑了:「这二十年的利息,连本带利,今晚一起还。」", style: "国风水墨" },
{ title: "红盖头之下", outline: "敌对家族送来一个新娘,遮着红盖头。我掀开那一刻,下面是和我死去的妹妹一模一样的脸。她抬眼:「哥…你别杀我。」", style: "超写实" },
{ title: "上海双面谍", outline: "1936 年。军统让我潜入日方,日方让我潜入军统。今晚——他们要见面,而我必须同时出现在两间房里。", style: "真实系" },
{ title: "比武场的茶博士", outline: "比武大会上,我端着茶水路过,宗主们的剑突然全都举不起来了。我抬眼:「老衲只是看不下去你们吵架。」", style: "国风水墨" },
{ title: "高考前夜", outline: "全市模考垫底的我,高考前夜被四个西装男按在桌前:「这次,你必须考第一。」原来——我爸是教育部的人。", style: "日系动画" },
{ title: "失踪一年", outline: "我被宣告死亡 12 个月后,背着血迹斑斑的包,站在了她婚礼现场的门口。新郎认出我,杯子摔到了地上。", style: "真实系" },
{ title: "天台堵她", outline: "学校最不好惹的那位转学生,第一天就堵了我的天台。我把她书包一扯——里面掉出来一沓我从小写的情书。", style: "日系动画" },
{ title: "转学第一天", outline: "转学第一天,年级第一坐我后桌。下课她把试卷拍在我面前:「这道题,你为什么写得和我答案一字不差?」", style: "二次元" },
{ title: "无职觉醒", outline: "成年礼上全班觉醒职业,只有我天命「无职」。所有人嘲笑我的时候,光柱从我身上炸开——觉醒结果:「神」。", style: "玄幻" },
{ title: "草稿纸里的我", outline: "睁眼发现自己是一张草稿纸上的火柴小人,住在 16-bit 的网格世界里。橡皮擦从天而降,正在抹掉这一行字——也包括我。", style: "像素风" },
{ title: "云上的国家", outline: "齿轮轰鸣的飞艇甲板上,独眼船长把望远镜递到我手里:「云的那一头有个国家,专门关像你这样的人。」", style: "蒸汽朋克" },
{ title: "舰桥上的少年", outline: "殖民母舰只剩 30 秒,主炮指挥官的椅子是空的。舰长抬眼看着 17 岁的我:「上去。整个人类,就交给你了。」", style: "赛博朋克" },
{ title: "末节队长服", outline: "全联盟都骂我废柴,机甲赛决赛末节,教练把队长徽章按在我手里:「上去,把这局赢回来——这一台,是人类最后的机甲。」", style: "赛博朋克" },
{ title: "学长的真面目", outline: "三年青梅当众接过富二代的玫瑰,转身扑进他怀里。我笑了笑——明天,是我接手父亲那个上市公司的日子。", style: "真实系" },
],
: [
{ title: "摄政王独宠", outline: "穿成将军府的废嫡女,冷面摄政王却把整个京城最名贵的红玉镯,亲手戴在了我的腕上……" },
{ title: "重生前夕", outline: "重生回到分手前夜,他还没说出那句「对不起」。这一次,让我先转身。" },
{ title: "恶役千金", outline: "一觉醒来,竟成了乙游里被命运钦点的恶役千金,要躲开所有 BAD END……" },
{ title: "天台之上", outline: "南方多雨的六月,转学第一天,我把伞悄悄递给了那个在天台读诗的少年。" },
{ title: "登基之夜", outline: "登基大典上群臣俯首,而我只想看那个一直立在阴影里的人,今夜会不会上前一步。" },
{ title: "江湖玉颜", outline: "江湖传言,那位执剑女侠从不动情。可那个雨夜,她为他收剑而立。" },
{ title: "学长的告白", outline: "夕阳染红了天台,那个总在篮球场被全校女生围观的学长,第一次叫住了我。" },
{ title: "夏祭灯影", outline: "夏祭的夜空下,他替你挡开人潮,低声说:最后一发烟火,只想和你一起看完。" },
{ title: "雨夜车站", outline: "末班电车迟迟未至,他脱下外套披在你肩上,霓虹在积水里碎成星河。" },
{ title: "黄昏并肩", outline: "夕阳染红的乡间月台,他终于停下脚步回头看你——那句话堵在喉咙里很久了。" },
{ title: "禁书之约", outline: "图书馆最深处,清冷的学生会长合上禁书,抬眼时眸色温柔得不像他。" },
{ title: "骑士誓约", outline: "红色警报响彻舰桥,他单膝跪在面前:以剑起誓,此生只为你出鞘。" },
{ title: "雨巷追影", outline: "午夜雨巷,他撑伞追上独行的你:这条路太黑,我送你回去。" },
{ title: "共伞之间", outline: "霓虹雨夜,他把全息伞偏向你这侧,自己半边肩膀已被雨打湿。" },
{ title: "竹影收剑", outline: "竹林深处刀光骤停,他为你收剑而立,落叶落在你们之间。" },
{ title: "深宫回眸", outline: "烛影摇红的宫宴上,冷面摄政王越过群臣,只朝你伸出了手。" },
{ title: "空教室", outline: "夕照斜斜铺满空教室,他把写满字的笔记本推到你面前,耳尖泛红。" },
{ title: "樱下情书", outline: "樱花树下,他递来第七封信,这一次落款不再是匿名。" },
{ title: "月下倾心", outline: "银发垂落、极光环绕,传说中的月神俯身,指尖轻触你的脸颊。" },
{ title: "血月相护", outline: "血色满月之下,他挡在你身前,刀光与樱瓣同时落下。" },
{ title: "魔药之约", outline: "森林小屋烛火摇曳,他为你熬一剂改写命运的魔药,只求换你一笑。" },
{ title: "海岸絮语", outline: "粉橙色夕阳里,他和你并肩坐在堤岸,把没说出口的心事交给海风。" },
{ title: "屏光之后", outline: "幽蓝屏光映在他脸上,敲下最后一行代码,他转头:我找到你了。" },
{ title: "龙王契约", outline: "古龙巢穴深处,化为人形的银发龙王单膝跪地,将一枚龙鳞戒指推到我面前。" },
{ title: "洋场先生", outline: "1936 年的上海公馆,那位留洋先生替我挡下流弹,西装袖口洇开一片猩红。" },
{ title: "最后一颗子弹", outline: "末世第 173 天,他用最后一颗子弹打穿破门的丧尸,转身把我护在身后。" },
{ title: "古堡伯爵", outline: "雾锁古堡的舞会上,苍白俊美的伯爵俯身吻过我的手背,唇下却没有一丝温度。" },
{ title: "鞍前", outline: "黄沙漫天的西部小镇,沉默的赏金猎人翻身上马,伸手把我拉上他的鞍前。" },
{ title: "深海王子", outline: "潜入万米海沟的遗迹,发光的人鱼王子环住我的腰,带我穿过沉睡的古城。" },
{ title: "只属于我们的航线", outline: "飞空艇甲板上,独眼船长把望远镜递到我眼前:「看,那是只属于我们的航线。」" },
{ title: "废柴嫡女", outline: "穿成将军府众人嫌弃的废嫡女,第一天就被打了一巴掌。门外冷面摄政王翻身下马,「我夫人的脸,谁敢动?」", style: "国风水墨" },
{ title: "乙游恶役", outline: "睁眼是乙游里五分钟必死的恶役千金,所有男主都恨我。我合上剧本笑了——上一世我是这游戏的主笔。", style: "二次元" },
{ title: "白月光归来", outline: "穿成男主念念不忘的白月光,但全书她只有死亡这一种结局。我捏着男主送的玉佩走进祠堂——这一次,我不躲了。", style: "玄幻" },
{ title: "凤袍之下", outline: "穿越来就是当朝皇后,三千佳丽看我笑话。皇上掀开龙袍跪在我面前:「皇后,朕想她想了三十年了。」", style: "国风水墨" },
{ title: "嫁错重生", outline: "嫁错了人毁了一辈子,重生回到婚礼前夜。这一次新娘休书我先写。新郎的弟弟突然走进来:「嫂子,要换人,换我。」", style: "二次元" },
{ title: "那杯咖啡", outline: "重生回到他亲手把我送进车祸的前夜。我笑着接过他递来的咖啡——这是一杯我前世死前最想泼他脸上的咖啡。", style: "真实系" },
{ title: "雨中撑伞", outline: "重生回到我亲手要了她命的前一天。她正抱着公文包路过我的车——这一次,我下车撑伞。", style: "真实系" },
{ title: "三十亿合同", outline: "重生回到我被父亲扫地出门的那个清晨。这一次,扫地出门前我把家族 30 亿的合同提前签了。", style: "真实系" },
{ title: "替嫁霸总", outline: "替姐姐嫁给那个传说眼瞎心冷的总裁。新婚夜他俯身在我耳边:「你姐没告诉你?我等了你三年了。」", style: "二次元" },
{ title: "错嫁那一夜", outline: "醉酒夜我闯进了错的酒店房间,醒来戒指已在手上。他穿好西装回头:「夫人,签字仪式三小时后。」", style: "真实系" },
{ title: "撕了离婚书", outline: "为了避税,我和那个最讨厌我的总裁假结婚一年。半年后他突然把离婚协议撕了——「续约。」", style: "真实系" },
{ title: "死对头跪了", outline: "天天和我互掐的死对头,今天跪在面前。他递上戒指:「再吵下去要影响我们的孩子。」——什么孩子?!", style: "二次元" },
{ title: "抽到的霸总", outline: "凌晨四点抽到 UR 卡——画面里是城里那个传说没人见过脸的盛家总裁。第二天他敲我家门:「我来报到。」", style: "3D 渲染" },
{ title: "攻略任务", outline: "系统说:「攻略他,否则你死。」可他是这本书里唯一恨我入骨的人。今天他亲手把我堵在了墙角。", style: "二次元" },
{ title: "商城上架", outline: "系统商城上架了「市值 800 亿盛总 × 1」。我咬牙刷光积蓄。下一秒,他出现在我家门口:「夫人,我已购入。」", style: "二次元" },
{ title: "老公赞助", outline: "直播间打赏榜第一名连续 30 天,备注写着「老公赞助」。我点开他的资料——城里那位传说从不出门的盛少。", style: "日系动画" },
{ title: "门外的他", outline: "末世第一夜,门外是丧尸群的撕咬声。隔壁刚搬来的男人撞开我家门:「我能进来吗?我有一把枪。」", style: "真实系" },
{ title: "末世空间", outline: "末世爆发的第一天,我意外觉醒了储物空间。屯了三车物资回家,发现那个总欺负我的高冷邻居跪在我门口。", style: "真实系" },
{ title: "异能撒娇", outline: "末世里所有男人都怕的那位 S 级异能者,今天蹲在我家门口:「姐姐,能让我进去吗?外面…丧尸太可怕了。」", style: "二次元" },
{ title: "末世重生", outline: "重生回到末世爆发前一周。这一次,那个抛弃我的男人——我先把他赶出门,把上一世救我的人接回家。", style: "真实系" },
{ title: "课桌里的纸条", outline: "隔壁班那个高冷年级第一,今天把一本日记塞进我课桌。第一页写着:「她笑起来的时候,三角函数都没那么复杂。」", style: "二次元" },
{ title: "校草八年", outline: "暗恋了八年的校草,今天突然走到我面前:「跟我走,我已经查清楚了——把你妹妹接走的那个人在哪。」", style: "吉卜力" },
{ title: "班长的秘密", outline: "天天和我同桌的班长,今天被四个保镖按在校门口接走。临走前他回头喊:「老婆,我先回总部一趟。」", style: "二次元" },
{ title: "走廊的手腕", outline: "走廊上人最多的时候,全校最不好惹的学长抓住了我的手腕:「我等了你三年,今天给我一个回应。」", style: "日系动画" },
{ title: "上海公馆", outline: "1936,我是父亲遗产的唯一继承人,全上海都在等看我嫁谁。今晚我推开门——那个传说不要女人的留洋先生,在喝我父亲的茶。", style: "超写实" },
{ title: "书店里的他", outline: "我是租界一家书店的老板娘。今晚穿西装的他第三次坐在窗边,第一次开口:「小姐,可以借您的店…藏一个东西吗?」", style: "真实系" },
{ title: "炼丹意外", outline: "我是仙门最废柴的炼丹弟子,三年没炼出一颗丹。今天偶然撞翻师尊的丹炉——一道光柱直冲云霄,惊动了三大长老。", style: "玄幻" },
{ title: "江湖归人", outline: "我一个人闯江湖三年,今天回到那座小镇。门口的少年抬头:「师姐,你说过五年就回,我等了三年又两个月。」", style: "国风水墨" },
{ title: "顶流的西瓜", outline: "顶流男星上节目被问感情,他笑了笑:「我老婆?她现在大概在家里啃我刚买的西瓜。」全网爆炸——我正趴在沙发上看直播。", style: "真实系" },
{ title: "同居一年", outline: "和合租室友同居一年了,今晚他突然把我堵在门口:「你说,我们…要不要别再装陌生人了?」", style: "日系动画" },
{ title: "机甲撞门", outline: "丧尸潮第七夜,全城断电。地下室的门被撞开,一架满是弹痕的机甲低下头,舱门弹开——里面坐着我那个失联三年的他。", style: "赛博朋克" },
{ title: "三分绝杀", outline: "决赛最后一秒,他在场边看了我一眼,转身投出那一记三分。哨声响时,他把奖杯举过头顶,朝我跑来。", style: "日系动画" },
],
};
@@ -466,20 +486,6 @@ export default function HomePage() {
.filter(Boolean)
.join("\n");
const styleMap: Record<string, string> = {
: "唯美二次元动漫插画,日系 galgame 精致质感,柔和温暖的自然光照。",
: "吉卜力工作室风格,手绘动画质感,柔和水彩底色,温暖治愈的氛围。",
: "真实电影感,柔和自然光照,胶片颗粒。",
: "超写实人像与场景,电影级布光,皮肤与材质细节精致。",
: "水彩插画,湿润晕染笔触,纸纹底色。",
: "像素风格,复古游戏 16-bit 调色,方块化几何造型。",
: "现代日系动画 cel-shading,硬光阴影分层,赛璐璐风。",
"3D 渲染": "3D 渲染卡通风格,柔和次表面散射,干净的电影级布光。",
: "蒸汽朋克美学,铜色齿轮与蒸汽,工业革命氛围。",
: "国风玄幻插画,仙气缭绕,群山烟雨与神兽萦绕。",
: "国潮唯美古风插画,水墨微晕渲染,仙侠浪漫色彩,极具东方神韵。",
: "赛博朋克都市,霓虹反射湿润街道,电子义体高光。",
};
// 「自动」→ fall back to 二次元 (project default). Plain prompts like
// "由模型自动判断画风" are not understood by FLUX — it just paints them
// literally, so we'd rather lock in a sensible default.
@@ -487,7 +493,7 @@ export default function HomePage() {
// 选出最合适的画风,再映射到对应风格提示词,而非固定回退到二次元。届时
// 同步更新风格弹窗副标题(「由模型根据 prompt 判断风格」)使文案与行为一致。
const effectiveStyle = artStyle === "自动" ? "二次元" : artStyle;
const styleGuide = styleMap[effectiveStyle] ?? styleMap["二次元"]!;
const styleGuide = STYLE_MAP[effectiveStyle] ?? STYLE_MAP["二次元"]!;
const audioEnabled = voice === "开启";
sessionStorage.setItem(
@@ -497,14 +503,28 @@ export default function HomePage() {
router.push("/play?custom=1");
};
const onCardClick = (seed?: string) => {
if (seed) setPrompt(seed);
inputRef.current?.focus();
};
const stories = STORIES[galleryGender];
const imgPrefix = galleryGender === "女性向" ? "f" : "m";
// 点卡片 = 直接开始这张卡的故事,零等待:跳 /play?card=m0/f0... 由 /play
// 页面从 /home/firstact/{name}.json 静态文件加载预烘焙好的首幕(含 scene /
// 角色 / 图片 URL / storyState),整张图都已在 FLUX 上画好且 URL 缓存命中。
// 「语音配音」选择仍然生效:把 audioEnabled 留在 sessionStorage 里,/play 的
// useState 初始化器会读它来设 muted 初值。其余选项(剧情风格 / 内容节奏)
// 在预烘焙时已锁成「多线转折 / 紧凑爽快」的红果默认基调,对精选卡不再生效。
const onCardClick = (idx: number, card: StoryContent) => {
const voice = OPTS[3]!.items[sel[3] ?? 1]!;
const audioEnabled = voice === "开启";
// 复用 infiplot:custom 这个 key 只为传递 audioEnabled —— ws/sg 在 ?card= 路径
// 上不会被读取(/play 里 cardName 优先级高于 sessionStorage)。这样实现量最小,
// 不必另起一个 audio-only 的 storage key。
sessionStorage.setItem(
"infiplot:custom",
JSON.stringify({ worldSetting: "", styleGuide: "", audioEnabled }),
);
router.push(`/play?card=${imgPrefix}${idx}`);
};
return (
<div className="min-h-screen flex flex-col">
{/* ================== HEADER ================== */}
@@ -640,7 +660,7 @@ export default function HomePage() {
title={c.title}
outline={c.outline}
image={`/home/${imgPrefix}${i}.webp`}
onClick={() => onCardClick(c.outline)}
onClick={() => onCardClick(i, c)}
/>
))}
</div>
+59 -33
View File
@@ -485,48 +485,74 @@ function PlayInner() {
if (startedRef.current) return;
startedRef.current = true;
let payload: { worldSetting: string; styleGuide: string } | null = null;
// 三条进入路径:
// ?card=<m0..f31> → 首页精选卡,直接从 /home/firstact/{name}.json
// 静态文件加载(已在构建期 prebake,免一切引擎调用)
// ?preset=<id> → 内置 PRESETS(仍走 /api/start 现场生成)
// ?custom=1 → 用户自定义 promptsessionStorage 取 ws/sg
// 后走 /api/start 现场生成
const cardName = params.get("card");
const presetId = params.get("preset");
const isCustom = params.get("custom") === "1";
if (presetId) {
const p = PRESETS.find((x) => x.id === presetId);
if (p) payload = { worldSetting: p.worldSetting, styleGuide: p.styleGuide };
} else if (params.get("custom") === "1") {
const stored = sessionStorage.getItem("infiplot:custom");
if (stored) {
try {
const parsed = JSON.parse(stored) as {
worldSetting: string;
styleGuide: string;
audioEnabled?: boolean;
};
payload = { worldSetting: parsed.worldSetting, styleGuide: parsed.styleGuide };
// audioEnabled 已在 useState 初始化时反向投射到 muted;这里无需再额外存。
} catch {
payload = null;
let livePayload: { worldSetting: string; styleGuide: string } | null = null;
if (!cardName) {
if (presetId) {
const p = PRESETS.find((x) => x.id === presetId);
if (p) livePayload = { worldSetting: p.worldSetting, styleGuide: p.styleGuide };
} else if (isCustom) {
const stored = sessionStorage.getItem("infiplot:custom");
if (stored) {
try {
const parsed = JSON.parse(stored) as {
worldSetting: string;
styleGuide: string;
audioEnabled?: boolean;
};
livePayload = { worldSetting: parsed.worldSetting, styleGuide: parsed.styleGuide };
// audioEnabled 已在 useState 初始化时反向投射到 muted;这里无需再额外存。
} catch {
livePayload = null;
}
}
}
}
if (!payload) {
if (!cardName && !livePayload) {
router.replace("/");
return;
}
const finalPayload = payload;
type PrebakedFirstAct = StartResponse & {
worldSetting: string;
styleGuide: string;
cardName?: string;
cardTitle?: string;
cardGender?: string;
};
fetch("/api/start", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(finalPayload),
})
.then(async (r) => {
if (!r.ok) {
const j = (await r.json().catch(() => ({}))) as { error?: string };
throw new Error(j.error ?? r.statusText);
}
return (await r.json()) as StartResponse;
})
const fetchStart: Promise<PrebakedFirstAct> = cardName
? fetch(`/home/firstact/${encodeURIComponent(cardName)}.json`).then(
async (r) => {
if (!r.ok) throw new Error(`找不到精选剧情:${cardName}`);
return (await r.json()) as PrebakedFirstAct;
},
)
: fetch("/api/start", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(livePayload),
}).then(async (r) => {
if (!r.ok) {
const j = (await r.json().catch(() => ({}))) as { error?: string };
throw new Error(j.error ?? r.statusText);
}
const data = (await r.json()) as StartResponse;
// Live /api/start doesn't echo ws/sg back — splice in what we sent.
return { ...data, worldSetting: livePayload!.worldSetting, styleGuide: livePayload!.styleGuide };
});
fetchStart
.then(async (data) => {
// Decode the Runware image in memory before committing to state, so
// the <img> renders instantly when it mounts (same rationale as the
@@ -536,8 +562,8 @@ function PlayInner() {
const initial: Session = {
id: data.sessionId,
createdAt: Date.now(),
worldSetting: finalPayload.worldSetting,
styleGuide: finalPayload.styleGuide,
worldSetting: data.worldSetting,
styleGuide: data.styleGuide,
history: [
{
scene: data.scene,
Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 54 KiB

File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
{"sessionId":"s_1780390588623_46anec","scene":{"id":"scene_1780390628580_h42b","scenePrompt":"Wide establishing shot of a surreal digital landscape resembling a vast sheet of gridded draft paper. A colossal white eraser descends from the sky like a catastrophic meteor, casting a long, ominous shadow across the terrain. Pixelated mathematical formulas and geometric shapes are violently fracturing and collapsing into blocky debris. The atmosphere is tense and apocalyptic, rendered in a retro 16-bit pixel art style with a limited color palette. Vibrant cyan grid lines contrast against the stark white of the falling eraser. High-contrast lighting, dramatic shadows, geometric destruction, 16-bit aesthetic, vintage game graphics, no characters present.","beats":[{"id":"b1","narration":"耳边传来震耳欲聋的摩擦声,整片白色大地在剧烈颤抖。你抬头望去,一块遮天蔽日的白色矩形——橡皮擦,正带着毁灭的气息碾压而来。","next":{"type":"continue","nextBeatId":"b2"}},{"id":"b2","narration":"你身边的‘E=mc²’像素块瞬间粉碎成渣,逻辑正在被物理抹除。再慢一秒,你就会变成一堆毫无意义的石墨粉末。","speaker":"你","line":"开什么玩笑……我可是顶级黑客,怎么能死在这这种鬼地方!","next":{"type":"continue","nextBeatId":"b3"}},{"id":"b3","narration":"生死关头,你注意到左前方有一滩尚未干透的蓝黑色墨水渍,那里是规则混乱的盲区;而正上方,那双名为‘造物主’的巨大眼睛正透过镜片冷漠地注视着纸面。","speaker":"你","line":"拼了!到底是躲进阴影,还是赌她会停手?","next":{"type":"choice","choices":[{"id":"c1","label":"跳入深色墨水渍","effect":{"kind":"change-scene","nextSceneSeed":"墨水渍内部是混乱的16-bit废墟,你在这里遇到了那个自称‘黑影’的家伙"}},{"id":"c2","label":"拼成SOS求救","effect":{"kind":"change-scene","nextSceneSeed":"巨大的笔尖停在你面前,苏清月疑惑地靠近纸面,你第一次近距离看清这位造物主的脸"}}]}}],"entryBeatId":"b1","sceneKey":"draft-paper-grid-white","imageUuid":"95a0f0bb-961b-4e0c-9728-68f809c68619","imageUrl":"https://im.runware.ai/image/os/a05d22/ws/3/ii/95a0f0bb-961b-4e0c-9728-68f809c68619.png"},"imageUrl":"/home/firstscene/m27.webp","characters":[],"storyState":{"logline":"命悬一线的草稿纸火柴人,如何操纵现实世界的“造物主”,在被彻底抹除前逆天改命?","genreTags":"脑洞快穿 / 爽剧 / 元叙事 / 像素风生存","protagonist":"你曾是现实中被陷害的顶级黑客,意识意外坠入死对头女儿的草稿纸上。此刻你不仅失去了实体,还面临物理意义上的“抹除”。你必须利用16-bit世界的底层逻辑生存下去,并试图向纸外的现实世界复仇,你的软肋是只要纸张湿透,你的记忆就会永久溶解。","castNotes":"苏清月:手持画笔的“造物主”,看似清纯内向的艺术生,实则在草稿纸上发泄着极其阴暗的杀戮欲望;她是唯一能改写你命运的人。\n黑影:纸张折痕处生存的“涂鸦前辈”,性格乖张狡诈,曾多次从橡皮擦下逃生,他知道跳出这张纸的方法,但需要拿你的意识做交易。","synopsis":"你在死对头女儿的草稿纸上以火柴人身份苏醒,在橡皮擦的灭顶之灾中极限逃生。你意识到自己必须在被彻底抹除前,利用纸上的规则向外界传递信号。","openThreads":["导致你意识坠入草稿纸的幕后黑手身份","苏清月在草稿纸边缘画下的那些诡异血腥图案的含义"],"relationships":["苏清月:视你为随手涂鸦的造物主,目前尚未意识到你的自我意识"],"nextHook":"无论你选择躲藏还是求救,你都即将接触到这个纸面世界背后的第一个禁忌——苏清月的阴暗面。"},"cardName":"m27","cardTitle":"草稿纸里的我","cardGender":"男性向","worldSetting":"这是一款面向【男性向】观众的 AI 交互剧情游戏,整体走红果短视频式的强戏剧冲突与快速反转。\n剧情风格:多线转折。内容节奏:紧凑爽快。\n精选剧情《草稿纸里的我》的开场设定:睁眼发现自己是一张草稿纸上的火柴小人,住在 16-bit 的网格世界里。橡皮擦从天而降,正在抹掉这一行字——也包括我。\n请直接以此开场切入,给玩家强烈的代入感与爽点;后续分支保持短剧式的反转密度,让玩家每一次选择都能立刻看到回响。","styleGuide":"像素风格,复古游戏 16-bit 调色,方块化几何造型。","imageUrlRemote":"https://im.runware.ai/image/os/a05d22/ws/3/ii/95a0f0bb-961b-4e0c-9728-68f809c68619.png"}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 272 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 296 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Some files were not shown because too many files have changed in this diff Show More