Spaces:
Running
Running
Upload 2 files
Browse files- README.md +17 -5
- index.html +884 -19
README.md
CHANGED
|
@@ -1,10 +1,22 @@
|
|
| 1 |
---
|
| 2 |
-
title: Recommendation Pool
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: static
|
|
|
|
| 7 |
pinned: false
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: The Recommendation Pool
|
| 3 |
+
emoji: 🐟
|
| 4 |
+
colorFrom: yellow
|
| 5 |
+
colorTo: red
|
| 6 |
sdk: static
|
| 7 |
+
app_file: index.html
|
| 8 |
pinned: false
|
| 9 |
+
short_description: A toy model of a free video feed under AI output growth
|
| 10 |
---
|
| 11 |
|
| 12 |
+
# The Recommendation Pool
|
| 13 |
+
|
| 14 |
+
This is an interactive demo for the paper "After One-Click Video" (https://arxiv.org/abs/2610.37695).
|
| 15 |
+
|
| 16 |
+
It shows a free, ad-funded video platform. New content must pass three layers of trial viewing before it reaches the main feed.
|
| 17 |
+
|
| 18 |
+
Set how much more content AI lets people make (from 1x to 100x), press Start, and watch one year go by.
|
| 19 |
+
|
| 20 |
+
It is a simple illustration, not a forecast. The page runs fully in your browser and needs no network.
|
| 21 |
+
|
| 22 |
+
You can switch to Chinese with the button under the picture.
|
index.html
CHANGED
|
@@ -1,19 +1,884 @@
|
|
| 1 |
-
<
|
| 2 |
-
<
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<meta charset="utf-8">
|
| 2 |
+
<title>The Recommendation Pool</title>
|
| 3 |
+
<meta name="claude:viewport-fit-sides" content="cover">
|
| 4 |
+
<style>
|
| 5 |
+
/* 版式:一张横放的方格草稿纸,整张画面就是那个循环;控件在纸下方。单一纸面风格,不随深浅色切换。 */
|
| 6 |
+
:root{
|
| 7 |
+
--paper:#fbfbf7; --grid:#dfe7ee; --ink:#262a33; --ink-soft:#6b7280;
|
| 8 |
+
--cake:#e0484f; --pop:#f2c14e; --poop:#8a5a3b; --coin:#d9a520; --accent:#2f5fb3; --bad:#c2410c;
|
| 9 |
+
--font:"PingFang SC","Hiragino Sans GB","Microsoft YaHei","Noto Sans CJK SC","Source Han Sans SC",system-ui,sans-serif;
|
| 10 |
+
color-scheme:light;
|
| 11 |
+
}
|
| 12 |
+
html,body{background:var(--paper);color:var(--ink)}
|
| 13 |
+
body{font-family:var(--font);padding-left:max(16px,env(safe-area-inset-left,0px));padding-right:max(16px,env(safe-area-inset-right,0px));padding-block:16px 28px}
|
| 14 |
+
.wrap{max-width:1280px;margin:0 auto;display:flex;flex-direction:column;gap:12px}
|
| 15 |
+
.scene{position:relative;width:100%;aspect-ratio:16/9;max-width:100%;border:1.5px solid var(--ink);border-radius:6px;overflow:hidden;background:var(--paper)}
|
| 16 |
+
canvas{display:block;width:100%;height:100%}
|
| 17 |
+
.controls{display:flex;flex-wrap:wrap;gap:10px 18px;align-items:center}
|
| 18 |
+
.group{display:flex;flex-wrap:wrap;gap:6px;align-items:center}
|
| 19 |
+
.lab{font-size:13px;color:var(--ink-soft);letter-spacing:.04em}
|
| 20 |
+
button{font:inherit;font-size:14px;color:var(--ink);background:var(--paper);border:1.5px solid var(--ink);border-radius:999px;padding:6px 14px;cursor:pointer}
|
| 21 |
+
button:hover{background:#eef2f7}
|
| 22 |
+
button:focus-visible,input:focus-visible{outline:3px solid var(--accent);outline-offset:2px}
|
| 23 |
+
button.primary{background:var(--ink);color:var(--paper)}
|
| 24 |
+
button.primary:hover{background:#3b4150}
|
| 25 |
+
button[aria-pressed="true"]{background:var(--accent);border-color:var(--accent);color:#fff}
|
| 26 |
+
input[type=range]{width:220px;max-width:100%;accent-color:var(--accent)}
|
| 27 |
+
output{font-variant-numeric:tabular-nums;font-weight:700;min-width:4.2em;display:inline-block}
|
| 28 |
+
.note{font-size:12.5px;color:var(--ink-soft);line-height:1.6;max-width:72ch;margin:0}
|
| 29 |
+
@media (max-width:640px){input[type=range]{width:160px}}
|
| 30 |
+
</style>
|
| 31 |
+
|
| 32 |
+
<div class="wrap">
|
| 33 |
+
<div class="scene"><canvas id="c" width="1600" height="900" aria-label="推荐池模拟画面"></canvas></div>
|
| 34 |
+
<div class="controls">
|
| 35 |
+
<div class="group">
|
| 36 |
+
<button id="play" class="primary" type="button">开始</button>
|
| 37 |
+
<button id="reset" type="button">重来</button>
|
| 38 |
+
</div>
|
| 39 |
+
<div class="group">
|
| 40 |
+
<span class="lab" id="kLab">AI 产能倍数</span>
|
| 41 |
+
<input id="k" type="range" min="0" max="2" step="0.01" value="2" aria-labelledby="kLab">
|
| 42 |
+
<output id="kOut" for="k">×100</output>
|
| 43 |
+
<span class="group" id="presets"></span>
|
| 44 |
+
</div>
|
| 45 |
+
<div class="group">
|
| 46 |
+
<span class="lab" id="spLab">速度</span>
|
| 47 |
+
<button type="button" data-speed="0.5">慢</button>
|
| 48 |
+
<button type="button" data-speed="1" aria-pressed="true">正常</button>
|
| 49 |
+
<button type="button" data-speed="2.5">快</button>
|
| 50 |
+
</div>
|
| 51 |
+
<div class="group"><button id="lang" type="button">English</button></div>
|
| 52 |
+
</div>
|
| 53 |
+
<p class="note" id="note">示意模型。筛选部分(三层试看、完播率、晋级线)沿用论文图 1 的参数;用户去留、分成、成本、创作者去留等循环部分的规则与参数未经实证校准,只用来展示方向。</p>
|
| 54 |
+
</div>
|
| 55 |
+
|
| 56 |
+
<script>
|
| 57 |
+
// 推荐池演示:模拟核心(浏览器和命令行共用)
|
| 58 |
+
// 一步 = 一天。筛选部分(三层试看)沿用 模拟.py 的参数和公式;其余为示意规则,未经实证校准。
|
| 59 |
+
(function (root) {
|
| 60 |
+
"use strict";
|
| 61 |
+
|
| 62 |
+
// ---------- 写死的设定 ----------
|
| 63 |
+
const P = {
|
| 64 |
+
// 筛选部分:与 模拟.py 相同
|
| 65 |
+
COMPLETION: { cake: 0.40, pop: 0.30, poop: 0.30 }, // 试看完播率:好内容 / 平庸内容 / 批量垃圾(试看表现和平庸内容一样,区别在用户看完会反感)。数值都取自论文图 1
|
| 66 |
+
CUT: 0.35, // 晋级线
|
| 67 |
+
TIERS: [100, 500, 2500], // 基线下三层每条试看人数
|
| 68 |
+
TRIAL_SHARE: 0.10, // 用户时间里用于试看的份额
|
| 69 |
+
|
| 70 |
+
// 用户
|
| 71 |
+
U0: 10000, // 一倍时的用户数
|
| 72 |
+
VIEWS_PER_USER: 60, // 每人每天看多少条(固定)
|
| 73 |
+
JOY: { cake: 1.0, pop: 0.0, poop: -1.5 }, // 看一条带来的满意度
|
| 74 |
+
USER_REACT: 0.04, // 用户数每天向"目标人数"靠拢的速度
|
| 75 |
+
USER_MEMORY: 0.15, // 满意��的平滑:越小,用户记得越久
|
| 76 |
+
USER_SENS: 0.7, // 满意度每差 1,平台能留住的人数变化多少(相对一倍时)
|
| 77 |
+
|
| 78 |
+
// 钱
|
| 79 |
+
AD_PRICE: 0.01, // 每次观看卖给广告商的价钱(元)
|
| 80 |
+
CREATOR_SHARE: 0.30, // 分给创作者的比例;平台拿大头
|
| 81 |
+
COST_PER_ITEM: 0.1, // 每条内容的存储、分发、审核成本(元)
|
| 82 |
+
COST_FIXED: 600, // 平台每天的固定开支(元)
|
| 83 |
+
|
| 84 |
+
// 创作者:一倍时
|
| 85 |
+
RATE0: { cake: 0.2, pop: 0.5, poop: 0.2 }, // 每人每天做几条(做蛋糕的五天一条)
|
| 86 |
+
ENTRY0: { cake: 0.27, pop: 13 }, // 每天进来试试的新人
|
| 87 |
+
WAGE: 22, // 每天至少要挣到多少才肯留下(元);做蛋糕的、做爆米花的、机器人都一样
|
| 88 |
+
PATIENCE: { cake: 30, pop: 30, poop: 20 }, // 连续挣不够多少天就走
|
| 89 |
+
RETIRE: 0.005, // 每天自然退出的比例
|
| 90 |
+
|
| 91 |
+
// AI 产能倍数 k 的作用
|
| 92 |
+
HUMAN_RATE_EXP: 0.65, // 真人每人产能 × k^0.65(要打磨,涨得少;一百倍时约二十倍)
|
| 93 |
+
TALENT_EXP: 0.35, // 工具释放有能力的人:做蛋糕的新人 × k^0.35。两者相乘,蛋糕总量 × k(乐观设定,同论文"同比例增加")
|
| 94 |
+
BOT_RATE_EXP: 1.0, // 做便便的:按账号算,每个号产能 × k(用 Boo 的数:不管质量,一百倍时一天二十条)
|
| 95 |
+
BOT_COST_EXP: 0.5, // 每个号每天要挣回的成本 = 工钱 ÷ k^0.5:AI 越强,一个人能管的号越多,摊到每个号上越便宜
|
| 96 |
+
|
| 97 |
+
POOP_PER_K: 320, // 倍数每涨 1,做便便的号的"目标数"多 160 个(平台收入不变时)
|
| 98 |
+
POOP_ARRIVE: 0.08, // 号数低于目标时,每天补上差距的 8%
|
| 99 |
+
POOP_LEAVE: 0.03, // 号数高于目标时,每天走掉超出部分的 3%
|
| 100 |
+
BOT_ENTRY_BASE: 1.0, // 有利可图时每天冒出来的机器人基数
|
| 101 |
+
BOT_COPY: 0.15, // 跟风:已有机器人越多,跟进得越快
|
| 102 |
+
CONVERT: 0.5, // 新机器人里,由做爆米花的人转行过来的比例
|
| 103 |
+
|
| 104 |
+
TOP_LIFE: 3, // 进了最上层的内容在推荐流里待几天
|
| 105 |
+
};
|
| 106 |
+
|
| 107 |
+
// 与 模拟.py 相同的晋级概率(正态近似;试看人数少于 1 按 1 计)
|
| 108 |
+
function erf(x) { // Abramowitz–Stegun 7.1.26,误差 < 1.5e-7
|
| 109 |
+
const s = Math.sign(x); x = Math.abs(x);
|
| 110 |
+
const t = 1 / (1 + 0.3275911 * x);
|
| 111 |
+
const y = 1 - (((((1.061405429 * t - 1.453152027) * t) + 1.421413741) * t - 0.284496736) * t + 0.254829592) * t * Math.exp(-x * x);
|
| 112 |
+
return s * y;
|
| 113 |
+
}
|
| 114 |
+
function cdf(x) { return 0.5 * (1 + erf(x / Math.SQRT2)); }
|
| 115 |
+
function passRate(p, n) { n = Math.max(n, 1); return 1 - cdf((P.CUT - p) / Math.sqrt(p * (1 - p) / n)); }
|
| 116 |
+
|
| 117 |
+
function rng(seed) { // 可复现的随机数
|
| 118 |
+
let s = seed >>> 0 || 1;
|
| 119 |
+
return function () { s ^= s << 13; s >>>= 0; s ^= s >> 17; s ^= s << 5; s >>>= 0; return s / 4294967296; };
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
const TYPES = ["cake", "pop", "poop"];
|
| 123 |
+
|
| 124 |
+
// 一倍时各层的流入量(每条新内容 = 1),用来确定每层的固定试看预算
|
| 125 |
+
const BASE_MIX = { cake: 0.05, pop: 0.95, poop: 0 };
|
| 126 |
+
const BASE_IN = (function () {
|
| 127 |
+
let m = Object.assign({}, BASE_MIX); const ins = [];
|
| 128 |
+
for (const n of P.TIERS) {
|
| 129 |
+
ins.push(m.cake + m.pop + m.poop);
|
| 130 |
+
m = { cake: m.cake * passRate(P.COMPLETION.cake, n), pop: m.pop * passRate(P.COMPLETION.pop, n), poop: 0 };
|
| 131 |
+
}
|
| 132 |
+
return ins;
|
| 133 |
+
})();
|
| 134 |
+
const TRIAL_VIEWS_PER_ITEM0 = P.TIERS.reduce((a, n, i) => a + n * BASE_IN[i], 0); // 一倍时每条新内容平均消耗的试看次数
|
| 135 |
+
const V0 = P.U0 * P.VIEWS_PER_USER * P.TRIAL_SHARE / TRIAL_VIEWS_PER_ITEM0; // 一倍时每天的新内容条数
|
| 136 |
+
|
| 137 |
+
// 一层层筛:给定今天各类新内容条数和用户数,返回各层流入、最上层产出、试看分布
|
| 138 |
+
function sieve(counts, users) {
|
| 139 |
+
const budgetScale = users / P.U0; // 注意力 ∝ 用户数
|
| 140 |
+
let m = Object.assign({}, counts);
|
| 141 |
+
const tiers = [];
|
| 142 |
+
const trialViews = { cake: 0, pop: 0, poop: 0 };
|
| 143 |
+
for (let i = 0; i < P.TIERS.length; i++) {
|
| 144 |
+
const inflow = m.cake + m.pop + m.poop;
|
| 145 |
+
const budget = P.TIERS[i] * BASE_IN[i] * V0 * budgetScale; // 这一层每天的固定试看总量
|
| 146 |
+
const n = inflow > 0 ? budget / inflow : P.TIERS[i];
|
| 147 |
+
const next = {};
|
| 148 |
+
for (const t of TYPES) { trialViews[t] += m[t] * n; next[t] = m[t] * passRate(P.COMPLETION[t], n); } // 实际花掉的试看次数按预算分;晋级概率按 模拟.py 的口径(少于 1 人按 1 计)
|
| 149 |
+
tiers.push({ inflow, n, mix: Object.assign({}, m) });
|
| 150 |
+
m = next;
|
| 151 |
+
}
|
| 152 |
+
return { tiers, top: m, trialViews };
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
function makeWorld(opts) {
|
| 156 |
+
opts = opts || {};
|
| 157 |
+
const R = rng(opts.seed || 7);
|
| 158 |
+
const w = {
|
| 159 |
+
day: 0, k: 1, kTarget: opts.k || 1, aiDay: opts.aiDay == null ? 20 : opts.aiDay,
|
| 160 |
+
users: P.U0, sat: 0, creators: [], nextId: 1,
|
| 161 |
+
topPool: [], // 每天进最上层的 {cake,pop,poop}
|
| 162 |
+
log: [], events: { left: [], joined: [], usersLeft: 0, usersJoined: 0 },
|
| 163 |
+
today: null, R,
|
| 164 |
+
};
|
| 165 |
+
// 一倍时的稳态人数
|
| 166 |
+
const nCake = Math.round(P.ENTRY0.cake / P.RETIRE * 1.0);
|
| 167 |
+
const nPop = Math.round(P.ENTRY0.pop * P.PATIENCE.pop * 0.9);
|
| 168 |
+
for (let i = 0; i < nCake; i++) w.creators.push(newCreator(w, "cake", Math.floor(R() * 200)));
|
| 169 |
+
for (let i = 0; i < nPop; i++) { const c = newCreator(w, "pop", 0); c.broke = Math.floor(R() * P.PATIENCE.pop); w.creators.push(c); }
|
| 170 |
+
// 预热:把最上层推荐流填成一倍时的样子
|
| 171 |
+
const warm = sieve({ cake: V0 * 0.05, pop: V0 * 0.95, poop: 0 }, P.U0).top;
|
| 172 |
+
for (let i = 0; i < P.TOP_LIFE; i++) w.topPool.push(Object.assign({}, warm));
|
| 173 |
+
w.sat = satisfaction(w, { cake: V0 * 0.05, pop: V0 * 0.95, poop: 0 });
|
| 174 |
+
w.sat0 = w.sat;
|
| 175 |
+
w.rev0 = P.U0 * P.VIEWS_PER_USER * P.AD_PRICE;
|
| 176 |
+
return w;
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
function newCreator(w, type, age) {
|
| 180 |
+
// luck:真人是运气(收入 ×0.6~1.4);做便便的号是成本高低(成本 ×0.2~1.8,有的号几乎不花钱)。patience:耐心 ×0.5~1.5,大家不会同一天一起走
|
| 181 |
+
return { id: w.nextId++, type, age: age || 0, broke: 0, earned: 0, items: 0, luck: 0.6 + 0.8 * w.R(), costMul: 0.2 + 1.6 * w.R(), pat: 0.5 + w.R() };
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
function feedMix(w) {
|
| 185 |
+
const s = { cake: 0, pop: 0, poop: 0 };
|
| 186 |
+
for (const d of w.topPool) for (const t of TYPES) s[t] += d[t];
|
| 187 |
+
const tot = s.cake + s.pop + s.poop;
|
| 188 |
+
return tot > 0 ? { cake: s.cake / tot, pop: s.pop / tot, poop: s.poop / tot, total: tot } : { cake: 0, pop: 0, poop: 0, total: 0 };
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
function satisfaction(w, newCounts) {
|
| 192 |
+
const f = feedMix(w);
|
| 193 |
+
const nt = newCounts.cake + newCounts.pop + newCounts.poop;
|
| 194 |
+
let s = 0;
|
| 195 |
+
for (const t of TYPES) s += P.JOY[t] * ((1 - P.TRIAL_SHARE) * f[t] + P.TRIAL_SHARE * (nt > 0 ? newCounts[t] / nt : 0));
|
| 196 |
+
return s;
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
function step(w) {
|
| 200 |
+
const R = w.R;
|
| 201 |
+
w.day++;
|
| 202 |
+
// AI 到来:第 aiDay 天起,倍数在两周内爬到目标值
|
| 203 |
+
if (w.day >= w.aiDay) w.k = Math.min(w.kTarget, w.k * Math.pow(w.kTarget, 1 / 14));
|
| 204 |
+
if (w.kTarget < w.k) w.k = w.kTarget;
|
| 205 |
+
const k = w.k;
|
| 206 |
+
const humanRate = Math.pow(k, P.HUMAN_RATE_EXP), talent = Math.pow(k, P.TALENT_EXP);
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
// 1. 出东西
|
| 210 |
+
const made = { cake: 0, pop: 0, poop: 0 };
|
| 211 |
+
for (const c of w.creators) {
|
| 212 |
+
const r = c.type === "poop" ? P.RATE0.poop * Math.pow(k, P.BOT_RATE_EXP) : P.RATE0[c.type] * humanRate;
|
| 213 |
+
c.todayItems = r; made[c.type] += r;
|
| 214 |
+
}
|
| 215 |
+
// 2. 筛
|
| 216 |
+
const sv = sieve(made, w.users);
|
| 217 |
+
w.topPool.push(sv.top); if (w.topPool.length > P.TOP_LIFE) w.topPool.shift();
|
| 218 |
+
|
| 219 |
+
// 3. 看:推荐流里各类内容的观看次数
|
| 220 |
+
const views = w.users * P.VIEWS_PER_USER;
|
| 221 |
+
const feedViews = views * (1 - P.TRIAL_SHARE);
|
| 222 |
+
const trialViews = views * P.TRIAL_SHARE;
|
| 223 |
+
const fm = feedMix(w);
|
| 224 |
+
// 每条新内容平均拿到的观看:试看 + 进了最上层后在推荐流里分到的
|
| 225 |
+
const topItems = fm.total; // 推荐流里同时在架的条数
|
| 226 |
+
const viewsPerTopItemPerDay = topItems > 0 ? feedViews / topItems : 0;
|
| 227 |
+
const trialPerItem = {}, topProb = {};
|
| 228 |
+
for (const t of TYPES) {
|
| 229 |
+
trialPerItem[t] = made[t] > 0 ? sv.trialViews[t] / made[t] : 0;
|
| 230 |
+
topProb[t] = made[t] > 0 ? sv.top[t] / made[t] : 0;
|
| 231 |
+
}
|
| 232 |
+
// 4. 钱
|
| 233 |
+
const revenue = views * P.AD_PRICE;
|
| 234 |
+
const pool = revenue * P.CREATOR_SHARE;
|
| 235 |
+
const yuanPerView = pool / views;
|
| 236 |
+
const cost = P.COST_FIXED + P.COST_PER_ITEM * (made.cake + made.pop + made.poop);
|
| 237 |
+
const perItemIncome = {};
|
| 238 |
+
for (const t of TYPES) perItemIncome[t] = yuanPerView * (trialPerItem[t] + topProb[t] * viewsPerTopItemPerDay * P.TOP_LIFE);
|
| 239 |
+
// 预期:如果有人做一条便便,它能挣多少(还没有机器人时也算得出来)
|
| 240 |
+
const n1 = sv.tiers[0].n, n2 = sv.tiers[1].n, n3 = sv.tiers[2].n;
|
| 241 |
+
const poopTopProb = passRate(P.COMPLETION.poop, n1) * passRate(P.COMPLETION.poop, n2) * passRate(P.COMPLETION.poop, n3);
|
| 242 |
+
const poopIncomeProspect = made.poop > 0 ? perItemIncome.poop : yuanPerView * (Math.max(n1, 1) + poopTopProb * viewsPerTopItemPerDay * P.TOP_LIFE);
|
| 243 |
+
|
| 244 |
+
// 5. 创作者去留
|
| 245 |
+
const left = [], joined = [];
|
| 246 |
+
const keep = [];
|
| 247 |
+
for (const c of w.creators) {
|
| 248 |
+
c.age++;
|
| 249 |
+
const inc = perItemIncome[c.type] * c.todayItems * (c.type === "poop" ? 1 : c.luck);
|
| 250 |
+
c.earned = 0.8 * c.earned + 0.2 * inc;
|
| 251 |
+
const need = c.type === "poop" ? c.costMul * P.WAGE / Math.pow(k, P.BOT_COST_EXP) : P.WAGE;
|
| 252 |
+
if (inc < need) c.broke++; else c.broke = Math.max(0, c.broke - 2);
|
| 253 |
+
let go = c.type !== "poop" && c.broke >= P.PATIENCE[c.type] * c.pat;
|
| 254 |
+
if (c.type !== "poop" && R() < P.RETIRE) go = true;
|
| 255 |
+
if (go) left.push(c); else keep.push(c);
|
| 256 |
+
}
|
| 257 |
+
w.creators = keep;
|
| 258 |
+
// 新人
|
| 259 |
+
const add = (type, expected) => { let n = Math.floor(expected); if (R() < expected - n) n++; for (let i = 0; i < n; i++) { const c = newCreator(w, type, 0); w.creators.push(c); joined.push(c); } };
|
| 260 |
+
add("cake", P.ENTRY0.cake * talent);
|
| 261 |
+
add("pop", P.ENTRY0.pop);
|
| 262 |
+
// 做便便的:成本几乎为零、买彩票心态,人数主要取决于 AI 让批量生产变得多容易(同 模拟.py:新增的量是批量内容),
|
| 263 |
+
// 只温和地跟着钱走:平台收入越少,来的人越少,但不会走光。一部分是做爆米花的人"装上天线"转过来的
|
| 264 |
+
const nBots = w.creators.reduce((a, c) => a + (c.type === "poop"), 0);
|
| 265 |
+
const botTarget = P.POOP_PER_K * (k - 1) * Math.sqrt(Math.max(0.02, revenue / w.rev0));
|
| 266 |
+
w.converted = [];
|
| 267 |
+
const gap = botTarget - nBots;
|
| 268 |
+
if (gap > 0) {
|
| 269 |
+
let n = Math.floor(gap * P.POOP_ARRIVE); if (R() < gap * P.POOP_ARRIVE - n) n++;
|
| 270 |
+
const pops = w.creators.filter(c => c.type === "pop"); let convLeft = Math.floor(pops.length * 0.02);
|
| 271 |
+
for (let i = 0; i < n; i++) {
|
| 272 |
+
if (convLeft > 0 && R() < P.CONVERT) { const c = pops[Math.floor(R() * pops.length)]; if (c.type === "pop") { c.type = "poop"; w.converted.push(c); convLeft--; continue; } }
|
| 273 |
+
const c = newCreator(w, "poop", 0); w.creators.push(c); joined.push(c);
|
| 274 |
+
}
|
| 275 |
+
} else if (gap < 0) {
|
| 276 |
+
let n = Math.floor(-gap * P.POOP_LEAVE); if (R() < -gap * P.POOP_LEAVE - n) n++;
|
| 277 |
+
if (n > 0) { const out = new Set(); const bots = w.creators.filter(c => c.type === "poop");
|
| 278 |
+
while (out.size < Math.min(n, bots.length)) out.add(bots[Math.floor(R() * bots.length)]);
|
| 279 |
+
w.creators = w.creators.filter(c => !out.has(c)); for (const c of out) left.push(c); }
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
// 6. 用户去留
|
| 283 |
+
const sToday = satisfaction(w, made);
|
| 284 |
+
w.sat = (1 - P.USER_MEMORY) * w.sat + P.USER_MEMORY * sToday;
|
| 285 |
+
// 用户忍耐度不一:满意度每差 1,留下的人按比例少一截,越往后剩下的越是不挑的人,走得越慢
|
| 286 |
+
const target = P.U0 * Math.exp(P.USER_SENS * (w.sat - w.sat0));
|
| 287 |
+
const before = w.users;
|
| 288 |
+
w.users = w.users + P.USER_REACT * (target - w.users);
|
| 289 |
+
|
| 290 |
+
const count = { cake: 0, pop: 0, poop: 0 };
|
| 291 |
+
for (const c of w.creators) count[c.type]++;
|
| 292 |
+
w.today = {
|
| 293 |
+
day: w.day, k, made, sv, fm, revenue, cost, pool, users: w.users, sat: w.sat,
|
| 294 |
+
count, perItemIncome, botTarget, poopIncomeProspect, converted: w.converted,
|
| 295 |
+
cakeShareTop: fm.cake, left, joined, usersDelta: w.users - before,
|
| 296 |
+
};
|
| 297 |
+
w.log.push({ day: w.day, k, users: w.users, revenue, cost, cakeTop: fm.cake, poopTop: fm.poop, nCake: count.cake, nPop: count.pop, nBot: count.poop, items: made.cake + made.pop + made.poop, n1 });
|
| 298 |
+
return w.today;
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
const API = { P, makeWorld, step, sieve, passRate, V0, TYPES };
|
| 302 |
+
if (typeof module !== "undefined" && module.exports) module.exports = API; else root.PoolSim = API;
|
| 303 |
+
})(typeof window !== "undefined" ? window : globalThis);
|
| 304 |
+
|
| 305 |
+
</script>
|
| 306 |
+
<script>
|
| 307 |
+
(function(){
|
| 308 |
+
"use strict";
|
| 309 |
+
const S = window.PoolSim, P = S.P;
|
| 310 |
+
const cv = document.getElementById("c"), ctx = cv.getContext("2d");
|
| 311 |
+
const W = 1600, H = 900;
|
| 312 |
+
const DPR = Math.min(2, window.devicePixelRatio || 1); cv.width = W * DPR; cv.height = H * DPR;
|
| 313 |
+
const FONT = getComputedStyle(document.documentElement).getPropertyValue("--font");
|
| 314 |
+
const C = { paper:"#fbfbf7", grid:"#dfe7ee", ink:"#262a33", soft:"#6b7280", cake:"#e0484f", cream:"#fff6f2", pink:"#f4a6b0", pop:"#f2c14e", popHi:"#fff1c4", poop:"#8a5a3b", poopHi:"#a87452", coin:"#d9a520", coinHi:"#f7d774", accent:"#2f5fb3", bad:"#c2410c", good:"#2f8f5b", leaf:"#3f9a4a", strip:"#ffffffee" };
|
| 315 |
+
const REDUCED = matchMedia("(prefers-reduced-motion: reduce)").matches;
|
| 316 |
+
|
| 317 |
+
// ---------- 文字 ----------
|
| 318 |
+
const T = {
|
| 319 |
+
zh:{ title:"推荐池", sub:"免费看 · 广告养 · 分成活", ai:"AI 产能", day:"第 {d} 天",
|
| 320 |
+
m:["用户","广告收入 / 天","平台成本 / 天","推荐池里的蛋糕"], unitYuan:"元",
|
| 321 |
+
stage:"创作者", cake:"做蛋糕的", pop:"做爆米花的", bot:"做便便的", stageNote:"台上小人按比例代表全部创作者(做便便的按账号算)",
|
| 322 |
+
tier:["第一层","第二层","第三层"], perItem:"每条 {n} 人试看", rejected:"没被推荐", feed:"推荐流", plaza:"用户",
|
| 323 |
+
plazaNote:"一个点 = 50 位用户", adv:"广告商", vault:"平台", keep:"净赚 {v} 元/天", lose:"亏 {v} 元/天",
|
| 324 |
+
server:"存储·分发·审核", toCreators:"分给创作者 {v} 元/天", adIn:"广告费 {v} 元/天", attention:"用户的时间,卖给广告商",
|
| 325 |
+
play:"开始", pause:"暂停", resume:"继续", reset:"重来", kLab:"AI 产能倍数", spLab:"速度", slow:"慢", normal:"正常", fast:"快", lang:"English",
|
| 326 |
+
note:"示意模型。筛选部分(三层试看、完播率、晋级线)沿用论文图 1 的参数;用户去留、分成、成本、创作者去留等循环部分的规则与参数未经实证校准,只用来展示方向。",
|
| 327 |
+
cap:{ intro:"一年前,AI 还没来。好内容被一层层试看挑出来,用户吃得开心。", ready:"拧好 AI 产能倍数,按「开始」。",
|
| 328 |
+
ai:"AI 来了:每个人的产能 ×{k}", bot:"装天线的来了:批量做,划算了。", flood:"推荐池里混进了大量便便。",
|
| 329 |
+
users:"用户开始走了,广告费跟着少了。", chefs:"做蛋糕的人挣不到钱,背上包走了。", botsGone:"钱少了,做便便的退了一批,剩下的赖着不走。",
|
| 330 |
+
popFlood:"爆米花挤满了推荐流,好内容被淹没。", calm:"产能涨了,推荐池还挑得出来。",
|
| 331 |
+
half:"上传的东西一半是便便,筛子还挡得住。", lottery:"每条只剩一两个人试看,筛子成了抽签。" },
|
| 332 |
+
endFail:"内容多了 {k} 倍,推荐池挑不出好内容了", endOk:"{k} 倍时,推荐池还挑得出好内容", endHead:"第 365 天", from:"一年前", to:"现在", again:"按「重来」再看一遍,或者换个倍数。" },
|
| 333 |
+
en:{ title:"The Recommendation Pool", sub:"Free to watch · ad-funded · revenue-shared", ai:"AI output", day:"Day {d}",
|
| 334 |
+
m:["Users","Ad revenue / day","Platform cost / day","Cake in the pool"], unitYuan:"¥",
|
| 335 |
+
stage:"Creators", cake:"Cake", pop:"Popcorn", bot:"Poop", stageNote:"Figures show the creator mix in proportion (poop makers counted by account)",
|
| 336 |
+
tier:["Tier 1","Tier 2","Tier 3"], perItem:"{n} trial viewers each", rejected:"Not recommended", feed:"Feed", plaza:"Users",
|
| 337 |
+
plazaNote:"one dot = 50 users", adv:"Advertisers", vault:"Platform", keep:"Nets ¥{v}/day", lose:"Loses ¥{v}/day",
|
| 338 |
+
server:"Storage · delivery · review", toCreators:"To creators ¥{v}/day", adIn:"Ad spend ¥{v}/day", attention:"Users' time, sold to advertisers",
|
| 339 |
+
play:"Start", pause:"Pause", resume:"Resume", reset:"Restart", kLab:"AI output multiple", spLab:"Speed", slow:"Slow", normal:"Normal", fast:"Fast", lang:"中文",
|
| 340 |
+
note:"Illustrative model. The selection part (three trial tiers, completion rates, cut-off) uses the parameters of Figure 1 in the paper; the loop part (users, revenue share, cost, creators coming and going) uses rules and parameters that are not empirically calibrated and only show the direction.",
|
| 341 |
+
cap:{ intro:"A year ago, before AI. Good content is picked out tier by tier, and users are happy.", ready:"Set the AI output multiple, then press Start.",
|
| 342 |
+
ai:"AI arrives: everyone's output ×{k}", bot:"Antennas show up: batch production now pays.", flood:"The pool is filling up with poop.",
|
| 343 |
+
users:"Users start leaving, and ad money leaves with them.", chefs:"Cake makers can't earn a living and pack up.", botsGone:"The money dries up. Some poop makers quit; the rest stay put.",
|
| 344 |
+
popFlood:"Popcorn crowds the feed and good content gets buried.", calm:"Output is up, and the pool still picks well.",
|
| 345 |
+
half:"Half of all uploads are poop. The sieve still holds.", lottery:"One or two trial viewers per item: the sieve has become a lottery." },
|
| 346 |
+
endFail:"With {k}× the content, the pool can no longer pick out good content", endOk:"At {k}×, the pool still picks out good content", endHead:"Day 365", from:"A year ago", to:"Now", again:"Press Restart to watch again, or try another multiple." },
|
| 347 |
+
};
|
| 348 |
+
let lang = "zh";
|
| 349 |
+
const tr = (key, vars) => { let s = key.split(".").reduce((o, k) => o[k], T[lang]); if (vars) for (const k in vars) s = s.replace("{" + k + "}", vars[k]); return s; };
|
| 350 |
+
const fmtInt = v => Math.round(v).toLocaleString(lang === "zh" ? "zh-CN" : "en-US");
|
| 351 |
+
const fmtK = k => k >= 10 ? Math.round(k).toString() : (Math.round(k * 10) / 10).toString();
|
| 352 |
+
|
| 353 |
+
// ---------- 手绘工具 ----------
|
| 354 |
+
function hash(n){ n = (n ^ 61) ^ (n >>> 16); n = n + (n << 3); n = n ^ (n >>> 4); n = Math.imul(n, 0x27d4eb2d); n = n ^ (n >>> 15); return (n >>> 0) / 4294967296; }
|
| 355 |
+
function jit(seed, a){ return (hash(seed) - 0.5) * 2 * a; }
|
| 356 |
+
function line(x1,y1,x2,y2,seed,w,col,a){ a = a == null ? 1.2 : a; ctx.strokeStyle = col || C.ink; ctx.lineWidth = w || 2; ctx.beginPath(); ctx.moveTo(x1 + jit(seed,a), y1 + jit(seed+1,a)); const mx = (x1+x2)/2 + jit(seed+2,a*1.5), my = (y1+y2)/2 + jit(seed+3,a*1.5); ctx.quadraticCurveTo(mx, my, x2 + jit(seed+4,a), y2 + jit(seed+5,a)); ctx.stroke(); }
|
| 357 |
+
function rrect(x,y,w,h,seed,fill,stroke,lw){ ctx.beginPath(); const a = 1.4; ctx.moveTo(x + jit(seed,a), y + jit(seed+1,a)); ctx.lineTo(x + w + jit(seed+2,a), y + jit(seed+3,a)); ctx.lineTo(x + w + jit(seed+4,a), y + h + jit(seed+5,a)); ctx.lineTo(x + jit(seed+6,a), y + h + jit(seed+7,a)); ctx.closePath(); if (fill){ ctx.fillStyle = fill; ctx.fill(); } if (stroke !== false){ ctx.strokeStyle = stroke || C.ink; ctx.lineWidth = lw || 2; ctx.stroke(); } }
|
| 358 |
+
function circ(x,y,r,fill,stroke,lw){ ctx.beginPath(); ctx.arc(x,y,r,0,Math.PI*2); if (fill){ ctx.fillStyle = fill; ctx.fill(); } if (stroke !== false){ ctx.strokeStyle = stroke || C.ink; ctx.lineWidth = lw || 2; ctx.stroke(); } }
|
| 359 |
+
function text(s,x,y,size,col,align,weight,base){ ctx.font = (weight||400) + " " + size + "px " + FONT; ctx.fillStyle = col || C.ink; ctx.textAlign = align || "left"; ctx.textBaseline = base || "alphabetic"; ctx.fillText(s,x,y); }
|
| 360 |
+
function textW(s,size,weight){ ctx.font = (weight||400) + " " + size + "px " + FONT; return ctx.measureText(s).width; }
|
| 361 |
+
|
| 362 |
+
// ---------- 三种东西 ----------
|
| 363 |
+
function drawItem(type,x,y,s,alpha){
|
| 364 |
+
ctx.save(); ctx.globalAlpha = alpha == null ? 1 : alpha; ctx.translate(x,y);
|
| 365 |
+
const lw = Math.max(1, s * 0.09);
|
| 366 |
+
if (type === "cake"){
|
| 367 |
+
ctx.beginPath(); ctx.moveTo(-s*0.5, s*0.32); ctx.lineTo(s*0.5, s*0.32); ctx.lineTo(s*0.42,-s*0.12); ctx.lineTo(-s*0.42,-s*0.12); ctx.closePath();
|
| 368 |
+
ctx.fillStyle = C.cream; ctx.fill(); ctx.strokeStyle = C.ink; ctx.lineWidth = lw; ctx.stroke();
|
| 369 |
+
ctx.fillStyle = C.pink; ctx.fillRect(-s*0.46, s*0.06, s*0.92, s*0.09);
|
| 370 |
+
circ(0, -s*0.26, s*0.2, C.cake, C.ink, lw);
|
| 371 |
+
ctx.fillStyle = C.leaf; ctx.beginPath(); ctx.ellipse(0, -s*0.47, s*0.12, s*0.05, 0, 0, Math.PI*2); ctx.fill();
|
| 372 |
+
} else if (type === "pop"){
|
| 373 |
+
const pts = [[-0.22,0.12],[0.2,0.14],[0,-0.1],[-0.25,-0.18],[0.24,-0.16],[0.02,0.3]];
|
| 374 |
+
for (const [px,py] of pts){ circ(px*s, py*s, s*0.2, C.pop, C.ink, lw); }
|
| 375 |
+
circ(-s*0.05, -s*0.14, s*0.07, C.popHi, false);
|
| 376 |
+
} else {
|
| 377 |
+
ctx.fillStyle = C.poop; ctx.strokeStyle = C.ink; ctx.lineWidth = lw;
|
| 378 |
+
ctx.beginPath(); ctx.ellipse(0, s*0.22, s*0.46, s*0.16, 0, 0, Math.PI*2); ctx.fill(); ctx.stroke();
|
| 379 |
+
ctx.beginPath(); ctx.ellipse(0, s*0.02, s*0.34, s*0.14, 0, 0, Math.PI*2); ctx.fill(); ctx.stroke();
|
| 380 |
+
ctx.beginPath(); ctx.ellipse(0, -s*0.16, s*0.22, s*0.12, 0, 0, Math.PI*2); ctx.fill(); ctx.stroke();
|
| 381 |
+
ctx.beginPath(); ctx.moveTo(-s*0.04,-s*0.27); ctx.quadraticCurveTo(s*0.08,-s*0.42, s*0.14,-s*0.46); ctx.lineTo(s*0.02,-s*0.27); ctx.closePath(); ctx.fill(); ctx.stroke();
|
| 382 |
+
if (s >= 18){ circ(-s*0.13, s*0.0, s*0.08, "#fff", C.ink, lw*0.7); circ(s*0.13, s*0.0, s*0.08, "#fff", C.ink, lw*0.7); circ(-s*0.12, s*0.01, s*0.035, C.ink, false); circ(s*0.14, s*0.01, s*0.035, C.ink, false);
|
| 383 |
+
ctx.beginPath(); ctx.arc(0, s*0.14, s*0.08, 0.15*Math.PI, 0.85*Math.PI); ctx.lineWidth = lw*0.7; ctx.stroke(); }
|
| 384 |
+
}
|
| 385 |
+
ctx.restore();
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
// ---------- 小人 ----------
|
| 389 |
+
function drawPerson(x,y,type,opt){
|
| 390 |
+
opt = opt || {}; const seed = opt.seed || 1, mood = opt.mood == null ? 1 : opt.mood;
|
| 391 |
+
ctx.save(); ctx.translate(x,y); if (opt.flip) ctx.scale(-1,1); ctx.globalAlpha = opt.alpha == null ? 1 : opt.alpha;
|
| 392 |
+
const lw = 2.2;
|
| 393 |
+
const walk = opt.walk || 0;
|
| 394 |
+
// 腿
|
| 395 |
+
line(0,-6, -7 + Math.sin(walk)*6, 14, seed, lw); line(0,-6, 7 - Math.sin(walk)*6, 14, seed+9, lw);
|
| 396 |
+
// 身子
|
| 397 |
+
line(0,-30, 0,-6, seed+20, lw);
|
| 398 |
+
// 背包
|
| 399 |
+
if (opt.pack){ rrect(-16,-30,11,18,seed+40,"#c9b48a",C.ink,1.8); }
|
| 400 |
+
// 左手:摊开
|
| 401 |
+
line(0,-24, -16,-16, seed+30, lw); ctx.beginPath(); ctx.arc(-19,-17,4,Math.PI*0.6,Math.PI*1.7); ctx.strokeStyle = C.ink; ctx.lineWidth = 1.8; ctx.stroke();
|
| 402 |
+
// 右手:往前推
|
| 403 |
+
const push = opt.push == null ? 0 : opt.push, blur = opt.blur || 0;
|
| 404 |
+
if (blur > 0){ for (let i = 0; i < 4; i++){ const p = i/3; ctx.globalAlpha = (opt.alpha==null?1:opt.alpha) * 0.25; line(0,-24, 6 + 16*p, -24 + 6*(1-p), seed+50+i, lw); } ctx.globalAlpha = opt.alpha == null ? 1 : opt.alpha;
|
| 405 |
+
ctx.strokeStyle = C.soft; ctx.lineWidth = 1.2; for (let i = 0; i < 3; i++){ ctx.beginPath(); ctx.moveTo(26, -26 + i*5); ctx.lineTo(34, -26 + i*5); ctx.stroke(); } }
|
| 406 |
+
else { line(0,-24, 8 + 14*push, -22 + 4*push, seed+60, lw); }
|
| 407 |
+
// 头
|
| 408 |
+
const headFill = type === "poop" ? "#eef1f6" : "#ffffff";
|
| 409 |
+
circ(0,-44, 14, headFill, C.ink, lw);
|
| 410 |
+
if (type === "poop"){ ctx.fillStyle = C.ink; ctx.fillRect(-7,-48,4,4); ctx.fillRect(3,-48,4,4); line(-5,-38,5,-38,seed+70,1.6,C.ink,0.3); }
|
| 411 |
+
else { circ(-5,-46,1.7,C.ink,false); circ(5,-46,1.7,C.ink,false); ctx.beginPath(); ctx.strokeStyle = C.ink; ctx.lineWidth = 1.6;
|
| 412 |
+
if (mood > 0.5) ctx.arc(0,-41,5,0.15*Math.PI,0.85*Math.PI); else if (mood > -0.2){ ctx.moveTo(-4,-38); ctx.lineTo(4,-38); } else ctx.arc(0,-34,5,1.15*Math.PI,1.85*Math.PI); ctx.stroke(); }
|
| 413 |
+
// 头顶
|
| 414 |
+
if (type === "cake"){ ctx.beginPath(); ctx.fillStyle = "#fff"; ctx.strokeStyle = C.ink; ctx.lineWidth = 1.8; ctx.rect(-10,-64,20,7); ctx.fill(); ctx.stroke();
|
| 415 |
+
circ(-7,-69,6,"#fff",C.ink,1.8); circ(0,-72,7,"#fff",C.ink,1.8); circ(7,-69,6,"#fff",C.ink,1.8); ctx.fillStyle = "#fff"; ctx.fillRect(-9,-67,18,5); }
|
| 416 |
+
if (type === "poop"){ line(0,-58,2,-72,seed+80,1.8); const blink = opt.blink ? C.cake : "#f08a8f"; circ(2,-75,3.5,blink,C.ink,1.4); }
|
| 417 |
+
if (opt.spark){ ctx.strokeStyle = C.coin; ctx.lineWidth = 2; for (let i = 0; i < 6; i++){ const a = i/6*Math.PI*2; ctx.beginPath(); ctx.moveTo(2+Math.cos(a)*8,-75+Math.sin(a)*8); ctx.lineTo(2+Math.cos(a)*14,-75+Math.sin(a)*14); ctx.stroke(); } }
|
| 418 |
+
ctx.restore();
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
// ---------- 版面坐标 ----------
|
| 422 |
+
const L = {
|
| 423 |
+
stage:{ x:28, y:150, w:450, h:500 },
|
| 424 |
+
slots:[], // 填在下面
|
| 425 |
+
sieve:[ {x:730,y:225}, {x:730,y:385}, {x:730,y:545} ], bowlW:240,
|
| 426 |
+
bin:{ x:730, y:640 },
|
| 427 |
+
feed:{ x1:1060, x2:1572, y:598 },
|
| 428 |
+
plaza:{ x:1040, y:150, w:540, h:420 },
|
| 429 |
+
adv:{ x:1500, y:810 }, vault:{ x:830, y:800 }, server:{ x:1050, y:820 },
|
| 430 |
+
};
|
| 431 |
+
const COLS = 6, ROWS = 5;
|
| 432 |
+
for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++) L.slots.push({ x: 72 + c*66 + (r%2)*14, y: 262 + r*76 });
|
| 433 |
+
const NSLOT = L.slots.length;
|
| 434 |
+
|
| 435 |
+
// ---------- 状态 ----------
|
| 436 |
+
let world, today, kSel = 100, running = false, started = false, ended = false, speed = 1, dayAcc = 0;
|
| 437 |
+
const DAY_SEC = 0.16;
|
| 438 |
+
let slots = [], walkers = [], items = [], pops = [], coins = [], dots = [], reacts = [], hist = [], belt = [];
|
| 439 |
+
let caps = [], capShown = null, capT = 0, flags = {}, peakBots = 0, startStats = null, endStats = null;
|
| 440 |
+
|
| 441 |
+
function reset(){
|
| 442 |
+
world = S.makeWorld({ k: kSel, seed: 7 }); world.kTarget = kSel;
|
| 443 |
+
today = probeToday();
|
| 444 |
+
running = false; started = false; ended = false; dayAcc = 0;
|
| 445 |
+
slots = []; walkers = []; items = []; pops = []; coins = []; dots = []; reacts = []; hist = []; belt = [];
|
| 446 |
+
caps = []; capShown = { key:"intro" }; capT = 0; flags = {}; peakBots = 0; endStats = null;
|
| 447 |
+
layoutSlots(true); layoutDots(true);
|
| 448 |
+
startStats = { users: P.U0, revenue: today.revenue, cost: today.cost, cake: today.cakeShareTop };
|
| 449 |
+
pushHist(today);
|
| 450 |
+
setPlayLabel();
|
| 451 |
+
}
|
| 452 |
+
// 一倍时的"今天":复制世界跑一步拿数,不推进真正的世界
|
| 453 |
+
function probeToday(){
|
| 454 |
+
const w2 = S.makeWorld({ k: 1, seed: 7 }); w2.kTarget = 1; return S.step(w2);
|
| 455 |
+
}
|
| 456 |
+
function pushHist(t){ hist.push({ users:t.users, revenue:t.revenue, cost:t.cost, cake:t.cakeShareTop }); }
|
| 457 |
+
|
| 458 |
+
// 台上的小人:按人数比例分 30 个位子,有人就至少 1 个位子
|
| 459 |
+
function targetSlots(count){
|
| 460 |
+
const tot = count.cake + count.pop + count.poop || 1; const types = ["cake","pop","poop"]; const out = {}; let used = 0; const rem = [];
|
| 461 |
+
for (const t of types){ const ex = count[t] / tot * NSLOT; out[t] = Math.floor(ex); if (count[t] > 0 && out[t] === 0) out[t] = 1; used += out[t]; rem.push([ex - Math.floor(ex), t]); }
|
| 462 |
+
rem.sort((a,b) => b[0]-a[0]); let i = 0;
|
| 463 |
+
while (used < NSLOT){ out[rem[i % 3][1]]++; used++; i++; }
|
| 464 |
+
while (used > NSLOT){ const t = types.slice().sort((a,b) => out[b]-out[a])[0]; out[t]--; used--; }
|
| 465 |
+
return out;
|
| 466 |
+
}
|
| 467 |
+
function layoutSlots(instant, converted){
|
| 468 |
+
const count = today ? today.count : world.creators.reduce((o,c) => (o[c.type]++, o), {cake:0,pop:0,poop:0});
|
| 469 |
+
const tgt = targetSlots(count);
|
| 470 |
+
if (instant || slots.length === 0){
|
| 471 |
+
const order = []; for (const t of ["cake","pop","poop"]) for (let i = 0; i < tgt[t]; i++) order.push(t);
|
| 472 |
+
// 打散,让三种人混站
|
| 473 |
+
for (let i = order.length - 1; i > 0; i--){ const j = Math.floor(hash(i*7+3) * (i+1)); [order[i],order[j]] = [order[j],order[i]]; }
|
| 474 |
+
slots = order.map((t,i) => ({ type:t, seed:i*31+5, phase:hash(i*13), pop:1, born:-9 }));
|
| 475 |
+
return;
|
| 476 |
+
}
|
| 477 |
+
const have = { cake:0, pop:0, poop:0 }; for (const s of slots) have[s.type]++;
|
| 478 |
+
let conv = converted || 0;
|
| 479 |
+
// 先让做爆米花的就地装天线
|
| 480 |
+
while (conv > 0 && have.poop < tgt.poop && have.pop > tgt.pop){
|
| 481 |
+
const cand = slots.filter(s => s.type === "pop"); if (!cand.length) break; const s = cand[Math.floor(Math.random()*cand.length)];
|
| 482 |
+
s.type = "poop"; s.spark = 1.2; have.pop--; have.poop++; conv--;
|
| 483 |
+
}
|
| 484 |
+
for (const t of ["cake","pop","poop"]){
|
| 485 |
+
while (have[t] > tgt[t]){
|
| 486 |
+
const cand = slots.map((s,i) => [s,i]).filter(([s]) => s.type === t && !s.leaving); if (!cand.length) break;
|
| 487 |
+
const [s,i] = cand[Math.floor(Math.random()*cand.length)];
|
| 488 |
+
walkers.push({ type:t, x:L.slots[i].x, y:L.slots[i].y, seed:s.seed, t:0 });
|
| 489 |
+
s.type = null; have[t]--;
|
| 490 |
+
}
|
| 491 |
+
}
|
| 492 |
+
const empty = slots.filter(s => s.type === null);
|
| 493 |
+
const need = []; for (const t of ["cake","pop","poop"]) for (let i = have[t]; i < tgt[t]; i++) need.push(t);
|
| 494 |
+
for (let i = 0; i < empty.length; i++){ const t = need[i] || "pop"; empty[i].type = t; empty[i].pop = 0; empty[i].born = 0; }
|
| 495 |
+
}
|
| 496 |
+
|
| 497 |
+
// 用户的点
|
| 498 |
+
function layoutDots(instant){
|
| 499 |
+
const n = Math.max(0, Math.round((today ? today.users : P.U0) / 50));
|
| 500 |
+
const cols = 20, cw = 26, rh = 38;
|
| 501 |
+
while (dots.filter(d => !d.leaving).length > n){
|
| 502 |
+
const live = dots.filter(d => !d.leaving); const d = live[Math.floor(Math.random()*live.length)]; d.leaving = true;
|
| 503 |
+
}
|
| 504 |
+
let live = dots.filter(d => !d.leaving).length;
|
| 505 |
+
while (live < n){
|
| 506 |
+
// 找空位
|
| 507 |
+
const used = new Set(dots.filter(d => !d.leaving).map(d => d.slot)); let slot = 0; while (used.has(slot)) slot++;
|
| 508 |
+
const cx = L.plaza.x + 22 + (slot % cols) * cw + (Math.floor(slot/cols)%2) * 9, cy = L.plaza.y + 56 + Math.floor(slot/cols) * rh;
|
| 509 |
+
dots.push({ slot, hx:cx, hy:cy, x: instant ? cx : W + 30 + Math.random()*120, y: instant ? cy : cy, off: Math.random()*1.2 - 0.6, seed: slot*17+3 });
|
| 510 |
+
live++;
|
| 511 |
+
}
|
| 512 |
+
}
|
| 513 |
+
|
| 514 |
+
// ---------- 一天 ----------
|
| 515 |
+
function advanceDay(){
|
| 516 |
+
if (world.day >= 365){ finish(); return; }
|
| 517 |
+
today = S.step(world);
|
| 518 |
+
pushHist(today);
|
| 519 |
+
layoutSlots(false, (today.converted || []).length);
|
| 520 |
+
layoutDots(false);
|
| 521 |
+
events();
|
| 522 |
+
if (world.day >= 365) finish();
|
| 523 |
+
}
|
| 524 |
+
function queue(key, vars){ if (flags[key]) return; flags[key] = true; caps = [{ key, vars }]; } // 只留最新的一条
|
| 525 |
+
function events(){
|
| 526 |
+
const t = today, d = world.day;
|
| 527 |
+
peakBots = Math.max(peakBots, t.count.poop);
|
| 528 |
+
if (d === world.aiDay) queue("ai", { k: fmtK(kSel) });
|
| 529 |
+
if (t.count.poop > 0) queue("bot");
|
| 530 |
+
{ const m = t.made, up = m.poop / (m.cake + m.pop + m.poop || 1); if (up > 0.4 && t.fm.poop < 0.1) queue("half"); }
|
| 531 |
+
if (t.sv.tiers[0].n < 2 && d > world.aiDay) queue("lottery");
|
| 532 |
+
if (t.fm.poop > 0.25) queue("flood");
|
| 533 |
+
if (t.users < 0.85 * P.U0) queue("users");
|
| 534 |
+
if (t.count.cake < 30 && d > world.aiDay) queue("chefs");
|
| 535 |
+
if (peakBots >= 10 && t.count.poop < peakBots * 0.3) queue("botsGone");
|
| 536 |
+
if (t.fm.pop > 0.5) queue("popFlood");
|
| 537 |
+
if (d === world.aiDay + 80 && t.users > 0.9 * P.U0 && t.cakeShareTop > 0.8) queue("calm");
|
| 538 |
+
}
|
| 539 |
+
function finish(){
|
| 540 |
+
running = false; ended = true; setPlayLabel();
|
| 541 |
+
endStats = { users: today.users, revenue: today.revenue, cost: today.cost, cake: today.cakeShareTop };
|
| 542 |
+
}
|
| 543 |
+
|
| 544 |
+
// ---------- 动画 ----------
|
| 545 |
+
function stageTarget(){ return { x: L.sieve[0].x - 40 + Math.random()*80, y: L.sieve[0].y - 22 }; }
|
| 546 |
+
function tierLoad(i){ const n = today ? today.sv.tiers[i].n : P.TIERS[i]; return P.TIERS[i] / Math.max(n, 0.5); }
|
| 547 |
+
function emitFrom(i){
|
| 548 |
+
const s = slots[i]; if (!s.type) return;
|
| 549 |
+
const p = L.slots[i]; const tg = stageTarget();
|
| 550 |
+
if (items.length > 520) return;
|
| 551 |
+
items.push({ type:s.type, st:"fly", x:p.x+24, y:p.y-22, sx:p.x+24, sy:p.y-22, tx:tg.x, ty:tg.y, t:0, dur:0.9 + Math.random()*0.35, tier:0, rot:Math.random()*6 });
|
| 552 |
+
}
|
| 553 |
+
function personRate(type){ const k = today ? today.k : 1; return type === "poop" ? P.RATE0.poop * Math.pow(k, P.BOT_RATE_EXP) : P.RATE0[type] * Math.pow(k, P.HUMAN_RATE_EXP); }
|
| 554 |
+
const VIS = 0.43; // 每天 1 条 ≈ 每秒 0.43 次推
|
| 555 |
+
function update(dt){
|
| 556 |
+
const simDt = dt * speed;
|
| 557 |
+
if (running && !ended){ dayAcc += simDt; while (dayAcc >= DAY_SEC){ dayAcc -= DAY_SEC; advanceDay(); if (ended) break; } }
|
| 558 |
+
// 推
|
| 559 |
+
for (let i = 0; i < slots.length; i++){
|
| 560 |
+
const s = slots[i]; if (!s.type) continue;
|
| 561 |
+
s.pop = Math.min(1, s.pop + dt*2.5); if (s.spark) s.spark = Math.max(0, s.spark - dt);
|
| 562 |
+
const f = personRate(s.type) * VIS; s.f = f;
|
| 563 |
+
s.phase += dt * f;
|
| 564 |
+
while (s.phase >= 1){ s.phase -= 1; if (Math.random() < 1) emitFrom(i); }
|
| 565 |
+
}
|
| 566 |
+
// 东西
|
| 567 |
+
pileCount = [0,0,0]; for (const it of items) if (it.st === "pile") pileCount[it.tier]++;
|
| 568 |
+
const fall = [];
|
| 569 |
+
for (const it of items){
|
| 570 |
+
it.t += dt;
|
| 571 |
+
if (it.st === "fly"){ const u = Math.min(1, it.t/it.dur); it.x = it.sx + (it.tx-it.sx)*u; it.y = it.sy + (it.ty-it.sy)*u - Math.sin(u*Math.PI)*45; if (u >= 1){ it.st = "pile"; it.t = 0; it.dwell = Math.min(4, 0.45 + 0.4*Math.log2(Math.max(1, tierLoad(0)))) * (0.7 + Math.random()*0.6); placeOnPile(it, 0); } }
|
| 572 |
+
else if (it.st === "pile"){ if (it.t >= it.dwell){ judge(it); } }
|
| 573 |
+
else if (it.st === "drop"){ it.vy += 900*dt; it.y += it.vy*dt; it.x += it.vx*dt;
|
| 574 |
+
if (it.next != null && it.y >= L.sieve[it.next].y - 18){ const tier = it.next; it.st = "pile"; it.t = 0; it.tier = tier; it.dwell = Math.min(4, 0.45 + 0.4*Math.log2(Math.max(1, tierLoad(tier)))) * (0.7 + Math.random()*0.6); placeOnPile(it, tier); }
|
| 575 |
+
else if (it.next == null && it.y >= L.bin.y - 6){ it.st = "dead"; binCount[it.type] = (binCount[it.type]||0) + 1; } }
|
| 576 |
+
else if (it.st === "chute"){ const u = Math.min(1, it.t/0.9); it.x = it.sx + (L.feed.x1 - it.sx)*u; it.y = it.sy + (L.feed.y - it.sy)*u; if (u >= 1){ it.st = "dead"; } }
|
| 577 |
+
else if (it.st === "feed"){ it.x += dt * 70; if (it.x > L.feed.x2){ it.st = "dead"; } }
|
| 578 |
+
}
|
| 579 |
+
items = items.filter(it => it.st !== "dead");
|
| 580 |
+
// 堆:每层最多显示 140 个,多的从底下消掉(仍按规则判)
|
| 581 |
+
// 推荐流里太挤就去掉旧的
|
| 582 |
+
// 推荐流传送带:上面每样东西的比例 = 当天推荐池里的真实比例
|
| 583 |
+
for (const b of belt) b.x += dt * 150 * Math.max(1, speed);
|
| 584 |
+
belt = belt.filter(b => b.x < L.feed.x2 - 6);
|
| 585 |
+
if (!belt.length || belt[belt.length-1].x > L.feed.x1 + 30){
|
| 586 |
+
if (!belt.length){ for (let x = L.feed.x2 - 20; x > L.feed.x1; x -= 30) belt.unshift({ x, type: sampleFeed() }); }
|
| 587 |
+
belt.push({ x: L.feed.x1 + 4, type: sampleFeed() });
|
| 588 |
+
}
|
| 589 |
+
// 走的人
|
| 590 |
+
for (const w of walkers){ w.t += dt; w.x -= dt*80; }
|
| 591 |
+
walkers = walkers.filter(w => w.x > -60);
|
| 592 |
+
// 用户
|
| 593 |
+
for (const d of dots){
|
| 594 |
+
if (d.leaving){ d.x += dt * 110; continue; }
|
| 595 |
+
d.x += (d.hx - d.x) * Math.min(1, dt*2.2); d.y += (d.hy - d.y) * Math.min(1, dt*2.2);
|
| 596 |
+
}
|
| 597 |
+
dots = dots.filter(d => d.x < W + 40);
|
| 598 |
+
// 反应
|
| 599 |
+
if (today){ const live = dots.filter(d => !d.leaving); const rate = live.length * 0.06 * dt * (REDUCED ? 0.3 : 1);
|
| 600 |
+
let n = Math.floor(rate); if (Math.random() < rate - n) n++;
|
| 601 |
+
for (let i = 0; i < n && live.length; i++){ const d = live[Math.floor(Math.random()*live.length)]; const r = Math.random(); const m = mixSeen();
|
| 602 |
+
const kind = r < m.cake ? "heart" : r < m.cake + m.poop ? "cloud" : null; if (kind) reacts.push({ kind, x:d.x, y:d.y-14, t:0 }); } }
|
| 603 |
+
for (const r of reacts){ r.t += dt; r.y -= dt*22; }
|
| 604 |
+
reacts = reacts.filter(r => r.t < 1.4);
|
| 605 |
+
// 金币
|
| 606 |
+
if (today){
|
| 607 |
+
const base = startStats.revenue;
|
| 608 |
+
spawnCoins("ad", today.revenue / base * 5 * dt);
|
| 609 |
+
spawnCoins("share", today.pool / base * 5 * dt * 2.2);
|
| 610 |
+
spawnCoins("cost", today.cost / base * 5 * dt * 2.2);
|
| 611 |
+
}
|
| 612 |
+
for (const c of coins){ c.t += dt; }
|
| 613 |
+
coins = coins.filter(c => c.t < c.dur);
|
| 614 |
+
// 字幕
|
| 615 |
+
capT += dt;
|
| 616 |
+
if (caps.length && (!capShown || capT > 1.4 || capShown.key === "intro" || capShown.key === "ready")){ capShown = caps.shift(); capT = 0; }
|
| 617 |
+
}
|
| 618 |
+
const binCount = {}; let pileCount = [0,0,0];
|
| 619 |
+
function mixSeen(){ const f = today.fm, m = today.made, nt = m.cake + m.pop + m.poop || 1; const ts = P.TRIAL_SHARE;
|
| 620 |
+
return { cake:(1-ts)*f.cake + ts*m.cake/nt, poop:(1-ts)*f.poop + ts*m.poop/nt }; }
|
| 621 |
+
function placeOnPile(it, tier){
|
| 622 |
+
const s = L.sieve[tier]; let idx = pileCount[tier];
|
| 623 |
+
let row = 0, perRow = 15; while (idx >= perRow && row < 9){ idx -= perRow; row++; perRow = Math.max(3, 15 - row*1.5) | 0; }
|
| 624 |
+
if (row >= 9){ it.tier = tier; it.t = 0; judge(it); return; } // 堆满了:不再往上堆,直接筛
|
| 625 |
+
const span = L.bowlW * 0.84 * perRow / 15;
|
| 626 |
+
it.tier = tier; it.x = s.x - span/2 + (idx + 0.5) * (span/perRow) + Math.random()*3 - 1.5; it.y = s.y - 4 - row*7 + Math.random()*2; pileCount[tier]++;
|
| 627 |
+
}
|
| 628 |
+
function judge(it, silent){
|
| 629 |
+
const n = today ? today.sv.tiers[it.tier].n : P.TIERS[it.tier];
|
| 630 |
+
const pass = Math.random() < S.passRate(P.COMPLETION[it.type], n);
|
| 631 |
+
if (silent){ it.st = "dead"; return; }
|
| 632 |
+
if (pass){
|
| 633 |
+
if (it.tier < 2){ it.st = "drop"; it.vy = 40; it.vx = 0; it.next = it.tier + 1; it.x = L.sieve[it.tier].x - 50 + Math.random()*100; it.y = L.sieve[it.tier].y + 26; }
|
| 634 |
+
else { it.st = "chute"; it.t = 0; it.sx = L.sieve[2].x + L.bowlW/2; it.sy = L.sieve[2].y + 2; }
|
| 635 |
+
} else { it.st = "drop"; it.vy = 10; it.vx = (Math.random() < 0.5 ? -1 : 1) * (10 + Math.random()*30); it.next = null; it.rej = true; it.x = L.sieve[it.tier].x + (it.vx < 0 ? -1 : 1) * (L.bowlW*0.5 - 8); it.y = L.sieve[it.tier].y + 4; }
|
| 636 |
+
}
|
| 637 |
+
function spawnCoins(kind, expected){
|
| 638 |
+
let n = Math.floor(expected); if (Math.random() < expected - n) n++;
|
| 639 |
+
for (let i = 0; i < n; i++) coins.push({ kind, t:0, dur: kind === "ad" ? 2.2 : kind === "share" ? 2.6 : 1.3, j:Math.random()*8-4 });
|
| 640 |
+
}
|
| 641 |
+
// 金币路径
|
| 642 |
+
const PATH = {
|
| 643 |
+
ad:[[L.adv.x-40, L.adv.y-10],[L.vault.x+70, L.vault.y-10]],
|
| 644 |
+
share:[[L.vault.x-70, L.vault.y-10],[452, L.vault.y-10],[452, 650]],
|
| 645 |
+
cost:[[L.vault.x+40, L.vault.y+45],[L.server.x-50, L.server.y+30]],
|
| 646 |
+
};
|
| 647 |
+
function along(path, u){
|
| 648 |
+
let len = 0; const seg = []; for (let i = 1; i < path.length; i++){ const l = Math.hypot(path[i][0]-path[i-1][0], path[i][1]-path[i-1][1]); seg.push(l); len += l; }
|
| 649 |
+
let d = u * len; for (let i = 0; i < seg.length; i++){ if (d <= seg[i]){ const v = d/seg[i]; return [path[i][0] + (path[i+1][0]-path[i][0])*v, path[i][1] + (path[i+1][1]-path[i][1])*v]; } d -= seg[i]; }
|
| 650 |
+
return path[path.length-1];
|
| 651 |
+
}
|
| 652 |
+
|
| 653 |
+
// ---------- 画 ----------
|
| 654 |
+
function drawPaper(){
|
| 655 |
+
ctx.fillStyle = C.paper; ctx.fillRect(0,0,W,H);
|
| 656 |
+
ctx.strokeStyle = C.grid; ctx.lineWidth = 1;
|
| 657 |
+
ctx.beginPath(); for (let x = 0; x <= W; x += 25){ ctx.moveTo(x,0); ctx.lineTo(x,H); } for (let y = 0; y <= H; y += 25){ ctx.moveTo(0,y); ctx.lineTo(W,y); } ctx.stroke();
|
| 658 |
+
}
|
| 659 |
+
function sparkline(x,y,w,h,key,base){
|
| 660 |
+
if (hist.length < 2) return;
|
| 661 |
+
const vals = hist.map(o => o[key]); const max = Math.max(base*1.15, ...vals);
|
| 662 |
+
ctx.beginPath(); for (let i = 0; i < 366; i++){ if (i >= vals.length) break; const px = x + i/365*w, py = y + h - vals[i]/max*h; if (i === 0) ctx.moveTo(px,py); else ctx.lineTo(px,py); }
|
| 663 |
+
ctx.strokeStyle = C.accent; ctx.lineWidth = 2; ctx.stroke();
|
| 664 |
+
ctx.setLineDash([3,4]); ctx.strokeStyle = C.soft; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(x, y + h - base/max*h); ctx.lineTo(x+w, y + h - base/max*h); ctx.stroke(); ctx.setLineDash([]);
|
| 665 |
+
}
|
| 666 |
+
function drawTop(){
|
| 667 |
+
ctx.fillStyle = "#ffffffcc"; ctx.fillRect(0,0,W,112); line(0,112,W,112,3,2.5);
|
| 668 |
+
text(tr("title"), 30, 52, lang === "zh" ? 40 : 30, C.ink, "left", 800);
|
| 669 |
+
text(tr("sub"), 32, 86, 18, C.soft);
|
| 670 |
+
// 倍数和天数
|
| 671 |
+
const k = today ? today.k : 1;
|
| 672 |
+
text(tr("ai"), 500, 44, 18, C.soft);
|
| 673 |
+
text("×" + fmtK(started ? k : kSel), 500, 92, 46, k > 1.05 ? C.bad : C.ink, "left", 800);
|
| 674 |
+
text(tr("day", { d: world.day }), 690, 92, 22, C.ink, "left", 600);
|
| 675 |
+
if (!started){ text(lang === "zh" ? "(即将到来)" : "(coming)", 690, 60, 15, C.soft); }
|
| 676 |
+
// 四个表
|
| 677 |
+
const t = today;
|
| 678 |
+
const ms = [
|
| 679 |
+
{ v: fmtInt(t.users), key:"users", base:P.U0, bad: t.users < 0.85*P.U0 },
|
| 680 |
+
{ v: (lang==="zh" ? "" : "¥") + fmtInt(t.revenue) + (lang==="zh" ? " 元" : ""), key:"revenue", base:startStats.revenue, bad: t.revenue < 0.85*startStats.revenue },
|
| 681 |
+
{ v: (lang==="zh" ? "" : "¥") + fmtInt(t.cost) + (lang==="zh" ? " 元" : ""), key:"cost", base:startStats.cost, bad: t.cost > 1.5*startStats.cost },
|
| 682 |
+
{ v: Math.round(t.cakeShareTop*100) + "%", key:"cake", base:1, bad: t.cakeShareTop < 0.5 },
|
| 683 |
+
];
|
| 684 |
+
for (let i = 0; i < 4; i++){
|
| 685 |
+
const x = 840 + i*188, y = 12, w = 176;
|
| 686 |
+
rrect(x, y, w, 90, 100+i*9, "#fff", C.ink, 1.6);
|
| 687 |
+
text(tr("m")[i], x+12, y+24, 15, C.soft);
|
| 688 |
+
text(ms[i].v, x+12, y+58, 27, ms[i].bad ? C.bad : C.ink, "left", 800);
|
| 689 |
+
sparkline(x+12, y+64, w-24, 20, ms[i].key, ms[i].base);
|
| 690 |
+
}
|
| 691 |
+
}
|
| 692 |
+
function drawCaption(){
|
| 693 |
+
if (!capShown) return;
|
| 694 |
+
const s = tr("cap." + capShown.key, capShown.vars);
|
| 695 |
+
const size = 24, w = textW(s, size, 700) + 44, x = W/2 - w/2 - 60, y = 124;
|
| 696 |
+
ctx.save(); ctx.globalAlpha = Math.min(1, capT*4 + (capShown.key==="intro"?1:0));
|
| 697 |
+
rrect(x, y, w, 42, 999, "#fff8d6", C.ink, 1.8);
|
| 698 |
+
text(s, x + w/2, y + 30, size, C.ink, "center", 700); ctx.restore();
|
| 699 |
+
}
|
| 700 |
+
function drawStage(){
|
| 701 |
+
const st = L.stage;
|
| 702 |
+
// 台子
|
| 703 |
+
rrect(st.x+6, 620, st.w-20, 22, 11, "#e8dcc2", C.ink, 2.2);
|
| 704 |
+
line(st.x+20, 642, st.x+20, 660, 12, 2.2); line(st.x+st.w-30, 642, st.x+st.w-30, 660, 13, 2.2);
|
| 705 |
+
text(tr("stage"), st.x+14, 168, 20, C.ink, "left", 700);
|
| 706 |
+
text(tr("stageNote"), st.x+14 + textW(tr("stage"), 20, 700) + 10, 168, 13, C.soft);
|
| 707 |
+
for (let i = 0; i < slots.length; i++){
|
| 708 |
+
const s = slots[i]; if (!s.type) continue; const p = L.slots[i];
|
| 709 |
+
const f = s.f || 0.1; const blur = f > 3.2 ? 1 : 0; const push = blur ? 0 : Math.max(0, 1 - Math.abs(s.phase - 0.12) * 6);
|
| 710 |
+
ctx.save(); const sc = 0.4 + 0.6 * easeOut(s.pop); ctx.translate(p.x, p.y); ctx.scale(sc, sc);
|
| 711 |
+
drawPerson(0, 0, s.type, { seed: s.seed, push, blur, mood: 1, blink: (performance.now()/400 + i) % 2 < 1, spark: s.spark > 0 });
|
| 712 |
+
ctx.restore();
|
| 713 |
+
}
|
| 714 |
+
for (const w of walkers){ drawPerson(w.x, w.y, w.type, { seed: w.seed, pack: true, flip: true, walk: w.t*9, mood: -1, alpha: Math.max(0, Math.min(1, (w.x+40)/120)) }); }
|
| 715 |
+
// 人数
|
| 716 |
+
const c = today.count;
|
| 717 |
+
const legend = [["cake", tr("cake"), c.cake], ["pop", tr("pop"), c.pop], ["poop", tr("bot"), c.poop]];
|
| 718 |
+
// 竖排,一行一种,图标放大到看得清
|
| 719 |
+
legend.forEach(([type, name, n], i) => {
|
| 720 |
+
const y = 706 + i * 68;
|
| 721 |
+
drawItem(type, st.x + 46, y, 50);
|
| 722 |
+
const col = type === "poop" && n > 0 ? C.bad : C.ink;
|
| 723 |
+
text(name, st.x + 92, y + 10, 26, col, "left", 700);
|
| 724 |
+
text(fmtInt(n), st.x + 92 + textW(name, 26, 700) + 14, y + 10, 30, col, "left", 800);
|
| 725 |
+
});
|
| 726 |
+
}
|
| 727 |
+
function easeOut(u){ return 1 - Math.pow(1-u, 3); }
|
| 728 |
+
function drawSieves(){
|
| 729 |
+
for (let i = 0; i < 3; i++){
|
| 730 |
+
const s = L.sieve[i], bw = L.bowlW;
|
| 731 |
+
// 碗
|
| 732 |
+
ctx.beginPath(); ctx.moveTo(s.x - bw/2, s.y); ctx.quadraticCurveTo(s.x, s.y + 64, s.x + bw/2, s.y); ctx.strokeStyle = C.ink; ctx.lineWidth = 2.4; ctx.stroke();
|
| 733 |
+
ctx.save(); ctx.beginPath(); ctx.moveTo(s.x - bw/2, s.y); ctx.quadraticCurveTo(s.x, s.y + 64, s.x + bw/2, s.y); ctx.closePath(); ctx.clip();
|
| 734 |
+
ctx.strokeStyle = "#9aa7b8"; ctx.lineWidth = 1; ctx.beginPath(); for (let x = -bw/2; x < bw/2; x += 10){ ctx.moveTo(s.x + x, s.y); ctx.lineTo(s.x + x + 30, s.y + 40); ctx.moveTo(s.x + x + 30, s.y); ctx.lineTo(s.x + x, s.y + 40); } ctx.stroke(); ctx.restore();
|
| 735 |
+
line(s.x - bw/2 - 10, s.y, s.x + bw/2 + 10, s.y, 300+i, 2.6);
|
| 736 |
+
// 试吃的人
|
| 737 |
+
const n = today.sv.tiers[i].n; const tasters = Math.max(1, Math.round(5 * today.users / P.U0));
|
| 738 |
+
for (let j = 0; j < tasters; j++){ const tx = s.x + bw/2 + 34 + (j%3)*24, ty = s.y - 12 + Math.floor(j/3)*30; const bob = Math.sin(performance.now()/180 + j*1.7) * 2;
|
| 739 |
+
circ(tx, ty + bob, 8, "#fff", C.ink, 1.8); circ(tx-3, ty-2+bob, 1.2, C.ink, false); circ(tx+3, ty-2+bob, 1.2, C.ink, false); line(tx-3, ty+3+bob, tx+3, ty+3+bob, j, 1.2, C.ink, 0.2); }
|
| 740 |
+
const lab = tr("tier")[i];
|
| 741 |
+
|
| 742 |
+
}
|
| 743 |
+
// 没被推荐
|
| 744 |
+
ctx.beginPath(); ctx.ellipse(L.bin.x, L.bin.y + 6, 120, 14, 0, 0, Math.PI*2); ctx.fillStyle = "#eceae3"; ctx.fill(); ctx.strokeStyle = C.ink; ctx.lineWidth = 1.6; ctx.stroke();
|
| 745 |
+
text(tr("rejected"), L.bin.x, L.bin.y + 42, 14, C.soft, "center");
|
| 746 |
+
// 滑道
|
| 747 |
+
line(L.sieve[2].x + L.bowlW/2, L.sieve[2].y + 12, L.feed.x1 - 6, L.feed.y + 14, 410, 2.2, C.ink, 0.8);
|
| 748 |
+
}
|
| 749 |
+
function drawTierLabels(){
|
| 750 |
+
for (let i = 0; i < 3; i++){ const s = L.sieve[i], n = today.sv.tiers[i].n, lab = tr("tier")[i];
|
| 751 |
+
const nTxt = n >= 10 ? fmtInt(n) : n >= 0.1 ? (Math.round(n*10)/10).toString() : (lang === "zh" ? "不到 0.1" : "<0.1");
|
| 752 |
+
const lt = lab + " " + tr("perItem", { n: nTxt }); const lw2 = textW(lt, 14, 700) + 20;
|
| 753 |
+
rrect(s.x - lw2/2, s.y + 46, lw2, 26, 320+i, "#fffffff2", C.ink, 1.2);
|
| 754 |
+
text(lab, s.x - lw2/2 + 10, s.y + 64, 14, C.ink, "left", 800);
|
| 755 |
+
text(tr("perItem", { n: nTxt }), s.x - lw2/2 + 10 + textW(lab + " ", 14, 800), s.y + 64, 14, n < P.TIERS[i] * 0.3 ? C.bad : C.soft, "left", 700); }
|
| 756 |
+
}
|
| 757 |
+
function sampleFeed(){ const f = today ? today.fm : { cake:1, pop:0, poop:0 }; const r = Math.random(); return r < f.cake ? "cake" : r < f.cake + f.pop ? "pop" : "poop"; }
|
| 758 |
+
function drawItems(){
|
| 759 |
+
for (const it of items){ const sz = it.st === "fly" ? 13 : 15; const a = it.rej ? Math.max(0.15, 1 - (it.y - L.sieve[it.tier].y) / 160) : 1; drawItem(it.type, it.x, it.y, sz, a); }
|
| 760 |
+
for (const b of belt) drawItem(b.type, b.x, L.feed.y - 2, 22);
|
| 761 |
+
}
|
| 762 |
+
function drawPlaza(){
|
| 763 |
+
// 推荐流
|
| 764 |
+
text(tr("feed"), L.feed.x1, L.feed.y + 46, 16, C.ink, "left", 700);
|
| 765 |
+
line(L.feed.x1 - 8, L.feed.y + 16, L.feed.x2 + 6, L.feed.y + 16, 500, 2.4);
|
| 766 |
+
for (let x = L.feed.x1; x < L.feed.x2; x += 28){ circ(x + ((performance.now()/1000*70) % 28), L.feed.y + 21, 3.5, "#fff", C.ink, 1.4); }
|
| 767 |
+
text(tr("plaza"), L.plaza.x + 10, L.plaza.y + 18, 18, C.ink, "left", 700);
|
| 768 |
+
text(tr("plazaNote"), L.plaza.x + 10 + textW(tr("plaza"), 18, 700) + 10, L.plaza.y + 18, 13, C.soft);
|
| 769 |
+
const sat = today ? today.sat : 0.9;
|
| 770 |
+
for (const d of dots){
|
| 771 |
+
const m = sat + d.off; const mood = m > 0.55 ? 1 : m > -0.1 ? 0 : -1;
|
| 772 |
+
ctx.save(); ctx.translate(d.x, d.y); const fill = mood > 0 ? "#ffeef0" : mood < 0 ? "#e9e3dc" : "#ffffff";
|
| 773 |
+
circ(0,0,9,fill,C.ink,1.6); circ(-3,-2,1.3,C.ink,false); circ(3,-2,1.3,C.ink,false);
|
| 774 |
+
ctx.beginPath(); ctx.strokeStyle = C.ink; ctx.lineWidth = 1.3; if (mood > 0) ctx.arc(0,1,3.5,0.15*Math.PI,0.85*Math.PI); else if (mood === 0){ ctx.moveTo(-3,4); ctx.lineTo(3,4); } else ctx.arc(0,7,3.5,1.15*Math.PI,1.85*Math.PI); ctx.stroke();
|
| 775 |
+
ctx.restore();
|
| 776 |
+
}
|
| 777 |
+
for (const r of reacts){ ctx.save(); ctx.globalAlpha = Math.max(0, 1 - r.t/1.4);
|
| 778 |
+
if (r.kind === "heart"){ ctx.fillStyle = C.cake; ctx.beginPath(); const x = r.x, y = r.y; ctx.moveTo(x, y+4); ctx.bezierCurveTo(x-8, y-2, x-4, y-9, x, y-4); ctx.bezierCurveTo(x+4, y-9, x+8, y-2, x, y+4); ctx.fill(); }
|
| 779 |
+
else { ctx.fillStyle = "#7c8796"; circ(r.x-4, r.y, 4, "#7c8796", false); circ(r.x+3, r.y-2, 5, "#7c8796", false); circ(r.x+8, r.y+1, 3.5, "#7c8796", false); }
|
| 780 |
+
ctx.restore(); }
|
| 781 |
+
}
|
| 782 |
+
function drawMoney(){
|
| 783 |
+
const t = today;
|
| 784 |
+
// 注意力:用户 → 广告商
|
| 785 |
+
ctx.setLineDash([6,6]); line(1420, 628, L.adv.x - 6, L.adv.y - 70, 600, 1.8, C.accent, 0.5); ctx.setLineDash([]);
|
| 786 |
+
text(tr("attention"), 1455, 672, 14, C.accent, "right", 600);
|
| 787 |
+
// 管道
|
| 788 |
+
const pipe = (path, w, seed) => { ctx.lineCap = "round"; ctx.lineJoin = "round"; ctx.strokeStyle = C.ink; ctx.lineWidth = w + 4; ctx.beginPath(); path.forEach(([x,y],i) => i ? ctx.lineTo(x,y) : ctx.moveTo(x,y)); ctx.stroke(); ctx.strokeStyle = "#fff"; ctx.lineWidth = w; ctx.stroke(); ctx.lineCap = "butt"; };
|
| 789 |
+
const base = startStats.revenue;
|
| 790 |
+
pipe(PATH.ad, 6 + 14 * Math.min(1.5, t.revenue/base), 1);
|
| 791 |
+
pipe(PATH.share, 3 + 6 * Math.min(1.5, t.pool/(base*P.CREATOR_SHARE)), 2);
|
| 792 |
+
pipe(PATH.cost, 3 + 5 * Math.min(4, t.cost/startStats.cost), 3);
|
| 793 |
+
for (const c of coins){ const u = c.t / c.dur; const [x,y] = along(PATH[c.kind], u); circ(x, y + (c.kind === "share" ? 0 : c.j*0.3), 6, C.coin, C.ink, 1.3); circ(x-1.5, y-1.5, 2, C.coinHi, false); }
|
| 794 |
+
// 广告商
|
| 795 |
+
drawPerson(L.adv.x, L.adv.y + 30, "pop", { seed: 901, mood: t.revenue > 0.85*base ? 1 : 0, push: 0 });
|
| 796 |
+
ctx.fillStyle = C.accent; ctx.beginPath(); ctx.moveTo(L.adv.x, L.adv.y + 0); ctx.lineTo(L.adv.x - 3, L.adv.y + 14); ctx.lineTo(L.adv.x + 3, L.adv.y + 14); ctx.fill();
|
| 797 |
+
rrect(L.adv.x + 14, L.adv.y + 2, 24, 18, 902, "#8f6b3e", C.ink, 1.6);
|
| 798 |
+
text(tr("adv"), L.adv.x, L.adv.y + 72, 16, C.ink, "center", 700);
|
| 799 |
+
text(tr("adIn", { v: fmtInt(t.revenue) }), (PATH.ad[0][0] + PATH.ad[1][0]) / 2, L.adv.y - 34, 15, C.ink, "center", 600);
|
| 800 |
+
// 金库
|
| 801 |
+
rrect(L.vault.x - 60, L.vault.y - 50, 120, 96, 910, "#e7ecf3", C.ink, 2.4);
|
| 802 |
+
circ(L.vault.x, L.vault.y - 2, 18, "#fff", C.ink, 2); line(L.vault.x, L.vault.y - 2, L.vault.x + 12, L.vault.y - 10, 911, 2);
|
| 803 |
+
text(tr("vault"), L.vault.x, L.vault.y - 60, 16, C.ink, "center", 700);
|
| 804 |
+
const net = t.revenue * (1 - P.CREATOR_SHARE) - t.cost;
|
| 805 |
+
text(net >= 0 ? tr("keep", { v: fmtInt(net) }) : tr("lose", { v: fmtInt(-net) }), L.vault.x, L.vault.y + 72, 15, net >= 0 ? C.good : C.bad, "center", 700);
|
| 806 |
+
// 服务器
|
| 807 |
+
const racks = Math.min(6, Math.max(1, Math.round(t.cost / startStats.cost)));
|
| 808 |
+
for (let i = 0; i < racks; i++){ rrect(L.server.x - 40 + (i%3)*30, L.server.y - (Math.floor(i/3))*30 - 4, 26, 28, 920+i, "#dfe4ea", C.ink, 1.6); circ(L.server.x - 33 + (i%3)*30, L.server.y + 4 - Math.floor(i/3)*30, 2, C.good, false); }
|
| 809 |
+
text(tr("server"), L.server.x + 60, L.server.y + 14, 14, C.soft, "left");
|
| 810 |
+
text((lang==="zh" ? "" : "¥") + fmtInt(t.cost) + (lang==="zh" ? " 元/天" : "/day"), L.server.x + 60, L.server.y + 34, 15, t.cost > 1.5*startStats.cost ? C.bad : C.ink, "left", 700);
|
| 811 |
+
// 分给创作者
|
| 812 |
+
text(tr("toCreators", { v: fmtInt(t.pool) }), 480, L.vault.y - 26, 15, C.ink, "left", 600);
|
| 813 |
+
}
|
| 814 |
+
function drawEnd(){
|
| 815 |
+
if (!ended || !endStats) return;
|
| 816 |
+
const fail = endStats.users < 0.75*P.U0 || endStats.cake < 0.5;
|
| 817 |
+
const head = fail ? tr("endFail", { k: fmtK(kSel) }) : tr("endOk", { k: fmtK(kSel) });
|
| 818 |
+
ctx.save(); ctx.fillStyle = "#fbfbf7e6"; ctx.fillRect(0, 112, W, H-112);
|
| 819 |
+
const x = 330, y = 210, w = 940, h = 470;
|
| 820 |
+
rrect(x, y, w, h, 1001, "#fff", C.ink, 2.6);
|
| 821 |
+
text(tr("endHead"), x + 40, y + 58, 20, C.soft, "left", 700);
|
| 822 |
+
const hs = textW(head, 34, 800) > w - 80 ? 26 : 34;
|
| 823 |
+
text(head, x + 40, y + 108, hs, C.ink, "left", 800);
|
| 824 |
+
const rows = [
|
| 825 |
+
[tr("m")[0], fmtInt(startStats.users), fmtInt(endStats.users)],
|
| 826 |
+
[tr("m")[1], fmtInt(startStats.revenue), fmtInt(endStats.revenue)],
|
| 827 |
+
[tr("m")[2], fmtInt(startStats.cost), fmtInt(endStats.cost)],
|
| 828 |
+
[tr("m")[3], Math.round(startStats.cake*100) + "%", Math.round(endStats.cake*100) + "%"],
|
| 829 |
+
];
|
| 830 |
+
text(tr("from"), x + 520, y + 168, 16, C.soft, "right", 700); text(tr("to"), x + 760, y + 168, 16, C.soft, "right", 700);
|
| 831 |
+
rows.forEach((r,i) => { const yy = y + 214 + i*52; line(x + 40, yy + 16, x + w - 40, yy + 16, 1100+i, 1, C.grid, 0.4);
|
| 832 |
+
text(r[0], x + 40, yy, 20, C.ink, "left", 600); text(r[1], x + 520, yy, 22, C.ink, "right", 700); text("→", x + 600, yy, 22, C.soft, "center"); text(r[2], x + 760, yy, 22, C.ink, "right", 800); });
|
| 833 |
+
text(tr("again"), x + 40, y + h - 30, 15, C.soft, "left");
|
| 834 |
+
ctx.restore();
|
| 835 |
+
}
|
| 836 |
+
function render(){
|
| 837 |
+
ctx.setTransform(DPR,0,0,DPR,0,0);
|
| 838 |
+
drawPaper(); drawStage(); drawSieves(); drawPlaza(); drawMoney(); drawItems(); drawTierLabels(); drawTop(); drawCaption(); drawEnd();
|
| 839 |
+
}
|
| 840 |
+
|
| 841 |
+
// ---------- 循环 ----------
|
| 842 |
+
let last = performance.now();
|
| 843 |
+
function frame(now){
|
| 844 |
+
const dt = Math.min(0.05, (now - last) / 1000); last = now;
|
| 845 |
+
update(dt); render(); requestAnimationFrame(frame);
|
| 846 |
+
}
|
| 847 |
+
|
| 848 |
+
// ---------- 控件 ----------
|
| 849 |
+
const btnPlay = document.getElementById("play"), btnReset = document.getElementById("reset"), kIn = document.getElementById("k"), kOut = document.getElementById("kOut"), btnLang = document.getElementById("lang");
|
| 850 |
+
const presets = [1, 3, 10, 30, 100]; const presetBox = document.getElementById("presets");
|
| 851 |
+
presets.forEach(v => { const b = document.createElement("button"); b.type = "button"; b.textContent = "×" + v; b.addEventListener("click", () => { setK(v); }); presetBox.appendChild(b); });
|
| 852 |
+
function niceK(v){ if (v < 10) return Math.round(v*10)/10; return Math.round(v); }
|
| 853 |
+
function setK(v){ kSel = niceK(v); kIn.value = Math.log10(kSel); kOut.textContent = "×" + fmtK(kSel); world.kTarget = kSel; if (!started) reset(); else if (world.day >= world.aiDay) {/* 运行中改倍数:立刻生效 */} }
|
| 854 |
+
kIn.addEventListener("input", () => setK(Math.pow(10, +kIn.value)));
|
| 855 |
+
btnPlay.addEventListener("click", () => {
|
| 856 |
+
if (ended){ reset(); }
|
| 857 |
+
if (!started){ started = true; running = true; capShown = { key:"intro" }; capT = 0; }
|
| 858 |
+
else running = !running;
|
| 859 |
+
setPlayLabel();
|
| 860 |
+
});
|
| 861 |
+
btnReset.addEventListener("click", () => { reset(); });
|
| 862 |
+
document.querySelectorAll("[data-speed]").forEach(b => b.addEventListener("click", () => { speed = +b.dataset.speed; document.querySelectorAll("[data-speed]").forEach(o => o.setAttribute("aria-pressed", o === b ? "true" : "false")); }));
|
| 863 |
+
btnLang.addEventListener("click", () => setLang(lang === "zh" ? "en" : "zh"));
|
| 864 |
+
function setPlayLabel(){ btnPlay.textContent = ended ? tr("play") : !started ? tr("play") : running ? tr("pause") : tr("resume"); }
|
| 865 |
+
function setLang(l){ lang = l; document.documentElement.lang = l === "zh" ? "zh-CN" : "en";
|
| 866 |
+
btnLang.textContent = tr("lang"); btnReset.textContent = tr("reset"); document.getElementById("kLab").textContent = tr("kLab"); document.getElementById("spLab").textContent = tr("spLab");
|
| 867 |
+
const sp = document.querySelectorAll("[data-speed]"); sp[0].textContent = tr("slow"); sp[1].textContent = tr("normal"); sp[2].textContent = tr("fast");
|
| 868 |
+
document.getElementById("note").textContent = tr("note"); cv.setAttribute("aria-label", l === "zh" ? "推荐池模拟画面" : "Recommendation pool simulation"); setPlayLabel(); }
|
| 869 |
+
|
| 870 |
+
// 截图用:快进到某一天,并让画面上的东西先流动一会儿
|
| 871 |
+
window.demo = {
|
| 872 |
+
jump(k, day, warmSec){ kSel = k; kIn.value = Math.log10(k); kOut.textContent = "×" + fmtK(k); reset(); started = true; running = false;
|
| 873 |
+
while (world.day < day){ today = S.step(world); pushHist(today); } peakBots = 0; layoutSlots(true); layoutDots(true); caps = []; capShown = null; ended = false;
|
| 874 |
+
const steps = Math.round((warmSec || 4) / 0.02); for (let i = 0; i < steps; i++) update(0.02); },
|
| 875 |
+
setLang, get today(){ return today; }, get world(){ return world; },
|
| 876 |
+
finishAt(k){ this.jump(k, 365, 3); finish(); },
|
| 877 |
+
};
|
| 878 |
+
|
| 879 |
+
reset(); setK(kSel);
|
| 880 |
+
setLang("en");
|
| 881 |
+
capShown = { key:"ready" };
|
| 882 |
+
requestAnimationFrame(frame);
|
| 883 |
+
})();
|
| 884 |
+
</script>
|