Implement scene-based chapter quest progression
Some checks failed
CI / verify (push) Has been cancelled
Some checks failed
CI / verify (push) Has been cancelled
This commit is contained in:
@@ -12,7 +12,6 @@ import {
|
||||
type ScenePresetInfo,
|
||||
type WorldType,
|
||||
} from '../../types';
|
||||
import {CharacterAnimator} from '../CharacterAnimator';
|
||||
import {HostileNpcAnimator} from '../HostileNpcAnimator';
|
||||
import {MedievalNpcAnimator} from '../MedievalNpcAnimator';
|
||||
import {getRenderableNpcFacing} from '../npcRenderUtils';
|
||||
@@ -31,7 +30,6 @@ import {
|
||||
mapHostileNpcAnimationToCharacterState,
|
||||
MONSTER_RENDER_OFFSETS,
|
||||
ROLE_CHARACTER_FRAME_CLASS,
|
||||
ROLE_CHARACTER_SPRITE_CLASS,
|
||||
RoleCharacterSprite,
|
||||
SCENE_TRANSITION_LOWER_COMPANION_DELAY_S,
|
||||
SCENE_TRANSITION_UPPER_COMPANION_DELAY_S,
|
||||
@@ -166,19 +164,11 @@ export function GameCanvasEntityLayer({
|
||||
</div>
|
||||
)}
|
||||
<div className={ROLE_CHARACTER_FRAME_CLASS}>
|
||||
<div
|
||||
className="h-full w-full"
|
||||
style={{
|
||||
transform:
|
||||
(sceneTransitionPhase === 'idle' ? companion.facing : 'right') === 'left'
|
||||
? 'scaleX(-1)'
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
<CharacterAnimator
|
||||
<div className={companion.hp <= 0 ? 'opacity-45 grayscale' : undefined}>
|
||||
<RoleCharacterSprite
|
||||
state={sceneTransitionPhase === 'idle' ? companion.animationState : AnimationState.RUN}
|
||||
character={companion.character}
|
||||
className={`${ROLE_CHARACTER_SPRITE_CLASS} ${companion.hp <= 0 ? 'opacity-45 grayscale' : ''}`}
|
||||
facing={sceneTransitionPhase === 'idle' ? (companion.facing ?? 'right') : 'right'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -220,13 +210,13 @@ export function GameCanvasEntityLayer({
|
||||
ariaLabel={playerCharacter ? `查看${playerCharacter.name}详情` : undefined}
|
||||
className="relative block"
|
||||
>
|
||||
<div className="relative" style={{transform: effectivePlayerFacing === 'left' ? 'scaleX(-1)' : undefined}}>
|
||||
<div className="relative">
|
||||
<div className={ROLE_CHARACTER_FRAME_CLASS}>
|
||||
{playerCharacter && (
|
||||
<CharacterAnimator
|
||||
<RoleCharacterSprite
|
||||
state={effectivePlayerAnimationState}
|
||||
character={playerCharacter}
|
||||
className={ROLE_CHARACTER_SPRITE_CLASS}
|
||||
facing={effectivePlayerFacing}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -248,15 +238,18 @@ export function GameCanvasEntityLayer({
|
||||
if (!npcEncounter) return null;
|
||||
const config = monsters.find(item => item.id === hostileNpc.id);
|
||||
const renderOffset = MONSTER_RENDER_OFFSETS[hostileNpc.id] ?? {x: 0, y: 0};
|
||||
const npcMonsterConfig = npcEncounter?.monsterPresetId
|
||||
const npcCharacter = npcEncounter?.characterId ? getCharacterById(npcEncounter.characterId) : null;
|
||||
const npcMonsterConfig = !npcCharacter && npcEncounter?.monsterPresetId
|
||||
? monsters.find(item => item.id === npcEncounter.monsterPresetId) ?? config ?? null
|
||||
: null;
|
||||
const npcCharacter = npcEncounter?.characterId ? getCharacterById(npcEncounter.characterId) : null;
|
||||
const npcSceneSpriteFacing =
|
||||
npcCharacter
|
||||
? hostileNpc.facing
|
||||
: getRenderableNpcFacing(npcEncounter, hostileNpc.facing, {medievalVisual: true});
|
||||
const npcCombatHpTop = getNpcCombatHpTop(npcEncounter?.characterId, npcEncounter?.monsterPresetId);
|
||||
const npcCombatHpTop = getNpcCombatHpTop(
|
||||
npcCharacter ? npcEncounter?.characterId : null,
|
||||
npcCharacter ? null : npcEncounter?.monsterPresetId,
|
||||
);
|
||||
const hostileNpcBottomOffsetPx = npcMonsterConfig
|
||||
? HOSTILE_NPC_SCENE_BOTTOM_OFFSET_PX
|
||||
: 0;
|
||||
@@ -350,7 +343,7 @@ export function GameCanvasEntityLayer({
|
||||
encounter.kind !== 'treasure' && encounter.characterId
|
||||
? getCharacterById(encounter.characterId)
|
||||
: null;
|
||||
const peacefulMonsterConfig =
|
||||
const peacefulMonsterConfig = !peacefulResolvedCharacter &&
|
||||
encounter.kind === 'npc' && encounter.monsterPresetId
|
||||
? monsters.find(item => item.id === encounter.monsterPresetId) ?? null
|
||||
: null;
|
||||
|
||||
@@ -2,7 +2,6 @@ import {useEffect, useLayoutEffect, useRef, useState} from 'react';
|
||||
|
||||
import {resolveRuleWorldType} from '../../data/customWorldRuntime';
|
||||
import {MONSTERS_BY_WORLD, PLAYER_BASE_X_METERS} from '../../data/hostileNpcs';
|
||||
import {getWorldCampScenePreset} from '../../data/scenePresets';
|
||||
import {AnimationState, WorldType} from '../../types';
|
||||
import {GameCanvasEffectLayer} from './GameCanvasEffectLayer';
|
||||
import {GameCanvasEntityLayer} from './GameCanvasEntityLayer';
|
||||
@@ -51,8 +50,6 @@ export function GameCanvasRuntime({
|
||||
const resolvedWorldType = worldType ? resolveRuleWorldType(worldType) ?? WorldType.WUXIA : null;
|
||||
const backgroundSrc = currentScenePreset?.imageSrc
|
||||
|| (resolvedWorldType === WorldType.WUXIA ? '/scene_bg/45_PixelSky.png' : '/scene_bg/47_PixelSky.png');
|
||||
const campSceneId = worldType ? getWorldCampScenePreset(worldType)?.id ?? null : null;
|
||||
const showOpeningCampOverlay = Boolean(!inBattle && currentScenePreset?.id && currentScenePreset.id === campSceneId);
|
||||
const monsters = resolvedWorldType ? MONSTERS_BY_WORLD[resolvedWorldType] : [];
|
||||
const groundBottom = '18%';
|
||||
const stageLiftPx = 68;
|
||||
@@ -154,7 +151,6 @@ export function GameCanvasRuntime({
|
||||
backgroundSrc={backgroundSrc}
|
||||
currentScenePreset={currentScenePreset}
|
||||
resolvedWorldType={resolvedWorldType}
|
||||
showOpeningCampOverlay={showOpeningCampOverlay}
|
||||
sceneTitleSpinToken={sceneTitleSpinToken}
|
||||
onSceneNameClick={onSceneNameClick}
|
||||
onBackgroundLoadError={() => setBackgroundLoadFailed(true)}
|
||||
|
||||
@@ -3,17 +3,13 @@ import {AnimatePresence, motion} from 'motion/react';
|
||||
import {type ScenePresetInfo, WorldType} from '../../types';
|
||||
import {CHROME_ICONS, getNineSliceStyle, UI_CHROME} from '../../uiAssets';
|
||||
import {PixelIcon} from '../PixelIcon';
|
||||
import {
|
||||
OPENING_CAMP_OVERLAY_SRC,
|
||||
SCENE_TITLE_GEAR_FILTER,
|
||||
} from './GameCanvasShared';
|
||||
import { SCENE_TITLE_GEAR_FILTER } from './GameCanvasShared';
|
||||
|
||||
interface GameCanvasSceneLayerProps {
|
||||
backgroundLoadFailed: boolean;
|
||||
backgroundSrc: string;
|
||||
currentScenePreset: ScenePresetInfo | null;
|
||||
resolvedWorldType: WorldType | null;
|
||||
showOpeningCampOverlay: boolean;
|
||||
sceneTitleSpinToken: number;
|
||||
onSceneNameClick?: (() => void) | null;
|
||||
onBackgroundLoadError: () => void;
|
||||
@@ -24,7 +20,6 @@ export function GameCanvasSceneLayer({
|
||||
backgroundSrc,
|
||||
currentScenePreset,
|
||||
resolvedWorldType,
|
||||
showOpeningCampOverlay,
|
||||
sceneTitleSpinToken,
|
||||
onSceneNameClick = null,
|
||||
onBackgroundLoadError,
|
||||
@@ -55,19 +50,6 @@ export function GameCanvasSceneLayer({
|
||||
|
||||
<div className="pointer-events-none absolute inset-0 opacity-10 mix-blend-overlay [background-image:radial-gradient(circle_at_20%_20%,rgba(255,255,255,0.14),transparent_20%),radial-gradient(circle_at_80%_30%,rgba(255,255,255,0.08),transparent_18%),radial-gradient(circle_at_50%_80%,rgba(255,255,255,0.06),transparent_22%)]" />
|
||||
|
||||
{showOpeningCampOverlay && (
|
||||
<img
|
||||
src={OPENING_CAMP_OVERLAY_SRC}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute bottom-[9%] left-1/2 z-[1] w-[min(92%,980px)] -translate-x-1/2 object-contain opacity-95"
|
||||
style={{
|
||||
imageRendering: 'pixelated',
|
||||
filter: 'drop-shadow(0 12px 30px rgba(0, 0, 0, 0.42))',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentScenePreset && (
|
||||
<div className="absolute left-1/2 top-3 z-20 -translate-x-1/2">
|
||||
<motion.div
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, {useEffect, useState} from 'react';
|
||||
|
||||
import {getCharacterById} from '../../data/characterPresets';
|
||||
import {METERS_TO_PIXELS} from '../../data/hostileNpcs';
|
||||
import {buildMedievalNpcVisualFromCustomWorldVisual} from '../../data/medievalNpcVisuals';
|
||||
import {
|
||||
AnimationState,
|
||||
Character,
|
||||
@@ -14,6 +15,7 @@ import {
|
||||
WorldType,
|
||||
} from '../../types';
|
||||
import {CharacterAnimator} from '../CharacterAnimator';
|
||||
import {MedievalNpcAnimator} from '../MedievalNpcAnimator';
|
||||
|
||||
export type GameCanvasEntitySelection =
|
||||
| {kind: 'player'}
|
||||
@@ -66,7 +68,6 @@ export const GENERIC_NPC_EFFECT_TARGET_OFFSET_PX = -16;
|
||||
export const HOSTILE_NPC_SCENE_INSET_PX = 28;
|
||||
export const HOSTILE_NPC_SCENE_BOTTOM_OFFSET_PX = -18;
|
||||
export const CHAT_BUBBLE_SPRITE_SRC = '/chat.png';
|
||||
export const OPENING_CAMP_OVERLAY_SRC = '/scene_bg/hut.png';
|
||||
export const CHAT_BUBBLE_FRAME_WIDTH = 27;
|
||||
export const CHAT_BUBBLE_FRAME_HEIGHT = 22;
|
||||
export const CHAT_BUBBLE_FRAME_COUNT = 12;
|
||||
@@ -219,6 +220,17 @@ export function RoleCharacterSprite({
|
||||
state: AnimationState;
|
||||
facing: 'left' | 'right';
|
||||
}) {
|
||||
if (character.visual) {
|
||||
return (
|
||||
<MedievalNpcAnimator
|
||||
visualSpec={buildMedievalNpcVisualFromCustomWorldVisual(character.visual)}
|
||||
className="origin-bottom"
|
||||
scale={1.36}
|
||||
facing={facing}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full w-full" style={{transform: facing === 'left' ? 'scaleX(-1)' : undefined}}>
|
||||
<CharacterAnimator
|
||||
|
||||
Reference in New Issue
Block a user