coworking-agents 0.2.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -6
- package/docs/achievements.png +0 -0
- package/docs/building.png +0 -0
- package/docs/camera.png +0 -0
- package/docs/christmas.png +0 -0
- package/docs/customise.png +0 -0
- package/docs/game-room.png +0 -0
- package/docs/halloween.png +0 -0
- package/docs/house-wall.png +0 -0
- package/docs/mobile.png +0 -0
- package/docs/office-day.png +0 -0
- package/docs/office-night.png +0 -0
- package/docs/panel.png +0 -0
- package/docs/paper.png +0 -0
- package/docs/servers.png +0 -0
- package/docs/talk.png +0 -0
- package/docs/top-bar.png +0 -0
- package/docs/trophy-room.png +0 -0
- package/docs/usage.png +0 -0
- package/docs/work-site.png +0 -0
- package/package.json +1 -1
- package/public/art.js +1 -1
- package/public/index.html +4 -0
- package/public/js/achievements.js +14 -3
- package/public/js/collection.js +21 -4
- package/public/js/fun2.js +224 -0
- package/public/js/i18n.js +56 -8
- package/public/js/keys.js +1 -0
- package/public/js/life.js +6 -3
- package/public/js/main.js +1 -1
- package/public/js/overlay.js +5 -2
- package/public/js/panel.js +4 -0
- package/public/js/personal.js +23 -9
- package/public/js/player.js +4 -3
- package/public/js/report.js +4 -1
- package/public/js/rooms.js +9 -5
- package/public/js/scene.js +1 -0
- package/public/js/talk.js +1 -1
- package/public/js/topbar.js +3 -1
- package/public/js/views.js +118 -0
- package/public/js/work.js +93 -0
- package/public/style.css +28 -0
- package/src/demo.js +3 -1
- package/src/focus.js +14 -1
- package/src/server.js +12 -1
- package/src/usage.js +6 -3
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
|  | **Agent panel.** Now, last request and reply, last-hour timeline, today's numbers, tokens, context, files, subagents, certificates. |
|
|
25
25
|
|  | **Usage report.** Tokens per day for 14 days, all-time totals per AI and per model, rate-limit windows with reset times. |
|
|
26
26
|
|  | **Arrivals and departures.** A new session gets its room boarded up and built (planks, dust, caution tape); a closed one walks out and its desk is demolished. |
|
|
27
|
-
|  | **Achievements.**
|
|
27
|
+
|  | **Achievements.** 86 of them, five tiers each, coins for every tier, the agent of the month on top. |
|
|
28
28
|
|  | **The Office Times.** Your week in headlines, written from measured history. |
|
|
29
29
|
|  | **Trophy room.** Glass cabinets with lit shelves, legend trophies in a glass case under spotlights, a crown under a dome for Legend of Legends. Click a trophy for its card. |
|
|
30
30
|
|  | **Holidays.** Lights, snow and Santa hats at Christmas; pumpkins, bats and witch hats at Halloween; flags at Festa Junina. |
|
|
@@ -126,6 +126,11 @@ Agents away from their desks (kitchen, meeting room, nap corner) carry a name ta
|
|
|
126
126
|
- **Git on the door:** each room sign shows uncommitted files (●), commits to push (↑) and to pull (↓), read with `git status` in the background (no fetch).
|
|
127
127
|
- **History and daily report:** the last 24 hours of arrivals, departures, requests and finished work; a daily report with active time, tools and files that copies as Markdown for a standup; your reply times.
|
|
128
128
|
- **Estimated cost:** what the tokens would cost at official API list prices (table in `src/prices.js`, dated; override in `~/.config/coworking-agents/prices.json`), priced per reply with Claude's cache-write split. Unknown models are never guessed. On a subscription this is not what you pay.
|
|
129
|
+
- **The queue** (`Q`): everyone waiting on you, longest wait first, with a button to answer each one.
|
|
130
|
+
- **Quick replies:** one click fills the reply box with "Go ahead", "Run the tests", "Commit and push" and more; you still choose to send.
|
|
131
|
+
- **Welcome back:** after 15+ minutes away, a card tells you what finished, who arrived or left and who is waiting.
|
|
132
|
+
- **Daily goal and cost alert:** set a goal of agent hours and a limit for the estimated cost of the day (daily report tab); you get told when either is reached.
|
|
133
|
+
- **New session:** ☰ → New session opens a Terminal tab in one of your folders and starts Claude Code or Codex (macOS; only an existing folder and those two commands are accepted).
|
|
129
134
|
- **Focus mode:** sounds and system notifications pause for 25 or 50 minutes.
|
|
130
135
|
- **Replay:** scrub or play back the last hour.
|
|
131
136
|
|
|
@@ -143,20 +148,31 @@ A marble portal leads to the **trophy room**, its own screen: legends with wings
|
|
|
143
148
|
|
|
144
149
|

|
|
145
150
|
|
|
151
|
+
## The elevator, the rooftop and the server room
|
|
152
|
+
|
|
153
|
+
Click the elevator (or press Enter next to it) to ride between floors, up to the **rooftop**, where agents whose turn ended relax on lounge chairs under the real sky with the city lights below, and down to the **server room**, where every open session is a rack whose LEDs blink with its real state and whose bar shows its output today. Click someone on the rooftop or a rack to open that session.
|
|
154
|
+
|
|
155
|
+

|
|
156
|
+
|
|
146
157
|
## Achievements, coins and the shop
|
|
147
158
|
|
|
148
|
-
- **
|
|
159
|
+
- **86 achievements**, each with five tiers from bronze to legend, measured from your history (hours, actions, streaks, nights, Friday evenings, weeks and months with work, tokens, cache, models, cost), the live office (parallel sessions, subagents, giant contexts, commits and pushes, repos and branches, quick answers, clean desks) and what you do in the office (walking, talking, the radio, the shop, customising). A few are secret.
|
|
149
160
|
- **Legend of Legends** when every achievement is at legend.
|
|
150
|
-
- **Coins:** each tier pays 10 to 50 coins (the super achievement 1000). Spend them in the **shop** on decor that appears in the office: corridor plants, a rug, a COFFEE neon, floor lamps, a disco ball, a golden statue of the agent of the month, a piano, a fountain. ☰ → **Decorate** lets you drag what you bought anywhere in the office (saved in your progress file).
|
|
161
|
+
- **Coins:** each tier pays 10 to 50 coins (the super achievement 1000). Spend them in the **shop** on decor that appears in the office: corridor plants, a rug, a COFFEE neon, floor lamps, a disco ball, a golden statue of the agent of the month, a piano, a fountain, a turtle terrarium, a bunny, a bigger aquarium, and clothes for your character. ☰ → **Decorate** lets you drag what you bought anywhere in the office (saved in your progress file).
|
|
151
162
|
- **Agent of the month:** a framed portrait on the wall of the conversation with the most active time this month; the top 5 are in the achievements tab.
|
|
152
163
|
- **The weekly paper**, *The Office Times*, tells your week in headlines, all from measured data.
|
|
164
|
+
- **Weekly missions:** three new challenges every week (hours, actions, days worked, commits, pushes, quick replies, pets, fast answers, long tasks), 100 coins each.
|
|
165
|
+
- **Your outfit:** caps, headphones, shades, a party hat, shirts and a crown from the shop; your character wears them (pick in Customise).
|
|
166
|
+
- **Bug Catcher:** click an arcade in the game room: catch falling bugs with your laptop, golden ones and combos score more, three misses end the game; the best score is kept.
|
|
167
|
+
- **Surprises:** now and then a cake, a pigeon at the window or a golden ticket shows up; click it for coins.
|
|
168
|
+
- **Share:** the paper tab makes a weekly card (hours, actions, trophies, a slice of the office) and a visitor photo of the office without names, branches or the office name. It also shows this week's AI scoreboard (hours, actions, tokens per AI).
|
|
153
169
|
- **Progress file:** achievements, coins, purchases, avatars and settings live in `~/.config/coworking-agents/progress.json` (mode `0600`). No account needed: the app runs on your machine, so the progress is yours.
|
|
154
170
|
|
|
155
171
|
## Make it yours
|
|
156
172
|
|
|
157
173
|

