feat: add common social fireplace
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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 });
|
||||
|
||||
Reference in New Issue
Block a user