Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ Plan → scope → `pnpm agent:verify` → `pnpm agent:verify --e2e`(golden fl
| 路径 | 契约 | 回归 gate |
|------|------|-----------|
| `apps/web/src/game/entityLabels.ts` | Proximity 铭牌:宋体、`LABEL_SCALE` 字号、无描边/无底条、轻投影(Guardrail #57) | `pnpm verify:phase13` + UAT #6/#7 |
| `apps/web/src/game/entitySprites.ts` | `spriteNameplateY` + **全员 LPC**(Guardrail #106) | 同上 + `pnpm verify:phase6:move-only` |
| `apps/web/src/game/entitySprites.ts` | `spriteNameplateY` + **全员 LPC**(玩家 + `npc-1`…`npc-12`;Guardrail #106) | 同上 + `pnpm verify:phase6:move-only` |
| `apps/web/src/game/ProximityNameplate.ts` | ≤2 格 fade;speak/thinking 强制显示 | 同上 |
| `apps/web/src/hooks/useNpcChat.ts` | 方案 A composer busy;`onSpeakBusy` 清 `sendingNpcId`(ISSUE-037) | `uat:phase8` Test 4 + web test |

Expand Down
6 changes: 3 additions & 3 deletions apps/web/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,13 @@ Home Tiled map (40×40 @ 16px → **32px/grid** in-game). Full bake / collision

## Character visuals(全员 LPC · 2026-07)

**产品决策:** 所有玩家 + `npc-1` → LPC `sprites/lpc-npc-1.png`;议会其余 NPC → Stardew `sprites/npcs.png`。
**产品决策:** 所有玩家 + `npc-1`…`npc-12` → LPC `sprites/lpc-player-1.png` + `sprites/lpc-npc-{1…12}.png`。

| Task | Command / path |
|------|----------------|
| 烘焙 LPC 皮 | `pnpm assets:sync:lpc-npc1`(源 `npc-asset/npc-1.png`) |
| 烘焙 LPC 皮 | `pnpm assets:sync:lpc-npcs`(别名 `assets:sync:lpc-npc1`;源 `npc-asset/player-1.png` + `npc-asset/npc-{1…12}.png`) |
| 常量 | `gridLayout.CELL_PX=32` · `entityLayout.CHAR_DISPLAY_PX=64` |
| 运行时 | `lpcNpc1Sheet.ts` · `entitySprites.ts` · `sceneLabelLayout.ts` |
| 运行时 | `lpcNpc1Sheet.ts` · `entitySprites.ts`(`registerLpcNpcAnims`)· `sceneLabelLayout.ts` |
| 文档 | [BEGINNING-FIELDS.md](../../docs/BEGINNING-FIELDS.md) §角色视觉 |
| 回归 | `pnpm verify:phase6:move-only` + `pnpm verify:phase13` |

Expand Down
13 changes: 6 additions & 7 deletions apps/web/public/assets/CREDITS.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,12 @@ Phase 13.2 farm art (default production). Regenerate atlases: `pnpm art:import`

| Field | Value |
|-------|--------|
| **决策** | **所有玩家**(本地 + 远端)与 **`npc-1`** 统一使用 LPC 烘焙皮;**不再**用 `sprites/characters.png` 四色 palette 区分玩家 |
| **源图** | `npc-asset/npc-1.png`(Universal LPC Spritesheet Character Generator) |
| **烘焙** | `pnpm assets:sync:lpc-npc1` → `scripts/sync-npc-lpc-assets.mjs` |
| **输出** | `sprites/lpc-npc-1.png` — 64×64 帧,walk(row 8–11)+ idle(row 22–25),方向 up/left/down/right |
| **运行时** | `lpcNpc1Sheet.ts` · `entitySprites.registerLpcNpc1Anims` · `useSpriteEntities()` 须 `spritesLpcNpc1` + `spritesNpcs` |
| **决策** | **所有玩家**(本地 + 远端)与 **`npc-1`…`npc-12`** 统一使用 LPC 烘焙皮;**不再**用 `sprites/characters.png` 四色 palette 区分玩家 |
| **源图** | `npc-asset/player-1.png` + `npc-asset/npc-{1…12}.png`(Universal LPC Spritesheet Character Generator) |
| **烘焙** | `pnpm assets:sync:lpc-npcs`(别名 `assets:sync:lpc-npc1`)→ `scripts/sync-npc-lpc-assets.mjs` |
| **输出** | `sprites/lpc-player-1.png` + `sprites/lpc-npc-{1…12}.png` — 64×64 帧,walk(row 8–11)+ idle(row 22–25),方向 up/left/down/right |
| **运行时** | `lpcNpc1Sheet.ts` · `entitySprites.registerLpcNpcAnims` · `useSpriteEntities()` 须 `spritesLpcNpc1` + `spritesNpcs` |
| **显示** | `CELL_PX=32`;角色高 `CHAR_DISPLAY_PX=64`(2 格);`LPC_NPC1_SCALE=1.0` |
| **其他 NPC** | `npc-2`…`npc-12` 仍用 Stardew `sprites/npcs.png`(16×32 @ 2×) |

文档:[BEGINNING-FIELDS.md](../../docs/BEGINNING-FIELDS.md) §角色视觉 · Guardrails #106–#107 in [ISSUE-LOG.md](../../docs/ISSUE-LOG.md)。

Expand All @@ -70,7 +69,7 @@ Phase 13.2 farm art (default production). Regenerate atlases: `pnpm art:import`
| **Facing** | Sheet rows: down / up / right; **left** = horizontal flip in `entitySprites.applyFacingFlip` |
| **Walk / idle** | 4 walk + 2 idle frames per facing (from 6 walk / 4 idle source columns) |

Council NPCs(npc-1):`sprites/npcs.png`(同上 import,2 variants × 4 facings)
Council NPCs(`npc-1`…`npc-12`)生产路径为 LPC 烘焙皮;`sprites/npcs.png` 仅作 legacy / Stardew fallback

Legacy LPC vendor pass: `pnpm art:import:lpc` (`scripts/vendor/phase13/lpc/`).

Expand Down
Binary file added apps/web/public/assets/sprites/lpc-npc-10.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/assets/sprites/lpc-npc-11.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/assets/sprites/lpc-npc-12.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/assets/sprites/lpc-npc-2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/assets/sprites/lpc-npc-3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/assets/sprites/lpc-npc-4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/assets/sprites/lpc-npc-5.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/assets/sprites/lpc-npc-6.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/assets/sprites/lpc-npc-7.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/assets/sprites/lpc-npc-8.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/assets/sprites/lpc-npc-9.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/assets/sprites/lpc-player-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
57 changes: 39 additions & 18 deletions apps/web/src/game/HomeMapBackground.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { HOME_MAP_TILE_H, HOME_MAP_TILE_W, isHomeMapRegionCell } from "@aetherlife/shared";
import type * as Phaser from "phaser";
import * as Phaser from "phaser";
import { ASSET_KEYS, TILE_PX } from "./assetManifest.js";
import { tiledObjectYSortDepth, YSORT_LAYER } from "./entityLayout.js";
import {
MAP_TILE_DEPTH_BASE,
MAP_TILE_DEPTH_STEP,
tiledObjectYSortDepth,
YSORT_LAYER,
} from "./entityLayout.js";
import { CELL_PX } from "./gridLayout.js";
import { isVisualFallbackActive } from "./visualFallback.js";

Expand Down Expand Up @@ -169,6 +174,7 @@ function applyObjectTileAnimation(
*/
export class HomeMapBackground {
private layers: Phaser.GameObjects.GameObject[] = [];
private objectShadowLayer: Phaser.GameObjects.Layer | null = null;
private map: Phaser.Tilemaps.Tilemap | null = null;
private tilesets: Phaser.Tilemaps.Tileset[] = [];

Expand All @@ -192,7 +198,7 @@ export class HomeMapBackground {
const layerMetas = (cacheEntry.data as { layers: TiledLayerMeta[] }).layers;
const bakedTilesets = (cacheEntry.data as { tilesets: TiledTilesetDef[] }).tilesets ?? [];

let depth = -1;
let tileBandDepth = MAP_TILE_DEPTH_BASE;
for (const meta of layerMetas) {
if (meta.visible === false) continue;
if (SKIP_LAYER_NAMES.has(meta.name)) continue;
Expand All @@ -204,8 +210,11 @@ export class HomeMapBackground {
if (meta.opacity !== undefined && meta.opacity < 1) {
layer.setAlpha(meta.opacity);
}
layer.setDepth(depth);
depth += 0.01;
if (meta.name === "Shadows") {
layer.setBlendMode(Phaser.BlendModes.MULTIPLY);
}
layer.setDepth(tileBandDepth);
tileBandDepth += MAP_TILE_DEPTH_STEP;
this.layers.push(layer);
continue;
}
Expand All @@ -215,6 +224,15 @@ export class HomeMapBackground {
if (!rawLayer) continue;

const isShadowLayer = meta.name === "Object Shadows";
if (isShadowLayer && !this.objectShadowLayer) {
this.objectShadowLayer = scene.add.layer();
this.objectShadowLayer.setDepth(tileBandDepth);
this.objectShadowLayer.setAlpha(meta.opacity ?? 0.25);
this.objectShadowLayer.setBlendMode(Phaser.BlendModes.MULTIPLY);
this.layers.push(this.objectShadowLayer);
tileBandDepth += MAP_TILE_DEPTH_STEP;
}

const sortedObjects =
meta.draworder === "topdown"
? [...rawLayer.objects].sort((a, b) => {
Expand All @@ -225,6 +243,7 @@ export class HomeMapBackground {
})
: rawLayer.objects;

let shadowIndex = 0;
for (const rawObj of sortedObjects) {
if (!rawObj.gid) continue;

Expand All @@ -235,24 +254,25 @@ export class HomeMapBackground {
: scene.add.sprite(0, 0, "__MISSING");

placeTiledTileObject(sprite, rawObj);
if (meta.opacity !== undefined && meta.opacity < 1) {
sprite.setAlpha(meta.opacity);

if (isShadowLayer) {
sprite.setDepth(shadowIndex++);
this.objectShadowLayer!.add(sprite);
} else {
if (meta.opacity !== undefined && meta.opacity < 1) {
sprite.setAlpha(meta.opacity);
}
const bottomWorldX = (rawObj.x ?? 0) * TILE_SCALE;
const bottomWorldY = (rawObj.y ?? 0) * TILE_SCALE;
sprite.setDepth(
tiledObjectYSortDepth(bottomWorldX, bottomWorldY, YSORT_LAYER.OBJECT),
);
this.layers.push(sprite);
}
const bottomWorldX = (rawObj.x ?? 0) * TILE_SCALE;
const bottomWorldY = (rawObj.y ?? 0) * TILE_SCALE;
sprite.setDepth(
tiledObjectYSortDepth(
bottomWorldX,
bottomWorldY,
isShadowLayer ? YSORT_LAYER.SHADOW : YSORT_LAYER.OBJECT,
),
);

if (resolved) {
applyObjectTileAnimation(scene, sprite, rawObj.gid);
}

this.layers.push(sprite);
}
}
}
Expand Down Expand Up @@ -291,6 +311,7 @@ export class HomeMapBackground {
}
this.map?.destroy();
this.layers = [];
this.objectShadowLayer = null;
this.map = null;
this.tilesets = [];
}
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/game/RoomScene.ts
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,7 @@ export class RoomScene extends Phaser.Scene {
private useSpriteEntities(): boolean {
return (
!isVisualFallbackActive(this)
&& this.textures.exists(ASSET_KEYS.spritesLpcPlayer1)
&& this.textures.exists(ASSET_KEYS.spritesLpcNpc1)
&& this.textures.exists(ASSET_KEYS.spritesNpcs)
);
Expand Down
27 changes: 24 additions & 3 deletions apps/web/src/game/assetManifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,20 @@ export const ASSET_KEYS = {
tilesDecor: "tiles/decor",
spritesCharacters: "sprites/characters",
spritesNpcs: "sprites/npcs",
/** Baked walk+idle from npc-asset/npc-1.png (scripts/sync-npc-lpc-assets.mjs). */
/** Baked walk+idle from npc-asset LPC composites (scripts/sync-npc-lpc-assets.mjs). */
spritesLpcPlayer1: "sprites/lpc-player-1",
spritesLpcNpc1: "sprites/lpc-npc-1",
spritesLpcNpc2: "sprites/lpc-npc-2",
spritesLpcNpc3: "sprites/lpc-npc-3",
spritesLpcNpc4: "sprites/lpc-npc-4",
spritesLpcNpc5: "sprites/lpc-npc-5",
spritesLpcNpc6: "sprites/lpc-npc-6",
spritesLpcNpc7: "sprites/lpc-npc-7",
spritesLpcNpc8: "sprites/lpc-npc-8",
spritesLpcNpc9: "sprites/lpc-npc-9",
spritesLpcNpc10: "sprites/lpc-npc-10",
spritesLpcNpc11: "sprites/lpc-npc-11",
spritesLpcNpc12: "sprites/lpc-npc-12",
spritesUiSpeech: "sprites/ui-speech",
tilesScrubPack: "tiles/biome-scrub",
tilesWetlandPack: "tiles/biome-wetland",
Expand Down Expand Up @@ -82,11 +94,20 @@ export const CORE_AREA_ASSETS: AssetSheetDef[] = [
},
{
kind: "spritesheet",
key: ASSET_KEYS.spritesLpcNpc1,
url: `${BASE}/sprites/lpc-npc-1.png`,
key: ASSET_KEYS.spritesLpcPlayer1,
url: `${BASE}/sprites/lpc-player-1.png`,
frameWidth: 64,
frameHeight: 64,
},
...([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] as const).map(
(n): AssetSheetDef => ({
kind: "spritesheet",
key: ASSET_KEYS[`spritesLpcNpc${n}` as keyof typeof ASSET_KEYS],
url: `${BASE}/sprites/lpc-npc-${n}.png`,
frameWidth: 64,
frameHeight: 64,
}),
),
{ kind: "image", key: ASSET_KEYS.spritesUiSpeech, url: `${BASE}/sprites/ui-speech.png` },
];

Expand Down
9 changes: 9 additions & 0 deletions apps/web/src/game/entityLayout.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import {
entityFootSortPoint,
entityYSortDepth,
entityYSortDepthFromCenter,
MAP_TILE_DEPTH_BASE,
MAP_TILE_DEPTH_STEP,
tiledObjectYSortDepth,
ySortDepth,
YSORT_LAYER,
Expand Down Expand Up @@ -82,3 +84,10 @@ describe("tiledObjectYSortDepth", () => {
expect(ySortDepth(0, 0, 0)).toBe(ENTITY_DEPTH_BASE);
});
});

describe("MAP tile depth bands", () => {
it("keeps tile + object-shadow bands below entity Y-sort", () => {
const objectShadowBand = MAP_TILE_DEPTH_BASE + 4 * MAP_TILE_DEPTH_STEP;
expect(objectShadowBand).toBeLessThan(ENTITY_DEPTH_BASE);
});
});
4 changes: 4 additions & 0 deletions apps/web/src/game/entityLayout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ export const MARKER_STROKE = 2;

export const MARKER_LABEL_MAX_WIDTH = labelOffset(44);

/** Tiled tile bands (Ground … Water) — sequential depth in map layer order. */
export const MAP_TILE_DEPTH_BASE = 0;
export const MAP_TILE_DEPTH_STEP = 0.01;

/** Floor/tile layers use depth below this; entities & Tiled objects sit above. */
export const ENTITY_DEPTH_BASE = 10_000;

Expand Down
Loading
Loading