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 +3 -3
- package/package.json +1 -1
- package/public/index.html +1 -0
- package/public/js/achievements.js +2 -1
- package/public/js/collection.js +73 -12
- package/public/js/i18n.js +6 -4
- package/public/js/rooms.js +1 -1
- package/public/js/topbar.js +3 -1
- package/public/style.css +4 -0
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.** 75 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. |
|
|
@@ -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
|
-
- **
|
|
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.
|
|
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] },
|
package/public/js/collection.js
CHANGED
|
@@ -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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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' },
|
package/public/js/rooms.js
CHANGED
|
@@ -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)
|
package/public/js/topbar.js
CHANGED
|
@@ -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; }
|