feat: add common social fireplace

This commit is contained in:
2026-09-05 18:20:49 +05:00
parent 32527ec2ed
commit ca830b00b9
11 changed files with 539 additions and 121 deletions
+11 -1
View File
@@ -19,6 +19,16 @@ interface Chip {
}
function makeTool(tool: string): Graphics {
if (tool === 'log') {
// бревно в руках — для подкинутого дрова у костра
return new Graphics()
.rect(-6, -3, 12, 5)
.fill(0x7a5230)
.circle(-6, -0.5, 2.2)
.fill(0x9a7448)
.circle(6, -0.5, 2.2)
.fill(0x9a7448);
}
const g = new Graphics().rect(-1, -12, 2, 12).fill(HANDLE);
if (tool === 'axe_rusty' || tool === 'axe_iron') {
g.poly([-1, -12, 7, -12, 7, -7, -1, -8]).fill(tool === 'axe_iron' ? STEEL : RUSTY);
@@ -68,7 +78,7 @@ export class AvatarView {
const body = new Graphics().roundRect(-7, -18, 14, 13, 4).fill(av.color);
const head = new Graphics().circle(0, -23, 6.5).fill(SKIN);
this.toolHolder.position.set(6, -14);
for (const tool of ['axe_rusty', 'axe_iron', 'pick_rusty', 'pick_iron', 'hammer', 'spoon', 'rod']) {
for (const tool of ['axe_rusty', 'axe_iron', 'pick_rusty', 'pick_iron', 'hammer', 'spoon', 'rod', 'log']) {
const g = makeTool(tool);
g.visible = false;
this.tools.set(tool, g);
+75 -12
View File
@@ -2,6 +2,7 @@ import { Application, Container, Graphics, Text } from 'pixi.js';
import type {
AvatarState,
CameraState,
CampfireState,
DeltaMsg,
GameEvent,
NodeState,
@@ -10,7 +11,7 @@ import type {
WelcomeMsg,
ZoneDef,
} from '@idle/shared';
import { RIVER_WATER_Y } from '@idle/shared';
import { CAMPFIRE, RIVER_WATER_Y } from '@idle/shared';
import { AvatarView } from './avatar';
import { FishSpotView } from './fish';
import { RockView } from './rock';
@@ -25,6 +26,7 @@ export interface MountWorldOptions {
onStatus?: (status: WorldStatus) => void;
onEvent?: (e: GameEvent) => void;
onStats?: (s: ViewerStat[]) => void;
onCampfire?: (s: CampfireState) => void;
}
export interface WorldHandle {
@@ -139,7 +141,9 @@ function drawAnvil(): Container {
return c;
}
function drawCampfire(flickers: Flicker[]): Container {
function drawCampfire(
flickers: Flicker[],
): { c: Container; scalable: Array<{ g: Graphics; baseScale: number }> } {
const c = new Container();
const stones = new Graphics();
for (let i = 0; i < 6; i++) {
@@ -158,8 +162,28 @@ function drawCampfire(flickers: Flicker[]): Container {
{ g: flame1, base: 0.95, phase: 0 },
{ g: flame2, base: 0.9, phase: 1.7 },
);
const scalable = [
{ g: glow, baseScale: 1 },
{ g: flame1, baseScale: 1 },
{ g: flame2, baseScale: 1 },
];
c.addChild(glow, stones, logA, logB, flame1, flame2);
return c;
return { c, scalable };
}
interface CampfireUi {
label: Text;
bar: Graphics;
/** Пламя/свечение, масштабируемые уровнем костра. */
scalable: Array<{ g: Graphics; baseScale: number }>;
}
function campfireBarPct(progress: number, level: number): number {
const thr = CAMPFIRE.levelThresholds;
if (level >= thr.length) return 100;
const lo = thr[level - 1] ?? 0;
const hi = thr[level] ?? 1;
return Math.min(100, Math.max(0, ((progress - lo) / (hi - lo)) * 100));
}
function drawStove(flickers: Flicker[]): Container {
@@ -208,6 +232,18 @@ export async function mountWorld(host: HTMLElement, opts: MountWorldOptions = {}
app.stage.addChild(worldLayer);
const flickers: Flicker[] = [];
let campfireUi: CampfireUi | null = null;
let flameScale = 1;
function updateCampfireVisual(cs: CampfireState): void {
if (!campfireUi) return;
flameScale = 1 + (cs.level - 1) * 0.18;
campfireUi.label.text = `Костёр ур.${cs.level}`;
const pct = campfireBarPct(cs.progress, cs.level);
campfireUi.bar.clear();
campfireUi.bar.roundRect(-24, -84, 48, 5, 2.5).fill({ color: 0x10131a, alpha: 0.6 });
campfireUi.bar.roundRect(-24, -84, 48 * pct, 5, 2.5).fill(0xff9a3b);
}
let clockOffset = 0; // serverNow - Date.now()
let camTarget: CameraState | null = null;
@@ -274,6 +310,9 @@ export async function mountWorld(host: HTMLElement, opts: MountWorldOptions = {}
for (const f of flickers) {
f.g.alpha = f.base + Math.sin(now / 150 + f.phase) * 0.15;
}
for (const s of campfireUi?.scalable ?? []) {
s.g.scale.set(flameScale);
}
for (const [id, view] of avatarViews) {
const av = avatars.get(id);
if (av) {
@@ -306,15 +345,31 @@ export async function mountWorld(host: HTMLElement, opts: MountWorldOptions = {}
ground.zIndex = -1000;
entityLayer.addChild(ground);
for (const prop of w.world.props) {
const view =
prop.kind === 'anvil'
? drawAnvil()
: prop.kind === 'campfire'
? drawCampfire(flickers)
: drawStove(flickers);
view.position.set(prop.x, prop.y);
view.zIndex = prop.y;
entityLayer.addChild(view);
if (prop.kind === 'anvil') {
const view = drawAnvil();
view.position.set(prop.x, prop.y);
view.zIndex = prop.y;
entityLayer.addChild(view);
} else if (prop.kind === 'campfire') {
const built = drawCampfire(flickers);
built.c.position.set(prop.x, prop.y);
built.c.zIndex = prop.y;
const label = new Text({
text: 'Костёр ур.1',
style: { fontFamily: 'monospace', fontSize: 11, fontWeight: 'bold', fill: 0xffc46b, stroke: { color: 0x10131a, width: 3 } },
});
label.anchor.set(0.5, 1);
label.y = -74;
const bar = new Graphics();
built.c.addChild(bar, label);
campfireUi = { label, bar, scalable: built.scalable };
entityLayer.addChild(built.c);
} else {
const view = drawStove(flickers);
view.position.set(prop.x, prop.y);
view.zIndex = prop.y;
entityLayer.addChild(view);
}
}
}
},
@@ -326,6 +381,10 @@ export async function mountWorld(host: HTMLElement, opts: MountWorldOptions = {}
for (const av of snap.avatars) syncAvatar(av);
for (const st of snap.nodes) syncNode(st);
pruneEntities();
if (snap.campfire) {
updateCampfireVisual(snap.campfire);
opts.onCampfire?.(snap.campfire);
}
opts.onStats?.(snap.stats);
},
applyDelta(d) {
@@ -335,6 +394,10 @@ export async function mountWorld(host: HTMLElement, opts: MountWorldOptions = {}
if (d.nodes) for (const st of d.nodes) syncNode(st);
for (const e of d.events ?? []) opts.onEvent?.(e);
if (d.stats) opts.onStats?.(d.stats);
if (d.campfire) {
updateCampfireVisual(d.campfire);
opts.onCampfire?.(d.campfire);
}
},
destroy() {
void app.destroy(true, { children: true });