coworking-agents 0.2.1 → 0.2.2

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 CHANGED
@@ -24,7 +24,7 @@
24
24
  | ![Panel](https://raw.githubusercontent.com/ChaconLucas/coworking-agents/main/docs/panel.png) | **Agent panel.** Now, last request and reply, last-hour timeline, today's numbers, tokens, context, files, subagents, certificates. |
25
25
  | ![Usage](https://raw.githubusercontent.com/ChaconLucas/coworking-agents/main/docs/usage.png) | **Usage report.** Tokens per day for 14 days, all-time totals per AI and per model, rate-limit windows with reset times. |
26
26
  | ![Work site](https://raw.githubusercontent.com/ChaconLucas/coworking-agents/main/docs/work-site.png) | **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](https://raw.githubusercontent.com/ChaconLucas/coworking-agents/main/docs/achievements.png) | **Achievements.** 74 of them, five tiers each, coins for every tier, the agent of the month on top. |
27
+ | ![Achievements](https://raw.githubusercontent.com/ChaconLucas/coworking-agents/main/docs/achievements.png) | **Achievements.** 75 of them, five tiers each, coins for every tier, the agent of the month on top. |
28
28
  | ![Paper](https://raw.githubusercontent.com/ChaconLucas/coworking-agents/main/docs/paper.png) | **The Office Times.** Your week in headlines, written from measured history. |
29
29
  | ![Trophy room](https://raw.githubusercontent.com/ChaconLucas/coworking-agents/main/docs/trophy-room.png) | **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
  | ![Christmas](https://raw.githubusercontent.com/ChaconLucas/coworking-agents/main/docs/christmas.png) | **Holidays.** Lights, snow and Santa hats at Christmas; pumpkins, bats and witch hats at Halloween; flags at Festa Junina. |
@@ -145,9 +145,9 @@ A marble portal leads to the **trophy room**, its own screen: legends with wings
145
145
 
146
146
  ## Achievements, coins and the shop
147
147
 
148
- - **74 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.
148
+ - **75 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
149
  - **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.
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).
151
151
  - **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
152
  - **The weekly paper**, *The Office Times*, tells your week in headlines, all from measured data.
153
153
  - **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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coworking-agents",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
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/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-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>
33
34
  <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>
34
35
  <button id="btn-lang" class="icon-btn lang"></button>
35
36
  </div>
@@ -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, themesTried: achCount('themesTried'), midnight: achCount('midnight'),
45
+ spent: prog.spent, moves: achCount('moves'), 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,7 @@ 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: 'arranger', g: 'you', icon: '✥', fact: 'moves', steps: [1, 10, 30, 100, 300] },
139
140
  { id: 'themer', g: 'you', icon: '🖌', fact: 'themesTried', steps: [2, 5, 10, 20, 40] },
140
141
  { id: 'dj', g: 'you', icon: '📻', fact: 'radio', steps: [1, 5, 20, 50, 100] },
141
142
  { id: 'cat', g: 'you', icon: '♥', fact: 'pets', steps: [1, 10, 50, 200, 1000] },
@@ -100,23 +100,84 @@ function renderShop() {
100
100
  const S2 = T.shop, body = reportEl.querySelector('.report-body'), c = coins();
101
101
  body.innerHTML = tabsHtml() + `<div class="wallet"><span class="coin"></span><b>${c.toLocaleString(lang === 'pt' ? 'pt-BR' : 'en')}</b> ${esc(T.ach.coins)}<span class="note">${esc(S2.sub)}</span></div>
102
102
  <div class="shop">${SHOP.map(it => { const has = owns(it.id), can = c >= it.price; return `<div class="item ${has ? 'owned' : ''}"><span class="ach-ico">${it.icon}</span><div><b>${esc(S2.items[it.id].name)}</b><p>${esc(S2.items[it.id].desc)}</p>
103
- ${has ? `<small class="ok">${esc(S2.owned)}</small>` : `<button class="btn small" data-buy="${it.id}" ${can ? '' : 'disabled'}><span class="coin"></span> ${it.price}</button>`}</div></div>`; }).join('')}</div>`;
103
+ ${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
+ ${prog.owned.length ? `<p><button class="btn small" data-move-item>✥ ${esc(S2.decoTitle)}</button></p>` : ''}`;
104
105
  }
105
- reportEl.addEventListener('click', e => { const b = e.target.closest('[data-buy]'); if (b) buy(b.dataset.buy); });
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); });
106
107
 
107
108
  // bought items, drawn in fixed places (floor items before the people, so people walk in front)
108
- function drawShopFloor(t, glows) {
109
- if (owns('plants')) for (let y = TOP + 140; y < H - 40; y += 150) Art.drawPlant(CX - 20, y, true);
110
- if (owns('lamps')) for (const lx of [RX + 4, RX + RW - 12]) { r(lx + 3, wing.ping - 4, 2, 14, PAL.ink); r(lx, wing.ping - 8, 8, 5, '#ffd84d'); glows.push({ x: lx + 4, y: wing.ping - 6, r: 22, c: '#ffd84d' }); }
111
- if (owns('statue')) { const m = usageData && usageData.month && usageData.month.top && usageData.month.top[0], x = RX + RW - 26, y = wing.ping + 34;
112
- r(x - 1, y + 16, 18, 8, PAL.ink); r(x, y + 17, 16, 6, '#8b9bb4'); if (m) { const lk = Art.look(m.id); Art.drawStanding(x, y - 6, { ...lk, s: '#ffd84d', S: '#d9a441', h: '#d9a441', H: '#b07d2a', c: '#ffd84d', C: '#d9a441', p: '#d9a441' }, 0, false); } glows.push({ x: x + 8, y: y + 6, r: 18, c: '#ffd84d' }); }
113
- if (owns('piano')) { const x = RX + RW - 44, y = wing.copa + 20; r(x - 1, y - 1, 24, 12, PAL.ink); r(x, y, 22, 10, '#1b1622'); for (let k = 0; k < 7; k++) r(x + 1 + k * 3, y + 6, 2, 4, '#ffffff'); }
114
- if (owns('fountain')) { const x = CX - 12, y = H - 70; r(x - 1, y + 8, 26, 10, PAL.ink); r(x, y + 9, 24, 8, '#8b9bb4'); r(x + 2, y + 10, 20, 5, '#4fa3d8'); r(x + 11, y, 2, 10, '#c0cbdc'); for (let i = 0; i < 4; i++) { const k = ((t / 500 + i / 4) % 1); r(x + 12 + Math.round(Math.cos(i * 1.6) * k * 8), y - 2 + Math.round(k * k * 12) - 4, 1, 1, '#9fd3ff'); } }
109
+ // ---- bought items: each one has a default spot and can be moved in decorate mode ----
110
+ // Positions are saved in progress.json as fractions of the office (prog.placed[id] = { fx, fy }) so an item
111
+ // stays where you put it when the window or the floor plan changes size.
112
+ const ITEMS = {
113
+ rug: { w: 76, h: 50, under: true, at: () => game ? { x: game.x + Math.round(game.w / 2) - 26, y: game.y + 40 } : { x: CX - 90, y: TOP + 60 } },
114
+ plants: { w: 36, h: 26, at: () => ({ x: CX - 46, y: TOP + 120 }) },
115
+ lamps: { w: 24, h: 18, at: () => ({ x: RX + 4, y: wing.ping - 10 }) },
116
+ statue: { w: 18, h: 32, at: () => ({ x: RX + RW - 26, y: wing.ping + 28 }) },
117
+ piano: { w: 24, h: 12, at: () => ({ x: RX + RW - 44, y: wing.copa + 20 }) },
118
+ fountain: { w: 26, h: 20, at: () => ({ x: CX - 12, y: H - 72 }) },
119
+ neon: { w: 30, h: 8, at: () => ({ x: Math.min(W - 34, RX + 150 > W - 40 ? RX + 2 : RX + 150), y: 2 }) },
120
+ disco: { w: 8, h: 14, at: () => ({ x: CX - 3, y: TOP - 2 }) },
121
+ };
122
+ function itemPos(id) {
123
+ const it = ITEMS[id], p = prog.placed && prog.placed[id];
124
+ const pos = p && Number.isFinite(p.fx) ? { x: Math.round(p.fx * W), y: Math.round(p.fy * H) } : it.at();
125
+ 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 };
115
126
  }
116
- function drawShopWall(t, glows) {
117
- if (owns('neon')) { const on = ((t / 130) | 0) % 41 !== 0, x = RX + 150 > W - 40 ? RX + 2 : RX + 150; r(x - 1, 2, 30, 7, '#1b1d2e'); pixText(x + 1, 3, 'COFFEE', on ? '#ff6ec7' : '#5a2a48'); if (on) glows.push({ x: x + 13, y: 5, r: 20, c: '#ff6ec7' }); }
118
- if (owns('disco')) { const x = CX, y = TOP + 4; r(x, TOP - 6, 1, 6, '#c0cbdc'); r(x - 3, y, 7, 7, '#c0cbdc'); for (let i = 0; i < 6; i++) r(x - 3 + (i * 2 + ((t / 150) | 0)) % 7, y + (i % 3) * 2, 1, 1, ['#ff6ec7', '#2ce8f5', '#ffd84d'][i % 3]); if (partyUntil > Date.now()) glows.push({ x, y: y + 3, r: 60, c: ['#ff6ec7', '#2ce8f5', '#ffd84d'][((t / 300) | 0) % 3] }); }
127
+ const ITEM_DRAW = {
128
+ rug: (x, y) => drawPersianRug(x, y + 3, 76, 44),
129
+ plants: (x, y) => { Art.drawPlant(x, y + 2, true); Art.drawPlant(x + 13, y, true); Art.drawPlant(x + 26, y + 3, false); },
130
+ 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' }); } },
131
+ statue: (x, y, t, glows) => { const m = usageData && usageData.month && usageData.month.top && usageData.month.top[0];
132
+ r(x - 1, y + 22, 18, 8, PAL.ink); r(x, y + 23, 16, 6, '#8b9bb4'); if (m) { const lk = Art.look(m.id); Art.drawStanding(x, y, { ...lk, s: '#ffd84d', S: '#d9a441', h: '#d9a441', H: '#b07d2a', c: '#ffd84d', C: '#d9a441', p: '#d9a441' }, 0, false); } glows.push({ x: x + 8, y: y + 12, r: 18, c: '#ffd84d' }); },
133
+ piano: (x, y) => { r(x - 1, y - 1, 24, 12, PAL.ink); r(x, y, 22, 10, '#1b1622'); for (let k = 0; k < 7; k++) r(x + 1 + k * 3, y + 6, 2, 4, '#ffffff'); },
134
+ fountain: (x, y, t) => { r(x - 1, y + 10, 26, 10, PAL.ink); r(x, y + 11, 24, 8, '#8b9bb4'); r(x + 2, y + 12, 20, 5, '#4fa3d8'); r(x + 11, y + 2, 2, 10, '#c0cbdc'); for (let i = 0; i < 4; i++) { const k = ((t / 500 + i / 4) % 1); r(x + 12 + Math.round(Math.cos(i * 1.6) * k * 8), y + Math.round(k * k * 12) - 4 + 2, 1, 1, '#9fd3ff'); } },
135
+ neon: (x, y, t, glows) => { const on = ((t / 130) | 0) % 41 !== 0; r(x - 1, y, 30, 8, '#1b1d2e'); pixText(x + 1, y + 1, 'COFFEE', on ? '#ff6ec7' : '#5a2a48'); if (on) glows.push({ x: x + 13, y: y + 4, r: 20, c: '#ff6ec7' }); },
136
+ disco: (x, y, t, glows) => { r(x + 3, y, 1, 6, '#c0cbdc'); r(x, y + 6, 7, 7, '#c0cbdc'); for (let i = 0; i < 6; i++) r(x + (i * 2 + ((t / 150) | 0)) % 7, y + 6 + (i % 3) * 2, 1, 1, ['#ff6ec7', '#2ce8f5', '#ffd84d'][i % 3]); if (partyUntil > Date.now()) glows.push({ x: x + 3, y: y + 9, r: 60, c: ['#ff6ec7', '#2ce8f5', '#ffd84d'][((t / 300) | 0) % 3] }); },
137
+ };
138
+ function drawItems(under, t, glows) {
139
+ for (const id of Object.keys(ITEMS)) {
140
+ if (!owns(id) || !!ITEMS[id].under !== under) continue;
141
+ const p = itemPos(id); ITEM_DRAW[id](p.x, p.y, t, glows || []);
142
+ if (decorating) { ctx.strokeStyle = dragItem && dragItem.id === id ? '#ffd84d' : 'rgba(255,216,77,.7)'; ctx.setLineDash([2, 2]); ctx.lineWidth = 1; ctx.strokeRect(p.x - 1.5, p.y - 1.5, p.w + 3, p.h + 3); ctx.setLineDash([]); }
143
+ }
144
+ }
145
+ // kept for the callers: the rug goes under the furniture (drawn with the game room), the rest on top
146
+ function drawShopFloor(t, glows) { drawItems(false, t, glows); }
147
+ function drawShopWall() {}
148
+
149
+ // ---- decorate mode: drag your bought items around ----
150
+ let decorating = false, dragItem = null;
151
+ const decoBar = document.createElement('div');
152
+ decoBar.className = 'deco-bar'; decoBar.hidden = true;
153
+ document.body.appendChild(decoBar);
154
+ function setDecorating(on) {
155
+ decorating = on; dragItem = null; decoBar.hidden = !on;
156
+ if (on) { closeReport(); decoBar.innerHTML = `<b>${esc(T.shop.decoTitle)}</b><span>${esc(T.shop.decoHint)}</span><button class="btn small" data-deco-reset>${esc(T.shop.decoReset)}</button><button class="btn small" data-deco-done>${esc(T.shop.decoDone)}</button>`; achBump('decorate'); }
157
+ lastHits = ''; renderOverlay();
119
158
  }
159
+ decoBar.addEventListener('click', e => {
160
+ if (e.target.closest('[data-deco-done]')) return setDecorating(false);
161
+ if (e.target.closest('[data-deco-reset]')) { prog.placed = {}; saveProgress(); }
162
+ });
163
+ const canvasPoint = e => { const rc = overlay.getBoundingClientRect(); return { x: (e.clientX - rc.left) / S, y: (e.clientY - rc.top) / S }; };
164
+ overlay.addEventListener('pointerdown', e => {
165
+ if (!decorating) return;
166
+ const pt = canvasPoint(e);
167
+ // the topmost owned item under the pointer (floor items over the rug)
168
+ const ids = Object.keys(ITEMS).filter(id => owns(id)).sort((a, b) => (ITEMS[b].under ? 0 : 1) - (ITEMS[a].under ? 0 : 1));
169
+ for (const id of ids) { const p = itemPos(id); if (pt.x >= p.x - 2 && pt.x <= p.x + p.w + 2 && pt.y >= p.y - 2 && pt.y <= p.y + p.h + 2) { dragItem = { id, dx: pt.x - p.x, dy: pt.y - p.y }; e.preventDefault(); e.stopPropagation(); overlay.setPointerCapture(e.pointerId); return; } }
170
+ }, true);
171
+ overlay.addEventListener('pointermove', e => {
172
+ if (!dragItem) return;
173
+ const pt = canvasPoint(e), it = ITEMS[dragItem.id];
174
+ const x = Math.max(0, Math.min(W - it.w, pt.x - dragItem.dx)), y = Math.max(0, Math.min(H - it.h, pt.y - dragItem.dy));
175
+ prog.placed = { ...(prog.placed || {}), [dragItem.id]: { fx: +(x / W).toFixed(4), fy: +(y / H).toFixed(4) } };
176
+ });
177
+ overlay.addEventListener('pointerup', () => { if (dragItem) { dragItem = null; saveProgress(); achBump('moves'); } });
178
+ // while decorating, clicks don't open agents or walk you around
179
+ overlay.addEventListener('click', e => { if (decorating) { e.stopImmediatePropagation(); e.preventDefault(); } }, true);
180
+ document.addEventListener('keydown', e => { if (e.key === 'Escape' && decorating) setDecorating(false); });
120
181
 
121
182
  // ---- party mode (hidden): three quick clicks on the </> neon sign ----
122
183
  let partyUntil = 0, neonClicks = [];
package/public/js/i18n.js CHANGED
@@ -79,6 +79,7 @@ const I18N = {
79
79
  historian: { name: 'Historiador', desc: n => `abrir o histórico ${n} vez${n > 1 ? 'es' : ''}` },
80
80
  windowshop: { name: 'Só olhando', desc: n => `abrir a loja ${n} vez${n > 1 ? 'es' : ''}` },
81
81
  spender: { name: 'Gastador', desc: n => `gastar ${n} moedas na loja` },
82
+ arranger: { name: 'Arrumador', desc: n => `mudar itens de lugar ${n} vez${n > 1 ? 'es' : ''} no modo Decorar` },
82
83
  themer: { name: 'Estilista de interiores', desc: n => `experimentar ${n} opções de personalização` },
83
84
  midnightoil: { name: 'Meia-noite em ponto', desc: n => `abrir o escritório à meia-noite ${n} vez${n > 1 ? 'es' : ''}` },
84
85
  explorer: { name: 'Explorador', desc: n => `andar pelo escritório ${n} vez${n > 1 ? 'es' : ''} (setas ou clique no chão)` },
@@ -113,7 +114,7 @@ const I18N = {
113
114
  trophies: 'Estante de troféus cresce', trophiesText: (tiers, leg, c) => `${tiers} níveis de conquista, ${leg} no nível lenda e ${c} moedas no cofre.`,
114
115
  replies: 'Chefe responde rápido', repliesText: (avg, best) => `Tempo médio de resposta hoje: ${avg}. O recorde do dia foi ${best}.`,
115
116
  footer: 'Diário do Escritório · tudo medido, nada inventado' },
116
- shop: { title: 'Loja', sub: 'Gaste as moedas das conquistas em decoração. O que você compra aparece no escritório.', bought: 'Comprado!', owned: 'no escritório',
117
+ shop: { move: 'Mover', decoTitle: 'Decorar', decoHint: 'Arraste os itens que você comprou para onde quiser.', decoReset: 'Voltar ao lugar original', decoDone: 'Pronto', decoEmpty: 'Compre algo na loja para decorar o escritório.', title: 'Loja', sub: 'Gaste as moedas das conquistas em decoração. O que você compra aparece no escritório.', bought: 'Comprado!', owned: 'no escritório',
117
118
  items: { plants: { name: 'Plantas no corredor', desc: 'Vasos grandes ao longo do corredor.' }, rug: { name: 'Tapete persa', desc: 'Um tapete vermelho e dourado com medalhão, embaixo da mesa de sinuca.' },
118
119
  neon: { name: 'Neon COFFEE', desc: 'Um letreiro neon rosa na parede da copa.' }, lamps: { name: 'Luminárias de pé', desc: 'Duas luminárias quentes no salão de jogos.' },
119
120
  disco: { name: 'Globo espelhado', desc: 'Pendurado no corredor; brilha de verdade nas festas.' }, statue: { name: 'Estátua dourada', desc: 'O Agente do Mês em ouro, num pedestal.' },
@@ -125,7 +126,7 @@ const I18N = {
125
126
  clockIn: 'Relógio de ponto: quem chegou hoje', clockNone: 'Ninguém bateu o ponto hoje ainda.',
126
127
  thoughts: ['será que esqueceram de mim?', 'hora do café?', 'terminei… alguém?', 'zzz… brincadeira, tô aqui', 'posso fazer mais uma coisa', 'que silêncio…', 'alguém viu meu grampeador?', 'revisando meus commits mentalmente', 'tô de olho no relógio', 'me chama que eu vou!'],
127
128
  radio: { label: '♪ Rádio chiptune', on: 'Rádio ligado: a estação segue a IA que mais trabalha, o ritmo segue o escritório (clique para desligar)', off: 'Rádio desligado (clique para ligar)' },
128
- menu: 'Menu', langName: 'Idioma: português (trocar para inglês)', menuItems: { walk: 'Andar pelo escritório', office: 'Personalizar escritório', photo: 'Foto do escritório', radio: 'Rádio', notify: 'Avisos do sistema', sound: 'Sons', focus: 'Modo foco', replay: 'Replay da última hora', search: 'Buscar agente', lang: 'English' },
129
+ menu: 'Menu', langName: 'Idioma: português (trocar para inglês)', menuItems: { deco: 'Decorar (mover itens)', walk: 'Andar pelo escritório', office: 'Personalizar escritório', photo: 'Foto do escritório', radio: 'Rádio', notify: 'Avisos do sistema', sound: 'Sons', focus: 'Modo foco', replay: 'Replay da última hora', search: 'Buscar agente', lang: 'English' },
129
130
  focus: { off: 'Modo foco: silencia sons e notificações por 25 min', on: m => `Modo foco: ${m} min restantes (clique para estender ou desligar)`, ended: 'Modo foco terminou' },
130
131
  feed: { title: 'Histórico', sub: 'O que aconteceu nas últimas 24 horas (guardado só neste navegador)', empty: 'Nada ainda. Chegadas, saídas, pedidos e entregas aparecem aqui.', clear: 'Limpar', thisHour: 'Nesta hora', kinds: { arrive: 'chegou', leave: 'saiu', done: 'terminou', needs_you: 'precisa de você', waiting: 'espera aprovação' } },
131
132
  git: { dirty: n => `${n} arquivo${n > 1 ? 's' : ''} sem commit`, clean: 'tudo commitado', ahead: n => `${n} commit${n > 1 ? 's' : ''} para enviar`, behind: n => `${n} commit${n > 1 ? 's' : ''} para puxar`, label: 'Git' },
@@ -261,6 +262,7 @@ const I18N = {
261
262
  historian: { name: 'Historian', desc: n => `open the history ${n} time${n > 1 ? 's' : ''}` },
262
263
  windowshop: { name: 'Just looking', desc: n => `open the shop ${n} time${n > 1 ? 's' : ''}` },
263
264
  spender: { name: 'Big spender', desc: n => `spend ${n} coins in the shop` },
265
+ arranger: { name: 'Arranger', desc: n => `move items ${n} time${n > 1 ? 's' : ''} in decorate mode` },
264
266
  themer: { name: 'Interior designer', desc: n => `try ${n} customisation options` },
265
267
  midnightoil: { name: 'Midnight sharp', desc: n => `open the office at midnight ${n} time${n > 1 ? 's' : ''}` },
266
268
  explorer: { name: 'Explorer', desc: n => `walk around the office ${n} time${n > 1 ? 's' : ''} (arrows or a click on the floor)` },
@@ -295,7 +297,7 @@ const I18N = {
295
297
  trophies: 'Trophy shelf grows', trophiesText: (tiers, leg, c) => `${tiers} achievement tiers, ${leg} at legend and ${c} coins in the safe.`,
296
298
  replies: 'Boss answers fast', repliesText: (avg, best) => `Average reply time today: ${avg}. The day's record was ${best}.`,
297
299
  footer: 'The Office Times · all measured, nothing made up' },
298
- shop: { title: 'Shop', sub: 'Spend achievement coins on decor. What you buy shows up in the office.', bought: 'Bought!', owned: 'in the office',
300
+ shop: { move: 'Move', decoTitle: 'Decorate', decoHint: 'Drag the items you bought wherever you like.', decoReset: 'Back to the original spots', decoDone: 'Done', decoEmpty: 'Buy something in the shop to decorate the office.', title: 'Shop', sub: 'Spend achievement coins on decor. What you buy shows up in the office.', bought: 'Bought!', owned: 'in the office',
299
301
  items: { plants: { name: 'Corridor plants', desc: 'Big pots along the corridor.' }, rug: { name: 'Persian rug', desc: 'A red and gold rug with a medallion, under the pool table.' },
300
302
  neon: { name: 'COFFEE neon', desc: 'A pink neon sign on the kitchen wall.' }, lamps: { name: 'Floor lamps', desc: 'Two warm lamps in the games room.' },
301
303
  disco: { name: 'Disco ball', desc: 'Hangs in the corridor; really shines at parties.' }, statue: { name: 'Golden statue', desc: 'The Agent of the Month in gold, on a pedestal.' },
@@ -307,7 +309,7 @@ const I18N = {
307
309
  clockIn: 'Punch clock: who arrived today', clockNone: 'Nobody clocked in today yet.',
308
310
  thoughts: ['did they forget about me?', 'coffee time?', 'done… anyone?', 'zzz… kidding, still here', 'I can do one more thing', 'so quiet…', 'has anyone seen my stapler?', 'reviewing my commits in my head', 'watching the clock', 'call me, I am ready!'],
309
311
  radio: { label: '♪ Chiptune radio', on: 'Radio on: the station follows the AI working the most, the beat follows the office (click to turn off)', off: 'Radio off (click to turn on)' },
310
- menu: 'Menu', langName: 'Language: English (switch to Portuguese)', menuItems: { walk: 'Walk around the office', office: 'Customise office', photo: 'Office photo', radio: 'Radio', notify: 'System alerts', sound: 'Sounds', focus: 'Focus mode', replay: 'Replay last hour', search: 'Search agents', lang: 'Português' },
312
+ menu: 'Menu', langName: 'Language: English (switch to Portuguese)', menuItems: { deco: 'Decorate (move items)', walk: 'Walk around the office', office: 'Customise office', photo: 'Office photo', radio: 'Radio', notify: 'System alerts', sound: 'Sounds', focus: 'Focus mode', replay: 'Replay last hour', search: 'Search agents', lang: 'Português' },
311
313
  focus: { off: 'Focus mode: mutes sounds and notifications for 25 min', on: m => `Focus mode: ${m} min left (click to extend or turn off)`, ended: 'Focus mode ended' },
312
314
  feed: { title: 'History', sub: 'What happened in the last 24 hours (kept in this browser only)', empty: 'Nothing yet. Arrivals, departures, requests and finished work show up here.', clear: 'Clear', thisHour: 'This hour', kinds: { arrive: 'arrived', leave: 'left', done: 'finished', needs_you: 'needs you', waiting: 'waits for approval' } },
313
315
  git: { dirty: n => `${n} uncommitted file${n > 1 ? 's' : ''}`, clean: 'all committed', ahead: n => `${n} commit${n > 1 ? 's' : ''} to push`, behind: n => `${n} commit${n > 1 ? 's' : ''} to pull`, label: 'Git' },
@@ -20,6 +20,7 @@ function drawGameRoom(G, t, glows) {
20
20
  // floor: dark teal carpet with a diamond pattern
21
21
  r(x, y, w, h, '#2f4f5a');
22
22
  for (let yy = 4; yy < h; yy += 8) for (let xx = (yy / 8) % 2 ? 4 : 0; xx < w; xx += 8) r(x + xx, y + yy, 2, 2, '#365b67');
23
+ drawItems(true, t, glows); // the rug (wherever you put it) lies under the furniture
23
24
  // walls (the top one has a face), door on the right wall to the corridor
24
25
  r(x - 2, y - 2, w + 4, 3, PAL.ink); r(x - 2, y + 1, w + 4, 8, PAL.wall); r(x - 2, y + 9, w + 4, 1, PAL.wallShade);
25
26
  r(x - 2, y, 3, h + 2, PAL.ink); r(x - 2, y + h, w + 4, 3, PAL.ink); r(x + w - 1, y, 3, h + 2, PAL.ink);
@@ -44,7 +45,6 @@ function drawGameRoom(G, t, glows) {
44
45
  // middle row: foosball, pool table, aquarium
45
46
  const playing = layout.filter(c => c.actor && c.actor.mode === 'lounge' && c.actor.spot && devSpots && devSpots.foos && devSpots.foos.some(s => s.x === c.actor.spot.x && s.y === c.actor.spot.y)).length >= 2;
46
47
  drawFoosball(x + 64, y + 52, t, playing);
47
- if (owns('rug')) drawPersianRug(x + Math.round(w / 2) - 26, y + 40, 76, 50);
48
48
  if (w >= 360) drawPool(x + Math.round(w / 2) - 10, y + 50);
49
49
  if (w >= 300) drawAquarium(x + w - 46, y + 56, t, glows);
50
50
  // bottom: bean bags, the vending machine (shop), an easel (customise), the guestbook (history)
@@ -51,11 +51,12 @@ function renderBar() {
51
51
  { const rb = document.getElementById('btn-replay'); rb.title = T.replay.title; rb.querySelector('.sr').textContent = T.replay.title; rb.setAttribute('aria-pressed', !replayEl.hidden); }
52
52
  renderFocus();
53
53
  // short visible labels in the ☰ menu (the long explanation stays in the tooltip)
54
- for (const [id, k] of [['btn-notify', 'notify'], ['btn-sound', 'sound'], ['btn-focus', 'focus'], ['btn-replay', 'replay'], ['btn-search', 'search'], ['btn-radio', 'radio'], ['btn-photo', 'photo'], ['btn-office', 'office'], ['btn-walk', 'walk']]) {
54
+ for (const [id, k] of [['btn-notify', 'notify'], ['btn-sound', 'sound'], ['btn-focus', 'focus'], ['btn-replay', 'replay'], ['btn-search', 'search'], ['btn-radio', 'radio'], ['btn-photo', 'photo'], ['btn-office', 'office'], ['btn-walk', 'walk'], ['btn-deco', 'deco']]) {
55
55
  const b = document.getElementById(id); let ml = b.querySelector('.ml');
56
56
  if (!ml) { ml = document.createElement('span'); ml.className = 'ml'; b.insertBefore(ml, b.querySelector('.focus-left')); }
57
57
  ml.textContent = T.menuItems[k];
58
58
  }
59
+ { const db = document.getElementById('btn-deco'); db.title = T.shop.decoTitle; db.querySelector('.sr').textContent = T.shop.decoTitle; }
59
60
  { const ob = document.getElementById('btn-office'); ob.title = T.office.tab; ob.querySelector('.sr').textContent = T.office.tab; }
60
61
  { const pb = document.getElementById('btn-photo'); pb.title = T.photo.title; pb.querySelector('.sr').textContent = T.photo.title; }
61
62
  { const sb = document.getElementById('btn-search'); sb.title = T.keys.searchTitle; sb.querySelector('.sr').textContent = T.keys.searchTitle; }
@@ -163,6 +164,7 @@ document.getElementById('btn-radio').onclick = () => setRadio(!radioOn);
163
164
  document.getElementById('btn-walk').onclick = () => setPlayer(!playerOn);
164
165
  document.getElementById('btn-photo').onclick = () => { setMenu(false); groupPhoto(); };
165
166
  document.getElementById('btn-office').onclick = () => { setMenu(false); openReport('office'); };
167
+ document.getElementById('btn-deco').onclick = () => { setMenu(false); if (prog.owned.length) setDecorating(true); else { openReport('shop'); toast(`<b>${esc(T.shop.decoTitle)}</b>${esc(T.shop.decoEmpty)}`, ''); } };
166
168
  document.getElementById('btn-replay').onclick = () => toggleReplay(replayEl.hidden);
167
169
  document.getElementById('btn-lang').onclick = () => { achBump('lang'); lang = lang === 'pt' ? 'en' : 'pt'; T = I18N[lang]; store.set('lang', lang); renderAll(); };
168
170
 
package/public/style.css CHANGED
@@ -729,3 +729,7 @@ body.trophy-view #clashes, body.trophy-view #floors { display: none; }
729
729
  body.trophy-view .help { display: none; }
730
730
  .you-face { width: 36px; height: 40px; display: grid; place-items: center; background: #2b2336; border: 2px solid var(--edge); border-radius: 6px; }
731
731
  .you-face canvas { width: 32px; height: 36px; image-rendering: pixelated; }
732
+ .deco-bar { position: fixed; left: 50%; top: 128px; transform: translateX(-50%); z-index: 35; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; padding: 8px 12px; background: #1b1622; color: #f4ecd8; border: 2px solid #ffd84d; border-radius: 8px; box-shadow: 4px 4px 0 #0b0a0e; font-size: 13px; }
733
+ .deco-bar[hidden] { display: none; }
734
+ .deco-bar b { font: 700 11px/1 var(--pix); color: #ffd84d; }
735
+ body:has(.deco-bar:not([hidden])) #overlay { cursor: move; }