|
|
158
174
|
|
|
159
|
-
☰ → **Customise** has 13 options: the office name (on a plaque by the elevator and in the top bar), your own look, floor (wood, dark wood, carpet, concrete, tiles, checker, marble, grass), walls (10 colours), room carpets (5 palettes), desks (5), the window view (city, beach, mountains, forest, space), the sky (automatic, always day, sunset, always night), lighting (natural, warm, cool, neon), the mascot (cat, dog, robot, fox, penguin, bunny), plants (normal or jungle), the radio station and the accent colour. The die next to an agent's face in its panel gives that agent a new look.
|
|
175
|
+
☰ → **Customise** shows a live preview of the office on top and has 13 options: the office name (on a plaque by the elevator and in the top bar), your own look, floor (wood, dark wood, carpet, concrete, tiles, checker, marble, grass), walls (10 colours), room carpets (5 palettes), desks (5), the window view (city, beach, mountains, forest, space), the sky (automatic, always day, sunset, always night), lighting (natural, warm, cool, neon), the mascot (cat, dog, robot, fox, penguin, bunny), plants (normal or jungle), the radio station and the accent colour. The die next to an agent's face in its panel gives that agent a new look.
|
|
160
176
|
|
|
161
177
|
## A living office
|
|
162
178
|
|
|
@@ -166,7 +182,8 @@ A marble portal leads to the **trophy room**, its own screen: legends with wings
|
|
|
166
182
|
- **Holidays:** Christmas, New Year, Halloween, Festa Junina, Carnaval and the office's birthday decorate the room and put hats on everyone (`E` turns decor off).
|
|
167
183
|
- **Pets:** the mascot visits whoever has waited on you the longest and likes being petted; a dog joins at 8 achievements at gold, a parrot at 16.
|
|
168
184
|
- **Certificates** for the skills and MCP servers each session used hang on its partition.
|
|
169
|
-
- **Office photo:** ☰ → Office photo saves the scene as a PNG
|
|
185
|
+
- **Office photo:** ☰ → Office photo saves the scene as a PNG, and the last four hang as framed photos on the game room wall.
|
|
186
|
+
- **Weather indoors:** umbrellas by the elevator when it rains outside (real weather on), coats on the rack in winter. And something happens if you click the `</>` neon three times.
|
|
170
187
|
- **Languages:** English and Brazilian Portuguese.
|
|
171
188
|
|
|
172
189
|
<p align="center"><img src="docs/mobile.png" alt="The office on a phone" width="300"></p>
|
|
@@ -178,7 +195,7 @@ A marble portal leads to the **trophy room**, its own screen: legends with wings
|
|
|
178
195
|
| `1`–`9` | open the n-th agent |
|
|
179
196
|
| `N` | next agent that needs you (opens its talk modal) |
|
|
180
197
|
| `/` | quick search by name, title, repository or state |
|
|
181
|
-
| `R` / `T` / `G` / `C` | usage report / daily report (today) / history / achievements |
|
|
198
|
+
| `Q` / `R` / `T` / `G` / `C` | queue / usage report / daily report (today) / history / achievements |
|
|
182
199
|
| WASD or arrows, Shift, Enter | walk, run, interact with what's near you |
|
|
183
200
|
| `F` | focus mode (25 min, again 50, again off) |
|
|
184
201
|
| `H` | replay the last hour |
|
package/docs/achievements.png
CHANGED
|
Binary file
|
package/docs/building.png
CHANGED
|
Binary file
|
package/docs/camera.png
CHANGED
|
Binary file
|
package/docs/christmas.png
CHANGED
|
Binary file
|
package/docs/customise.png
CHANGED
|
Binary file
|
package/docs/game-room.png
CHANGED
|
Binary file
|
package/docs/halloween.png
CHANGED
|
Binary file
|
package/docs/house-wall.png
CHANGED
|
Binary file
|
package/docs/mobile.png
CHANGED
|
Binary file
|
package/docs/office-day.png
CHANGED
|
Binary file
|
package/docs/office-night.png
CHANGED
|
Binary file
|
package/docs/panel.png
CHANGED
|
Binary file
|
package/docs/paper.png
CHANGED
|
Binary file
|
package/docs/servers.png
ADDED
|
Binary file
|
package/docs/talk.png
CHANGED
|
Binary file
|
package/docs/top-bar.png
CHANGED
|
Binary file
|
package/docs/trophy-room.png
CHANGED
|
Binary file
|
package/docs/usage.png
CHANGED
|
Binary file
|
package/docs/work-site.png
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "coworking-agents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "A pixel-art coworking office for your AI coding agents (Claude Code, Codex, …): see who's working, who needs you, and who's editing the same checkout.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"coworking-agents": "bin/coworking-agents.js"
|
package/public/art.js
CHANGED
|
@@ -759,7 +759,7 @@ function aiIconEl(id, color) {
|
|
|
759
759
|
|
|
760
760
|
window.Art = {
|
|
761
761
|
aiIcon, aiIconEl, portrait, drawArcade, drawDiagramBoard, drawNeon, drawPoster, drawServerRack, drawDuck, drawSideMonitor, drawStickies, drawBeanSack, drawPizza, drawCupStack,
|
|
762
|
-
PAL, SKIN, HAIR, SHIRT, SCREEN_GLOW, hash, shade, look, setLookSeeds, setHat, setCtx, r, sprite, blit,
|
|
762
|
+
PAL, SKIN, HAIR, SHIRT, SCREEN_GLOW, hash, shade, look, setLookSeeds, setHat, drawHat, setCtx, r, sprite, blit,
|
|
763
763
|
drawSeatedBack, drawFront, drawStanding, drawSleeping, drawChairBack, drawChairBase, drawCat, bubble, drawScreen,
|
|
764
764
|
drawPlant, drawCertificate, drawLamp, drawWhiteboard, drawCork, drawClock, skyFor, drawWindow, drawSofa, drawCoffeeMachine,
|
|
765
765
|
drawCooler, drawRug, drawFloor, applyLight, LEGS_SIT,
|
package/public/index.html
CHANGED
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
<button id="btn-replay" class="icon-btn"><svg viewBox="0 0 16 16" shape-rendering="crispEdges" aria-hidden="true"><path fill="currentColor" d="M6 1h4v1h2v1h1v2h1v6h-1v2h-1v1h-2v1H6v-1H4v-1H3v-2H2V9h2v2h1v1h1v1h4v-1h1v-1h1V5h-1V4h-1V3H6v1H5v1H4v1h2v2H1V3h2v1h1V3h1V2h1zM7 5h2v3h2v2H7z"/></svg><span class="sr"></span></button>
|
|
31
31
|
<button id="btn-search" class="icon-btn"><svg viewBox="0 0 16 16" shape-rendering="crispEdges" aria-hidden="true"><path fill="currentColor" d="M5 1h4v1h1v1h1v4h-1v1H9v1H5V8H4V7H3V3h1V2h1zm0 2v4h4V3zM10 9h1v1h1v1h1v1h1v1h-1v1h-1v-1h-1v-1h-1v-1H9v-1h1z"/></svg><span class="sr"></span></button>
|
|
32
32
|
<button id="btn-photo" class="icon-btn"><svg viewBox="0 0 16 16" shape-rendering="crispEdges" aria-hidden="true"><path fill="currentColor" d="M5 2h6v2h3v10H2V4h3zm3 4H7v1H6v3h1v1h2v-1h1V7H9V6z"/></svg><span class="sr"></span></button>
|
|
33
|
+
<button id="btn-launch" class="icon-btn"><svg viewBox="0 0 16 16" shape-rendering="crispEdges" aria-hidden="true"><path fill="currentColor" d="M1 2h14v12H1zm2 2v8h10V4zm1 1h2v1h1v1H6v1H4V7h1V6H4zm4 3h3v1H8z"/></svg><span class="sr"></span></button>
|
|
33
34
|
<button id="btn-deco" class="icon-btn"><svg viewBox="0 0 16 16" shape-rendering="crispEdges" aria-hidden="true"><path fill="currentColor" d="M7 1h2v3h2l-3 3-3-3h2zM1 7h3V5l3 3-3 3V9H1zm11-2 3 3-3 3V9H9V7h3zM5 12l3-3 3 3H9v3H7v-3z"/></svg><span class="sr"></span></button>
|
|
34
35
|
<button id="btn-office" class="icon-btn"><svg viewBox="0 0 16 16" shape-rendering="crispEdges" aria-hidden="true"><path fill="currentColor" d="M7 1h2v2h2v1h2v2h-1v2h1v2h-2v2h-1v1H9v2H7v-2H5v-1H4v-2H3V8h1V6H3V4h2V3h2zm0 5v1H6v2h1v1h2V9h1V7H9V6z"/></svg><span class="sr"></span></button>
|
|
35
36
|
<button id="btn-lang" class="icon-btn lang"></button>
|
|
@@ -75,6 +76,9 @@
|
|
|
75
76
|
<script src="js/personal.js"></script>
|
|
76
77
|
<script src="js/rooms.js"></script>
|
|
77
78
|
<script src="js/player.js"></script>
|
|
79
|
+
<script src="js/work.js"></script>
|
|
80
|
+
<script src="js/views.js"></script>
|
|
81
|
+
<script src="js/fun2.js"></script>
|
|
78
82
|
<script src="js/main.js"></script>
|
|
79
83
|
</body>
|
|
80
84
|
</html>
|
|
@@ -42,7 +42,7 @@ function achFacts() {
|
|
|
42
42
|
skillsUsed: Math.max(0, ...cred.map(c => (c.topSkills || []).length)),
|
|
43
43
|
commits: achCount('commits'), pushes: achCount('pushes'), repos: achCount('repos'), branches: achCount('branches'), cleanDesk: achCount('cleanDesk'), talks: achCount('talks'),
|
|
44
44
|
sprints: achCount('sprints'), neon: achCount('neon'), tabPaper: achCount('tab_paper'), tabUsage: achCount('tab_usage'), tabToday: achCount('tab_today'), tabFeed: achCount('tab_feed'), tabShop: achCount('tab_shop'),
|
|
45
|
-
spent: prog.spent, moves: achCount('moves'), themesTried: achCount('themesTried'), midnight: achCount('midnight'),
|
|
45
|
+
spent: prog.spent, moves: achCount('moves'), missionsDone: achCount('missionsDone'), arcadeBest: achCount('arcadeBest'), surprises: achCount('surprises'), outfits: achCount('outfits'), shares: achCount('shares'), terraceVisits: achCount('terraceVisits'), serverVisits: achCount('serverVisits'), lifts: achCount('lifts'), quickReplies: achCount('quickReplies'), goals: achCount('goals'), launches: achCount('launches'), themesTried: achCount('themesTried'), midnight: achCount('midnight'),
|
|
46
46
|
dayHours: Math.max(0, ...vals.map(d => d.activeMs / 36e5)),
|
|
47
47
|
dayTools: Math.max(0, ...vals.map(d => d.tools)),
|
|
48
48
|
dayOut: Math.max(0, ...vals.map(d => d.output || 0)),
|
|
@@ -136,6 +136,17 @@ const ACH = [
|
|
|
136
136
|
{ id: 'historian', g: 'you', icon: '📜', fact: 'tabFeed', steps: [1, 10, 30, 100, 300] },
|
|
137
137
|
{ id: 'windowshop', g: 'you', icon: '🪟', fact: 'tabShop', steps: [1, 10, 30, 100, 300] },
|
|
138
138
|
{ id: 'spender', g: 'you', icon: '💸', fact: 'spent', steps: [200, 1000, 3000, 6000, 10000] },
|
|
139
|
+
{ id: 'quick', g: 'you', icon: '⚡', fact: 'quickReplies', steps: [1, 10, 50, 200, 500] },
|
|
140
|
+
{ id: 'goalgetter', g: 'you', icon: '🎯', fact: 'goals', steps: [1, 5, 20, 60, 150] },
|
|
141
|
+
{ id: 'founder', g: 'you', icon: '🧑💻', fact: 'launches', steps: [1, 5, 20, 60, 150] },
|
|
142
|
+
{ id: 'rooftop', g: 'you', icon: '🌇', fact: 'terraceVisits', steps: [1, 10, 30, 100, 300] },
|
|
143
|
+
{ id: 'sysadmin', g: 'you', icon: '🖧', fact: 'serverVisits', steps: [1, 10, 30, 100, 300] },
|
|
144
|
+
{ id: 'liftboy', g: 'you', icon: '🛗', fact: 'lifts', steps: [1, 10, 50, 150, 500] },
|
|
145
|
+
{ id: 'missionary', g: 'you', icon: '🎯', fact: 'missionsDone', steps: [1, 5, 15, 40, 100] },
|
|
146
|
+
{ id: 'gamer', g: 'you', icon: '🕹', fact: 'arcadeBest', steps: [10, 25, 50, 80, 120] },
|
|
147
|
+
{ id: 'lucky', g: 'you', icon: '🍀', fact: 'surprises', steps: [1, 10, 30, 100, 300] },
|
|
148
|
+
{ id: 'fashion', g: 'you', icon: '🎩', fact: 'outfits', steps: [1, 5, 20, 50, 100] },
|
|
149
|
+
{ id: 'sharer', g: 'you', icon: '📤', fact: 'shares', steps: [1, 5, 20, 50, 100] },
|
|
139
150
|
{ id: 'arranger', g: 'you', icon: '✥', fact: 'moves', steps: [1, 10, 30, 100, 300] },
|
|
140
151
|
{ id: 'themer', g: 'you', icon: '🖌', fact: 'themesTried', steps: [2, 5, 10, 20, 40] },
|
|
141
152
|
{ id: 'dj', g: 'you', icon: '📻', fact: 'radio', steps: [1, 5, 20, 50, 100] },
|
|
@@ -151,7 +162,7 @@ const ACH = [
|
|
|
151
162
|
const TIER_COINS = [0, 10, 20, 30, 40, 50];
|
|
152
163
|
const superDone = st => st.every(a => a.tier >= 5);
|
|
153
164
|
function coinsEarned(st = achState()) { return st.reduce((n, a) => n + TIER_COINS.slice(1, a.tier + 1).reduce((x, y) => x + y, 0), 0) + (superDone(st) ? 1000 : 0); }
|
|
154
|
-
const coins = () => Math.max(0, coinsEarned() - prog.spent);
|
|
165
|
+
const coins = () => Math.max(0, coinsEarned() + (Number(prog.record.missionCoins) || 0) + (Number(prog.record.eventCoins) || 0) - prog.spent);
|
|
155
166
|
|
|
156
167
|
function achState() {
|
|
157
168
|
const f = achFacts();
|
|
@@ -239,7 +250,7 @@ function renderAchievements() {
|
|
|
239
250
|
const legends = st.filter(a => a.tier >= 5).length, done = superDone(st);
|
|
240
251
|
const superCard = `<div class="ach super ${done ? 'on' : ''}"><span class="ach-ico">👑</span><div><b>${esc(A.superName)}</b><small>${esc(done ? A.tiers[5] : A.locked)}</small><p>${esc(done ? A.superDone : A.superDesc)}</p><div class="ach-bar"><i style="width:${(legends / st.length * 100).toFixed(1)}%"></i></div><div class="ach-foot"><span></span><span>${legends} / ${st.length}</span></div></div></div>`;
|
|
241
252
|
const wallet = `<div class="wallet"><span class="coin"></span><b>${coins().toLocaleString(lang === 'pt' ? 'pt-BR' : 'en')}</b> ${esc(A.coins)}<span class="note">${esc(A.coinsHow)}</span></div>`;
|
|
242
|
-
body.innerHTML = tabsHtml() + `${wallet}${superCard}<h3>${esc(A.aotm)} · ${esc(monthName)}</h3><p class="sub">${esc(A.aotmSub)}</p>${podium}
|
|
253
|
+
body.innerHTML = tabsHtml() + `${wallet}${missionsHtml()}${superCard}<h3>${esc(A.aotm)} · ${esc(monthName)}</h3><p class="sub">${esc(A.aotmSub)}</p>${podium}
|
|
243
254
|
<h3>${esc(A.title)} · ${got}/${all}</h3><p class="sub">${esc(A.sub)}</p>
|
|
244
255
|
${['history', 'live', 'you'].map(g => `<h4 class="ach-g">${esc(A.groups[g])}</h4><div class="achs">${st.filter(a => a.g === g).sort((x, y) => (x.secret && !x.tier) - (y.secret && !y.tier) || y.tier / y.max - x.tier / x.max || y.progress - x.progress).map(card).join('')}</div>`).join('')}`;
|
|
245
256
|
body.querySelectorAll('[data-face]').forEach(el => el.appendChild(Art.portrait(el.dataset.face)));
|
package/public/js/collection.js
CHANGED
|
@@ -32,7 +32,7 @@ function renderPaper() {
|
|
|
32
32
|
const st = achState(), legends = st.filter(a => a.tier >= 5).length;
|
|
33
33
|
const story = (h, p) => `<article><h4>${esc(h)}</h4><p>${esc(p)}</p></article>`;
|
|
34
34
|
const today = new Date().toLocaleDateString(loc, { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });
|
|
35
|
-
body.innerHTML = tabsHtml() + `<div class="paper">
|
|
35
|
+
body.innerHTML = tabsHtml() + `<div class="rhead"><span></span><span><button class="btn small" data-share-card>📤 ${esc(T.share.card)}</button> <button class="btn small" data-visit-photo>📷 ${esc(T.share.visit)}</button></span></div><div class="paper">
|
|
36
36
|
<header><small>${esc(today)}</small><h2>${esc(P.name)}</h2><small>${esc(P.edition(keys.length))}</small></header>
|
|
37
37
|
<div class="lead"><h3>${esc(P.headline(hrs(total)))}</h3><p>${esc(P.leadText(fmtK(tools), fmtK(out), keys.length))}</p></div>
|
|
38
38
|
<div class="cols">
|
|
@@ -40,6 +40,7 @@ function renderPaper() {
|
|
|
40
40
|
${top ? story(P.star((live && live.name) || top.title || top.id.slice(0, 8)), P.starText(hrs(top.activeMs), fmtK(top.tools))) : ''}
|
|
41
41
|
${night ? story(P.owls, P.owlsText(night)) : story(P.sleep, P.sleepText)}
|
|
42
42
|
${both ? story(P.duo, P.duoText(both)) : ''}
|
|
43
|
+
${scoreboardHtml()}
|
|
43
44
|
${story(P.trophies, P.trophiesText(st.reduce((n, a) => n + a.tier, 0), legends, coins()))}
|
|
44
45
|
${(() => { const k = new Date().toISOString().slice(0, 10), d = replies[k]; return d && d.n ? story(P.replies, P.repliesText(fmtDur(d.total / d.n), fmtDur(d.best))) : ''; })()}
|
|
45
46
|
</div>
|
|
@@ -48,7 +49,7 @@ function renderPaper() {
|
|
|
48
49
|
|
|
49
50
|
// ---- pets unlocked by achievements: a dog at 8 achievements at gold or better, a parrot at 16 ----
|
|
50
51
|
const goldCount = () => achState().filter(a => a.tier >= 3).length;
|
|
51
|
-
const dog = { x: 0, y: 0, tx: 0, ty: 0, until: 0, ready: false };
|
|
52
|
+
const dog = { x: 0, y: 0, tx: 0, ty: 0, until: 0, ready: false }, bunny = { x: 0, y: 0, tx: 0, ty: 0, until: 0, ready: false };
|
|
52
53
|
function drawPets(t, movers) {
|
|
53
54
|
const g = goldCount();
|
|
54
55
|
if (g >= 8 && prog.mascot !== 'dog') {
|
|
@@ -60,6 +61,13 @@ function drawPets(t, movers) {
|
|
|
60
61
|
movers.push({ y: dog.y, draw: () => drawDog(dog.x, dog.y, t, dog.flip, moving) });
|
|
61
62
|
}
|
|
62
63
|
if (g >= 16) drawParrot(RX + RW - 30, TOP - 26, t);
|
|
64
|
+
if (owns('bunnypet') && prog.mascot !== 'bunny') {
|
|
65
|
+
if (!bunny.ready) Object.assign(bunny, { x: RX + 60, y: TOP + 100, tx: RX + 60, ty: TOP + 100, ready: true });
|
|
66
|
+
const dx = bunny.tx - bunny.x, dy = bunny.ty - bunny.y, d = Math.hypot(dx, dy);
|
|
67
|
+
if (d > 1) { const hop = ((t / 200) | 0) % 2; bunny.x += dx / d * (hop ? 1.4 : 0); bunny.y += dy / d * (hop ? 1.4 : 0); bunny.flip = dx < 0; }
|
|
68
|
+
else if (Date.now() > bunny.until) { bunny.tx = RX + 10 + Math.random() * (RW - 30); bunny.ty = wing.ping - 20 + Math.random() * 16; bunny.until = Date.now() + 3000 + Math.random() * 5000; }
|
|
69
|
+
movers.push({ y: bunny.y, draw: () => drawCritter('bunny', bunny.x, bunny.y, d > 1 ? 'walk' : 'sit', t, bunny.flip) });
|
|
70
|
+
}
|
|
63
71
|
}
|
|
64
72
|
function drawDog(x, y, t, flip, moving) {
|
|
65
73
|
const f = moving ? ((t / 120) | 0) % 2 : 0, c = '#c98f5a', d = '#8a5a3a';
|
|
@@ -87,6 +95,11 @@ const SHOP = [
|
|
|
87
95
|
{ id: 'statue', price: 800, icon: '🗽' },
|
|
88
96
|
{ id: 'piano', price: 1200, icon: '🎹' },
|
|
89
97
|
{ id: 'fountain', price: 2000, icon: '⛲' },
|
|
98
|
+
{ id: 'turtle', price: 400, icon: '🐢' },
|
|
99
|
+
{ id: 'bunnypet', price: 600, icon: '🐇' },
|
|
100
|
+
{ id: 'bigtank', price: 900, icon: '🐠' },
|
|
101
|
+
{ id: 'cap', price: 150, icon: '🧢' }, { id: 'headphones', price: 300, icon: '🎧' }, { id: 'shades', price: 250, icon: '🕶' }, { id: 'party', price: 200, icon: '🥳' },
|
|
102
|
+
{ id: 'shirt_pink', price: 300, icon: '👕' }, { id: 'shirt_black', price: 300, icon: '👕' }, { id: 'shirt_gold', price: 400, icon: '👕' }, { id: 'crown', price: 1500, icon: '👑' },
|
|
90
103
|
];
|
|
91
104
|
const owns = id => prog.owned.includes(id);
|
|
92
105
|
function buy(id) {
|
|
@@ -103,7 +116,7 @@ function renderShop() {
|
|
|
103
116
|
${has ? `<small class="ok">${esc(S2.owned)}</small> <button class="btn small" data-move-item>${esc(S2.move)}</button>` : `<button class="btn small" data-buy="${it.id}" ${can ? '' : 'disabled'}><span class="coin"></span> ${it.price}</button>`}</div></div>`; }).join('')}</div>
|
|
104
117
|
${prog.owned.length ? `<p><button class="btn small" data-move-item>✥ ${esc(S2.decoTitle)}</button></p>` : ''}`;
|
|
105
118
|
}
|
|
106
|
-
reportEl.addEventListener('click', e => { const b = e.target.closest('[data-buy]'); if (b) buy(b.dataset.buy); if (e.target.closest('[data-move-item]')) setDecorating(true); });
|
|
119
|
+
reportEl.addEventListener('click', e => { if (e.target.closest('[data-share-card]')) return shareCard(); if (e.target.closest('[data-visit-photo]')) return visitPhoto(); const b = e.target.closest('[data-buy]'); if (b) buy(b.dataset.buy); if (e.target.closest('[data-move-item]')) setDecorating(true); });
|
|
107
120
|
|
|
108
121
|
// bought items, drawn in fixed places (floor items before the people, so people walk in front)
|
|
109
122
|
// ---- bought items: each one has a default spot and can be moved in decorate mode ----
|
|
@@ -118,6 +131,7 @@ const ITEMS = {
|
|
|
118
131
|
fountain: { w: 26, h: 20, at: () => ({ x: CX - 12, y: H - 72 }) },
|
|
119
132
|
neon: { w: 30, h: 8, at: () => ({ x: Math.min(W - 34, RX + 150 > W - 40 ? RX + 2 : RX + 150), y: 2 }) },
|
|
120
133
|
disco: { w: 8, h: 14, at: () => ({ x: CX - 3, y: TOP - 2 }) },
|
|
134
|
+
turtle: { w: 24, h: 16, at: () => ({ x: RX + 110, y: wing.copa + 30 }) },
|
|
121
135
|
};
|
|
122
136
|
function itemPos(id) {
|
|
123
137
|
const it = ITEMS[id], p = prog.placed && prog.placed[id];
|
|
@@ -125,6 +139,9 @@ function itemPos(id) {
|
|
|
125
139
|
return { x: Math.max(0, Math.min(W - it.w, pos.x)), y: Math.max(0, Math.min(H - it.h, pos.y)), w: it.w, h: it.h };
|
|
126
140
|
}
|
|
127
141
|
const ITEM_DRAW = {
|
|
142
|
+
turtle: (x, y, t) => { r(x - 1, y - 1, 26, 18, PAL.ink); r(x, y, 24, 16, '#bfe3f0'); r(x, y + 11, 24, 5, '#c9a86b'); r(x + 3, y + 8, 1, 4, '#3a8f46'); r(x + 19, y + 7, 1, 5, '#3a8f46');
|
|
143
|
+
const tx = x + 6 + Math.round((Math.sin(t / 3000) + 1) * 4); r(tx, y + 8, 8, 4, '#3a6a2a'); r(tx + 1, y + 7, 6, 1, '#5a8a3a'); r(tx + 8, y + 9, 2, 2, '#7aa04a'); r(tx + 1, y + 12, 1, 1, '#7aa04a'); r(tx + 6, y + 12, 1, 1, '#7aa04a');
|
|
144
|
+
ctx.fillStyle = 'rgba(255,255,255,.18)'; ctx.fillRect(x + 2, y + 1, 2, 9); },
|
|
128
145
|
rug: (x, y) => drawPersianRug(x, y + 3, 76, 44),
|
|
129
146
|
plants: (x, y) => { Art.drawPlant(x, y + 2, true); Art.drawPlant(x + 13, y, true); Art.drawPlant(x + 26, y + 3, false); },
|
|
130
147
|
lamps: (x, y, t, glows) => { for (const lx of [x, x + 16]) { r(lx + 3, y + 4, 2, 14, PAL.ink); r(lx, y, 8, 5, '#ffd84d'); glows.push({ x: lx + 4, y: y + 2, r: 22, c: '#ffd84d' }); } },
|
|
@@ -199,7 +216,7 @@ const partyBounce = (id, t) => partying() ? -Math.abs(Math.round(Math.sin(t / 11
|
|
|
199
216
|
// ---- group photo: the office canvas as a PNG ----
|
|
200
217
|
function groupPhoto() {
|
|
201
218
|
const cv = document.getElementById('cv');
|
|
202
|
-
achBump('photos');
|
|
219
|
+
achBump('photos'); savePhotoThumb();
|
|
203
220
|
cv.toBlob(b => {
|
|
204
221
|
if (!b) return;
|
|
205
222
|
const a = document.createElement('a'), d = new Date();
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// ---------------- weekly missions, your outfit, the arcade game, surprise events, the AI scoreboard, sharing ----------------
|
|
3
|
+
|
|
4
|
+
// ---- weekly missions: three per week, picked by the week number, measured; 100 coins each ----
|
|
5
|
+
const weekKey = (d = new Date()) => { const j = new Date(d.getFullYear(), 0, 1); return d.getFullYear() + '-W' + Math.floor((d - j) / 6048e5 + 1); };
|
|
6
|
+
const weekDays = () => { const now = new Date(), out = []; const dow = (now.getDay() + 6) % 7; for (let i = 0; i <= dow; i++) { const d = new Date(now - (dow - i) * 864e5); out.push(d.toISOString().slice(0, 10)); } return out; };
|
|
7
|
+
// counters measured since the week began: the value at the first look this week is the baseline
|
|
8
|
+
function weekDelta(k) {
|
|
9
|
+
const wk = weekKey(), base = store.get('missionBase', null);
|
|
10
|
+
if (!base || base.week !== wk) { const b = { week: wk, v: {} }; for (const c of ['commits', 'pushes', 'talks', 'quickReplies', 'pets', 'fast', 'celebrations', 'trophyVisits']) b.v[c] = achCount(c); store.set('missionBase', b); return 0; }
|
|
11
|
+
return Math.max(0, achCount(k) - (base.v[k] || 0));
|
|
12
|
+
}
|
|
13
|
+
const MISSIONS = [
|
|
14
|
+
{ id: 'hours', goal: 20, value: () => weekDays().reduce((n, k) => n + ((usageData && usageData.days && usageData.days[k] && usageData.days[k].activeMs) || 0), 0) / 36e5 },
|
|
15
|
+
{ id: 'tools', goal: 3000, value: () => weekDays().reduce((n, k) => n + ((usageData && usageData.days && usageData.days[k] && usageData.days[k].tools) || 0), 0) },
|
|
16
|
+
{ id: 'days', goal: 4, value: () => weekDays().filter(k => usageData && usageData.days && usageData.days[k] && usageData.days[k].tools >= 10).length },
|
|
17
|
+
{ id: 'commits', goal: 5, value: () => weekDelta('commits') },
|
|
18
|
+
{ id: 'pushes', goal: 3, value: () => weekDelta('pushes') },
|
|
19
|
+
{ id: 'talks', goal: 10, value: () => weekDelta('talks') },
|
|
20
|
+
{ id: 'quick', goal: 5, value: () => weekDelta('quickReplies') },
|
|
21
|
+
{ id: 'pets', goal: 10, value: () => weekDelta('pets') },
|
|
22
|
+
{ id: 'fast', goal: 5, value: () => weekDelta('fast') },
|
|
23
|
+
{ id: 'deliver', goal: 5, value: () => weekDelta('celebrations') },
|
|
24
|
+
];
|
|
25
|
+
function weekMissions() {
|
|
26
|
+
const wk = weekKey(), h = hash(wk), pool = MISSIONS.slice(), out = [];
|
|
27
|
+
for (let i = 0; i < 3; i++) out.push(pool.splice((h >>> (i * 5)) % pool.length, 1)[0]);
|
|
28
|
+
return out.map(m => { const v = m.value(), done = achCount('missionsDone') && (prog.record.missionsDone || []).includes(wk + ':' + m.id); return { ...m, v, done: done || v >= m.goal, claimed: done, key: wk + ':' + m.id }; });
|
|
29
|
+
}
|
|
30
|
+
function checkMissions() {
|
|
31
|
+
if (!usageData || !usageData.days) return;
|
|
32
|
+
for (const m of weekMissions()) if (!m.claimed && m.v >= m.goal) {
|
|
33
|
+
achAdd('missionsDone', m.key); achBump('missionCoins', 100);
|
|
34
|
+
toast(`<b>${esc(T.missions.done)}</b>${esc(T.missions.list[m.id](m.goal))} · +100 ${esc(T.ach.coins)}`, 'ok'); playTune('trophy');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
setInterval(checkMissions, 30000);
|
|
38
|
+
function missionsHtml() {
|
|
39
|
+
const M = T.missions, list = weekMissions();
|
|
40
|
+
return `<h3>${esc(M.title)}</h3><p class="sub">${esc(M.sub)}</p><div class="achs">${list.map(m => `<div class="ach ${m.done ? 'on' : ''}" style="--c:${m.done ? '#63c74d' : '#ffd84d'}"><span class="ach-ico">${m.done ? '✓' : '🎯'}</span><div><b>${esc(M.list[m.id](m.goal))}</b><small>${esc(m.done ? M.claimed : '+100 ' + T.ach.coins)}</small>
|
|
41
|
+
<div class="ach-bar"><i style="width:${Math.min(100, m.v / m.goal * 100).toFixed(1)}%"></i></div><div class="ach-foot"><span></span><span>${Math.floor(Math.min(m.v, m.goal))} / ${m.goal}</span></div></div></div>`).join('')}</div>`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ---- your outfit: hats, shades and shirts from the shop, worn by your character ----
|
|
45
|
+
const OUTFIT = { cap: 'hat', crown: 'hat', headphones: 'hat', party: 'hat', shades: 'face', shirt_gold: 'shirt', shirt_pink: 'shirt', shirt_black: 'shirt' };
|
|
46
|
+
const SHIRTS = { shirt_gold: '#d9a441', shirt_pink: '#e8579a', shirt_black: '#2a2630' };
|
|
47
|
+
function wearing(slot) { const w = (prog.style || {})['wear_' + slot]; return w && owns(w) ? w : ''; }
|
|
48
|
+
function youLook() {
|
|
49
|
+
const lk = Art.look('you:' + (avatars.__you || 0)), sh = wearing('shirt');
|
|
50
|
+
return sh ? { ...lk, c: SHIRTS[sh], C: Art.shade(SHIRTS[sh], .72) } : lk;
|
|
51
|
+
}
|
|
52
|
+
function drawOutfit(x, y) { // over the standing sprite's head (top at y - 2)
|
|
53
|
+
const hat = wearing('hat');
|
|
54
|
+
if (hat === 'cap') { r(x + 2, y - 3, 12, 4, PAL.ink); r(x + 3, y - 2, 10, 2, '#e43b44'); r(x + 10, y, 7, 2, PAL.ink); r(x + 11, y, 5, 1, '#e43b44'); }
|
|
55
|
+
if (hat === 'crown') { r(x + 3, y - 4, 10, 4, '#ffd84d'); r(x + 3, y - 6, 2, 2, '#ffd84d'); r(x + 7, y - 7, 2, 3, '#ffd84d'); r(x + 11, y - 6, 2, 2, '#ffd84d'); r(x + 7, y - 3, 2, 1, '#e43b44'); }
|
|
56
|
+
if (hat === 'headphones') { r(x + 1, y - 3, 14, 2, PAL.ink); r(x, y + 3, 3, 5, '#3b5dc9'); r(x + 13, y + 3, 3, 5, '#3b5dc9'); }
|
|
57
|
+
if (hat === 'party') Art.drawHat('party', x, y - 2);
|
|
58
|
+
if (wearing('face') === 'shades') { r(x + 3, y + 5, 10, 2, PAL.ink); r(x + 4, y + 5, 3, 1, '#3a4566'); r(x + 9, y + 5, 3, 1, '#3a4566'); }
|
|
59
|
+
}
|
|
60
|
+
function outfitHtml() {
|
|
61
|
+
const O = T.outfit, have = Object.keys(OUTFIT).filter(owns);
|
|
62
|
+
if (!have.length) return `<p class="note">${esc(O.none)}</p>`;
|
|
63
|
+
const slot = s => `<div class="opts"><button class="opt ${!wearing(s) ? 'on' : ''}" data-wear="${s}" data-item="">${esc(O.nothing)}</button>${have.filter(i => OUTFIT[i] === s).map(i => `<button class="opt ${wearing(s) === i ? 'on' : ''}" data-wear="${s}" data-item="${i}">${esc(T.shop.items[i].name)}</button>`).join('')}</div>`;
|
|
64
|
+
return ['hat', 'face', 'shirt'].filter(s => have.some(i => OUTFIT[i] === s)).map(s => `<small class="note">${esc(O.slots[s])}</small>${slot(s)}`).join('');
|
|
65
|
+
}
|
|
66
|
+
reportEl.addEventListener('click', e => {
|
|
67
|
+
const w = e.target.closest('[data-wear]'); if (!w) return;
|
|
68
|
+
prog.style = { ...(prog.style || {}), ['wear_' + w.dataset.wear]: w.dataset.item }; saveProgress(); achBump('outfits');
|
|
69
|
+
reportEl.querySelectorAll(`[data-wear="${w.dataset.wear}"]`).forEach(b => b.classList.toggle('on', b === w));
|
|
70
|
+
const f = document.getElementById('you-face'); if (f) { f.innerHTML = ''; f.appendChild(youPortrait()); }
|
|
71
|
+
});
|
|
72
|
+
function youPortrait() { // the portrait with the outfit drawn on top
|
|
73
|
+
const c = Art.portrait('you:' + (avatars.__you || 0)), g = c.getContext('2d'), prev = ctx;
|
|
74
|
+
const sh = wearing('shirt'); if (sh) { g.fillStyle = SHIRTS[sh]; g.fillRect(1, 12, 14, 6); }
|
|
75
|
+
ctx = g; Art.setCtx(g); try { drawOutfit(0, 2); } finally { ctx = prev; Art.setCtx(prev); }
|
|
76
|
+
return c;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---- the arcade: Bug Catcher. Catch falling bugs with your laptop; 3 lives, gentle start, speeds up slowly ----
|
|
80
|
+
const arcadeEl = document.createElement('div');
|
|
81
|
+
arcadeEl.className = 'trophy-card'; arcadeEl.hidden = true;
|
|
82
|
+
document.body.appendChild(arcadeEl);
|
|
83
|
+
const AW = 240, AH = 160;
|
|
84
|
+
let game2 = null;
|
|
85
|
+
function openArcade() {
|
|
86
|
+
arcadeEl.innerHTML = `<div class="tc-box arcade-box"><div><b>🕹 ${esc(T.arcade.title)}</b><p>${esc(T.arcade.help)}</p><canvas width="${AW}" height="${AH}" class="arcade-cv"></canvas>
|
|
87
|
+
<div class="ach-foot"><span id="arcade-score"></span><span>${esc(T.arcade.best)}: <b id="arcade-best">${Number(store.get('arcadeBest', 0)) || 0}</b></span></div><button class="btn small" data-arcade-start>▶ ${esc(T.arcade.start)}</button></div></div>`;
|
|
88
|
+
arcadeEl.hidden = false; achBump('arcadeOpens');
|
|
89
|
+
drawArcadeTitle();
|
|
90
|
+
}
|
|
91
|
+
function drawArcadeTitle() {
|
|
92
|
+
const c = arcadeEl.querySelector('.arcade-cv'); if (!c) return;
|
|
93
|
+
const g = c.getContext('2d'); arcadeBg(g, performance.now());
|
|
94
|
+
g.fillStyle = '#ffd84d'; g.font = 'bold 18px monospace'; g.textAlign = 'center'; g.fillText('BUG CATCHER', AW / 2, 62);
|
|
95
|
+
g.fillStyle = '#c0cbdc'; g.font = '10px monospace'; g.fillText(T.arcade.press, AW / 2, 86); g.textAlign = 'left';
|
|
96
|
+
drawBug(g, AW / 2 - 30, 100, false, 0); drawBug(g, AW / 2 - 4, 104, true, 1); drawBug(g, AW / 2 + 22, 100, false, 0);
|
|
97
|
+
}
|
|
98
|
+
function arcadeBg(g, now) {
|
|
99
|
+
g.fillStyle = '#141826'; g.fillRect(0, 0, AW, AH);
|
|
100
|
+
g.fillStyle = '#1c2236'; for (let y = 0; y < AH; y += 8) { const sh = (y * 13) % 70; g.fillRect(6 + sh % 20, (y + now / 60) % AH, 30 + sh, 2); } // code scrolling behind
|
|
101
|
+
g.fillStyle = '#2a2f45'; g.fillRect(0, AH - 8, AW, 8);
|
|
102
|
+
}
|
|
103
|
+
function drawBug(g, x, y, gold, f) {
|
|
104
|
+
const body = gold ? '#ffd84d' : '#63c74d', dark = gold ? '#b07d2a' : '#2f7a3b';
|
|
105
|
+
g.fillStyle = '#1b1622'; for (const dx of [-2, 9]) for (const dy of [2, 5]) g.fillRect(x + dx + (f && dx < 0 ? -1 : 0), y + dy + (f ? 1 : 0), 3, 1);
|
|
106
|
+
g.fillStyle = dark; g.fillRect(x, y, 10, 8); g.fillStyle = body; g.fillRect(x + 1, y + 1, 8, 6); g.fillStyle = dark; g.fillRect(x + 4, y + 1, 2, 6);
|
|
107
|
+
g.fillStyle = '#1b1622'; g.fillRect(x + 2, y - 2, 6, 3); g.fillRect(x + 2, y - 4, 1, 2); g.fillRect(x + 7, y - 4, 1, 2);
|
|
108
|
+
if (gold) { g.fillStyle = '#ffffff'; g.fillRect(x + 2, y + 2, 1, 1); }
|
|
109
|
+
}
|
|
110
|
+
function startArcade() {
|
|
111
|
+
const cv2 = arcadeEl.querySelector('.arcade-cv'); if (!cv2) return;
|
|
112
|
+
game2 = { cv: cv2, g: cv2.getContext('2d'), x: AW / 2 - 14, bugs: [], sparks: [], score: 0, lives: 3, combo: 0, t0: performance.now(), last: performance.now(), keys: new Set(), spawn: 0 };
|
|
113
|
+
requestAnimationFrame(tickArcade);
|
|
114
|
+
}
|
|
115
|
+
function tickArcade(now) {
|
|
116
|
+
const G = game2; if (!G || arcadeEl.hidden) { game2 = null; return; }
|
|
117
|
+
const dt = Math.min(.05, (now - G.last) / 1000), age = (now - G.t0) / 1000; G.last = now;
|
|
118
|
+
if (G.keys.has('ArrowLeft') || G.keys.has('a')) G.x -= 190 * dt; if (G.keys.has('ArrowRight') || G.keys.has('d')) G.x += 190 * dt;
|
|
119
|
+
G.x = Math.max(0, Math.min(AW - 28, G.x));
|
|
120
|
+
// gentle start: one bug every ~1.4s, slowly more and faster
|
|
121
|
+
G.spawn -= dt; if (G.spawn <= 0) { G.spawn = Math.max(.45, 1.4 - age * .02); G.bugs.push({ x: 6 + Math.random() * (AW - 20), y: -8, v: 26 + Math.min(70, age * 1.6) + Math.random() * 10, gold: Math.random() < .12, f: 0 }); }
|
|
122
|
+
const g = G.g; arcadeBg(g, now);
|
|
123
|
+
for (const b of G.bugs) {
|
|
124
|
+
b.y += b.v * dt; b.f = ((now / 160) | 0) % 2;
|
|
125
|
+
drawBug(g, b.x, b.y, b.gold, b.f);
|
|
126
|
+
if (b.y + 8 >= AH - 22 && b.y <= AH - 14 && b.x + 10 > G.x && b.x < G.x + 28) {
|
|
127
|
+
b.hit = true; G.combo++; const pts = (b.gold ? 5 : 1) * (G.combo >= 5 ? 2 : 1); G.score += pts; beep(b.gold);
|
|
128
|
+
for (let i = 0; i < 8; i++) G.sparks.push({ x: b.x + 5, y: b.y + 4, vx: (Math.random() - .5) * 60, vy: -Math.random() * 50, life: .5, c: b.gold ? '#ffd84d' : '#63c74d', txt: i ? '' : '+' + pts });
|
|
129
|
+
} else if (b.y > AH - 8) { b.miss = true; G.lives--; G.combo = 0; }
|
|
130
|
+
}
|
|
131
|
+
G.bugs = G.bugs.filter(b => !b.hit && !b.miss);
|
|
132
|
+
for (const s of G.sparks) { s.x += s.vx * dt; s.y += s.vy * dt; s.vy += 80 * dt; s.life -= dt; g.fillStyle = s.c; if (s.txt) { g.font = 'bold 9px monospace'; g.fillText(s.txt, s.x, s.y); } else g.fillRect(s.x, s.y, 2, 2); }
|
|
133
|
+
G.sparks = G.sparks.filter(s => s.life > 0);
|
|
134
|
+
// the laptop paddle
|
|
135
|
+
const px = Math.round(G.x), py = AH - 18;
|
|
136
|
+
g.fillStyle = '#1b1622'; g.fillRect(px - 1, py - 9, 30, 19); g.fillStyle = '#c0cbdc'; g.fillRect(px + 2, py - 8, 24, 10); g.fillStyle = '#2ce8f5'; g.fillRect(px + 4, py - 6, 20, 6);
|
|
137
|
+
g.fillStyle = '#8b9bb4'; g.fillRect(px, py + 2, 28, 4); g.fillStyle = '#d97757'; g.fillRect(px + 11, py - 4, 6, 2);
|
|
138
|
+
// HUD: hearts, score, combo
|
|
139
|
+
for (let i = 0; i < 3; i++) { g.fillStyle = i < G.lives ? '#e43b44' : '#3a3448'; g.fillRect(6 + i * 10, 6, 3, 2); g.fillRect(10 + i * 10, 6, 3, 2); g.fillRect(6 + i * 10, 8, 7, 2); g.fillRect(7 + i * 10, 10, 5, 1); g.fillRect(8 + i * 10, 11, 3, 1); }
|
|
140
|
+
g.fillStyle = '#ffffff'; g.font = 'bold 10px monospace'; g.textAlign = 'right'; g.fillText(String(G.score), AW - 6, 14); g.textAlign = 'left';
|
|
141
|
+
if (G.combo >= 5) { g.fillStyle = '#ff6ec7'; g.font = 'bold 9px monospace'; g.fillText('COMBO x2', AW / 2 - 22, 14); }
|
|
142
|
+
const sc = arcadeEl.querySelector('#arcade-score'); if (sc) sc.textContent = `${T.arcade.score}: ${G.score}`;
|
|
143
|
+
if (G.lives <= 0) {
|
|
144
|
+
const best = Number(store.get('arcadeBest', 0)) || 0;
|
|
145
|
+
if (G.score > best) { store.set('arcadeBest', G.score); toast(`<b>${esc(T.arcade.record)}</b>${G.score}`, 'ok'); const be = arcadeEl.querySelector('#arcade-best'); if (be) be.textContent = G.score; }
|
|
146
|
+
achRecord.arcadeBest = Math.max(achRecord.arcadeBest || 0, G.score); achBump('arcadeGames');
|
|
147
|
+
g.fillStyle = 'rgba(0,0,0,.65)'; g.fillRect(0, 56, AW, 46); g.fillStyle = '#ffd84d'; g.font = 'bold 14px monospace'; g.textAlign = 'center'; g.fillText(T.arcade.over, AW / 2, 76); g.fillStyle = '#ffffff'; g.font = '10px monospace'; g.fillText(`${T.arcade.score}: ${G.score}`, AW / 2, 92); g.textAlign = 'left';
|
|
148
|
+
game2 = null; return;
|
|
149
|
+
}
|
|
150
|
+
requestAnimationFrame(tickArcade);
|
|
151
|
+
}
|
|
152
|
+
arcadeEl.addEventListener('click', e => { if (e.target === arcadeEl) { arcadeEl.hidden = true; game2 = null; } if (e.target.closest('[data-arcade-start]')) startArcade(); });
|
|
153
|
+
document.addEventListener('keydown', e => {
|
|
154
|
+
if (arcadeEl.hidden) return;
|
|
155
|
+
if (!game2 && (e.key === ' ' || e.key === 'Enter')) { e.preventDefault(); e.stopImmediatePropagation(); return startArcade(); }
|
|
156
|
+
if (!game2) return;
|
|
157
|
+
const k = e.key.length === 1 ? e.key.toLowerCase() : e.key;
|
|
158
|
+
if (['ArrowLeft', 'ArrowRight', 'a', 'd'].includes(k)) { game2.keys.add(k); e.preventDefault(); e.stopImmediatePropagation(); }
|
|
159
|
+
if (e.key === 'Escape') { arcadeEl.hidden = true; game2 = null; }
|
|
160
|
+
}, true);
|
|
161
|
+
document.addEventListener('keyup', e => { if (game2) { game2.keys.delete(e.key); game2.keys.delete(e.key.toLowerCase()); } });
|
|
162
|
+
arcadeEl.addEventListener('pointermove', e => { if (!game2) return; const rc = game2.cv.getBoundingClientRect(); game2.x = (e.clientX - rc.left) / rc.width * AW - 14; });
|
|
163
|
+
|
|
164
|
+
// ---- surprise events: now and then something appears; click it for coins ----
|
|
165
|
+
let surprise = null; // { kind, x, y, until, coins }
|
|
166
|
+
function maybeSurprise() {
|
|
167
|
+
if (surprise || !data || trophyView || document.hidden || Math.random() > .25) return;
|
|
168
|
+
const kinds = [
|
|
169
|
+
{ kind: 'cake', x: RX + 90, y: TOP - 12, coins: 10 },
|
|
170
|
+
{ kind: 'pigeon', x: windowBoxes.length ? windowBoxes[(Math.random() * windowBoxes.length) | 0].x + 20 : CX - 40, y: 30, coins: 5 },
|
|
171
|
+
{ kind: 'ticket', x: CX - 4, y: TOP + 40 + Math.random() * (H - TOP - 120), coins: 25 },
|
|
172
|
+
];
|
|
173
|
+
surprise = { ...kinds[(Math.random() * kinds.length) | 0], until: Date.now() + 90000 };
|
|
174
|
+
}
|
|
175
|
+
setInterval(maybeSurprise, 5 * 60000);
|
|
176
|
+
function drawSurprise(t) {
|
|
177
|
+
if (!surprise) return; if (Date.now() > surprise.until) { surprise = null; return; }
|
|
178
|
+
const { x, y, kind } = surprise, b = ((t / 300) | 0) % 2;
|
|
179
|
+
if (kind === 'cake') { r(x, y + 4, 12, 6, '#f4dfc6'); r(x, y + 4, 12, 2, '#ff6ec7'); r(x + 5, y, 1, 4, '#2ce8f5'); if (b) r(x + 5, y - 2, 1, 2, '#ffd84d'); }
|
|
180
|
+
if (kind === 'pigeon') { r(x, y + 2, 8, 5, '#9aa6b8'); r(x + 6, y, 4, 4, '#9aa6b8'); r(x + 9, y + 1, 2, 1, '#feae34'); r(x + 7, y + 1, 1, 1, PAL.ink); r(x + 2, y + 7, 1, 2 - b, '#feae34'); r(x + 5, y + 7, 1, 1 + b, '#feae34'); }
|
|
181
|
+
if (kind === 'ticket') { r(x - 1, y - 1, 12, 8, PAL.ink); r(x, y, 10, 6, '#ffd84d'); r(x + 2, y + 2, 6, 1, '#b07d2a'); if (b) r(x + 9, y - 2, 1, 1, '#ffffff'); }
|
|
182
|
+
surpriseBox = { x: x - 4, y: y - 4, w: 20, h: 16 };
|
|
183
|
+
}
|
|
184
|
+
let surpriseBox = null;
|
|
185
|
+
function claimSurprise() {
|
|
186
|
+
if (!surprise) return;
|
|
187
|
+
achBump('eventCoins', surprise.coins); achBump('surprises');
|
|
188
|
+
toast(`<b>${esc(T.surprise[surprise.kind])}</b>+${surprise.coins} ${esc(T.ach.coins)}`, 'ok'); playTune('done');
|
|
189
|
+
surprise = null; surpriseBox = null; lastHits = ''; renderOverlay();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ---- the AI scoreboard: this week's hours, actions and tokens per AI ----
|
|
193
|
+
function scoreboardHtml() {
|
|
194
|
+
const S2 = T.scoreboard, days = weekDays(), sum = {};
|
|
195
|
+
for (const k of days) { const d = usageData && usageData.days && usageData.days[k]; if (!d || !d.byAgent) continue; for (const [a, v] of Object.entries(d.byAgent)) { const s = sum[a] || (sum[a] = { activeMs: 0, tools: 0, output: 0 }); s.activeMs += v.activeMs; s.tools += v.tools; s.output += v.output || 0; } }
|
|
196
|
+
const rows = Object.entries(sum).sort((a, b) => b[1].activeMs - a[1].activeMs);
|
|
197
|
+
if (!rows.length) return '';
|
|
198
|
+
return `<article><h4>${esc(S2.title)}</h4><table class="score"><tr><th></th><th>${esc(S2.hours)}</th><th>${esc(S2.actions)}</th><th>${esc(S2.tokens)}</th></tr>${rows.map(([a, s], i) => `<tr><td>${i ? '' : '🥇 '}${esc((AGENT[a] || {}).label || a)}</td><td>${(s.activeMs / 36e5).toFixed(1)}h</td><td>${fmtK(s.tools)}</td><td>${fmtK(s.output)}</td></tr>`).join('')}</table></article>`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ---- share card: a picture of your week, ready to post ----
|
|
202
|
+
function shareCard() {
|
|
203
|
+
const c = document.createElement('canvas'); c.width = 600; c.height = 315;
|
|
204
|
+
const g = c.getContext('2d'), days = weekDays(), d = k => (usageData && usageData.days && usageData.days[k]) || {};
|
|
205
|
+
const hours = days.reduce((n, k) => n + (d(k).activeMs || 0), 0) / 36e5, tools = days.reduce((n, k) => n + (d(k).tools || 0), 0);
|
|
206
|
+
const st = achState(), tiers = st.reduce((n, a) => n + a.tier, 0), legends = st.filter(a => a.tier >= 5).length;
|
|
207
|
+
g.fillStyle = '#1b1622'; g.fillRect(0, 0, 600, 315);
|
|
208
|
+
g.drawImage(document.getElementById('cv'), 0, 0, Math.min(W, 380), Math.min(H, 220), 300, 40, 280, 170 * Math.min(1, Math.min(H, 220) / 220));
|
|
209
|
+
g.fillStyle = '#ffd84d'; g.font = 'bold 26px system-ui, sans-serif'; g.fillText(prog.officeName || 'coworking-agents', 24, 52);
|
|
210
|
+
g.fillStyle = '#c0cbdc'; g.font = '14px system-ui, sans-serif'; g.fillText(T.share.week, 24, 76);
|
|
211
|
+
const stat = (y, big, label) => { g.fillStyle = '#ffffff'; g.font = 'bold 30px system-ui, sans-serif'; g.fillText(big, 24, y); g.fillStyle = '#9a9187'; g.font = '13px system-ui, sans-serif'; g.fillText(label, 24, y + 18); };
|
|
212
|
+
stat(126, hours.toFixed(1) + 'h', T.share.hours); stat(186, fmtK(tools), T.share.actions); stat(246, `${tiers} · ${legends}★`, T.share.trophies);
|
|
213
|
+
g.fillStyle = '#d97757'; g.fillRect(0, 290, 600, 25); g.fillStyle = '#ffffff'; g.font = 'bold 13px system-ui, sans-serif'; g.fillText('npx coworking-agents', 24, 307);
|
|
214
|
+
c.toBlob(b => { if (!b) return; const a = document.createElement('a'); a.href = URL.createObjectURL(b); a.download = `coworking-week-${weekKey()}.png`; document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(a.href), 2000); toast(`<b>${esc(T.share.saved)}</b>${esc(a.download)}`, 'ok'); achBump('shares'); }, 'image/png');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ---- a photo for visitors: the office without names, branches or the office name ----
|
|
218
|
+
let snapshotMode = false;
|
|
219
|
+
function visitPhoto() {
|
|
220
|
+
snapshotMode = true;
|
|
221
|
+
try { drawScene(performance.now(), 0); const cv = document.getElementById('cv');
|
|
222
|
+
cv.toBlob(b => { if (!b) return; const a = document.createElement('a'); a.href = URL.createObjectURL(b); a.download = `coworking-visit-${new Date().toISOString().slice(0, 10)}.png`; document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(a.href), 2000); toast(`<b>${esc(T.share.visitSaved)}</b>${esc(T.share.visitNote)}`, 'ok'); achBump('shares'); }, 'image/png');
|
|
223
|
+
} finally { snapshotMode = false; }
|
|
224
|
+
}
|