first commmit

This commit is contained in:
jakciehan
2026-05-06 08:17:32 +08:00
commit 427a33c55b
269 changed files with 20906 additions and 0 deletions
+101
View File
@@ -0,0 +1,101 @@
/**
* Project-wide constants. This module is platform-agnostic and MUST NOT import
* from `cc` so that it can be unit-tested under Jest.
*
* All numeric values are defined against the landscape baseline design
* resolution 960x540 (16:9). Physical screen adaptation is handled by the
* UI layer (see `@ui/FloatingControlLayer`).
*/
/** Landscape design resolution baseline width (px). */
export const DESIGN_WIDTH = 960;
/** Landscape design resolution baseline height (px). */
export const DESIGN_HEIGHT = 540;
/** Target frame rate (locked 30fps per performance requirement 18.1-18.3). */
export const TARGET_FPS = 30;
/** Max first-package size (bytes) per requirement 18.7. */
export const MAX_FIRST_PACKAGE_BYTES = 4 * 1024 * 1024;
/** Max audio bundle size (bytes) per requirement 16.5 / 19.7. */
export const MAX_AUDIO_BUNDLE_BYTES = 500 * 1024;
/** Max runtime memory peak (bytes) per requirement 18.4. */
export const MAX_MEMORY_PEAK_BYTES = 200 * 1024 * 1024;
/**
* Player character color states. Red = base (1-hit kill),
* Green = 1 crystal buff, Yellow = 2 crystals (faster movement).
* Per requirement 5.1-5.6.
*/
export enum PlayerColorState {
Red = 'red',
Green = 'green',
Yellow = 'yellow',
}
/** Horizontal movement speed (px/s) per color state, per requirement 5.1-5.2. */
export const MOVE_SPEED: Record<PlayerColorState, number> = {
[PlayerColorState.Red]: 100,
[PlayerColorState.Green]: 100,
[PlayerColorState.Yellow]: 150,
};
/** Standard vertical jump height (px) per requirement 2.2 (red/green baseline). */
export const JUMP_HEIGHT_STANDARD = 250;
/** Charged jump height (px) per requirement 2.3. */
export const JUMP_HEIGHT_CHARGED = 375;
/** Yellow-state jump height (px) per requirement 2.2. */
export const JUMP_HEIGHT_YELLOW = 300;
/** Crouch delay before actually leaving the ground (ms) per requirement 2.8. */
export const JUMP_PREPARE_DELAY_MS = 150;
/** Long-press threshold to trigger charged jump (ms) per requirement 2.3. */
export const JUMP_CHARGE_THRESHOLD_MS = 500;
/**
* Parabolic jump angle tolerance windows (degrees). A joystick direction that
* lies within ±ANGLE_TOLERANCE of 45° or 135° triggers a parabolic jump.
* Per requirement 2.5 and requirement 20.3 (>=95% recognition rate).
*/
export const PARABOLIC_ANGLE_RIGHT = 45;
export const PARABOLIC_ANGLE_LEFT = 135;
export const PARABOLIC_ANGLE_TOLERANCE = 15;
/** Weapon attack intervals (s). Per requirement 3.4 / 3.6. */
export const SHURIKEN_INTERVAL_BASE = 0.3;
export const SHURIKEN_INTERVAL_UPGRADED = 0.25;
export const SWORD_INTERVAL = 0.5;
/** Max shuriken burst count when long-pressing attack button (req 3.5). */
export const SHURIKEN_BURST_MAX = 3;
/** Combo-input recognition window (ms) for "jump + attack" per req 4.1. */
export const COMBO_INPUT_WINDOW_MS = 100;
/** Player invincibility frames duration (s) after a knockback per req 10.2. */
export const PLAYER_IFRAME_SECONDS = 0.5;
/**
* Performance KPI thresholds used by Logger / BI埋点 layer.
* Per requirement 20.1-20.6.
*/
export const PERF_TOUCH_RESPONSE_MAX_MS = 50;
export const PERF_JUMP_STATE_TOGGLE_MAX_MS = 50;
export const PERF_COMBO_RECOGNITION_MAX_MS = 100;
export const PERF_PARABOLIC_ANGLE_ACCURACY_TARGET = 0.95;
export const PERF_AIR_JUMP_BLOCK_RATE_TARGET = 0.99;
/** Local storage keys (req 17.1-17.5, 19.5). */
export const STORAGE_KEY = {
LevelUnlock: 'kl_level_unlock',
ControlLayout: 'kl_control_layout',
AudioVolume: 'kl_audio_volume',
TutorialDone: 'kl_tutorial_done',
StoryIntroSeen: 'kl_story_intro_seen',
} as const;
+9
View File
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "e42a8542-158c-4965-96fd-4b0cb6457b41",
"files": [],
"subMetas": {},
"userData": {}
}
+115
View File
@@ -0,0 +1,115 @@
/**
* A lightweight, framework-agnostic pub/sub event bus.
*
* Used project-wide to decouple:
* - UI layer (floating controls) → logic layer (player controller)
* - Logic layer (damage system) → UI layer (HUD / feedback)
* - Any manager → Logger / BI埋点
*
* Design notes:
* - `emit` is synchronous to avoid a frame of latency for combat events.
* - Subscribing the same callback twice is a no-op (idempotent) to avoid
* double-fire bugs when views are rebuilt.
* - `once` unsubscribes itself after the first invocation.
* - Handler errors are caught and forwarded to a user-provided error hook
* (defaulting to `console.error`) so that one bad listener cannot break
* the rest of the fan-out.
*/
export type EventHandler<TPayload = unknown> = (payload: TPayload) => void;
export type ErrorHook = (event: string, err: unknown) => void;
interface HandlerRecord {
fn: EventHandler<any>;
once: boolean;
}
export class EventBus {
private readonly handlers = new Map<string, HandlerRecord[]>();
private errorHook: ErrorHook = (event, err) => {
// Fallback error hook; replaced via `setErrorHook` once Logger is
// available.
// eslint-disable-next-line no-console
console.error(`[EventBus] handler for "${event}" threw:`, err);
};
/** Override the default error hook (used by Logger integration). */
public setErrorHook(hook: ErrorHook): void {
this.errorHook = hook;
}
/**
* Subscribe a handler. Idempotent — the same `fn` cannot be registered
* twice for the same event.
*/
public on<T>(event: string, fn: EventHandler<T>): void {
this.register(event, fn as EventHandler<any>, false);
}
/** Subscribe a handler that auto-unsubscribes after one invocation. */
public once<T>(event: string, fn: EventHandler<T>): void {
this.register(event, fn as EventHandler<any>, true);
}
/**
* Unsubscribe. If `fn` is omitted, all handlers for `event` are cleared.
*/
public off<T>(event: string, fn?: EventHandler<T>): void {
const list = this.handlers.get(event);
if (!list) {
return;
}
if (!fn) {
this.handlers.delete(event);
return;
}
const filtered = list.filter((r) => r.fn !== fn);
if (filtered.length === 0) {
this.handlers.delete(event);
} else {
this.handlers.set(event, filtered);
}
}
/** Synchronously dispatch `payload` to every handler registered for `event`. */
public emit<T>(event: string, payload?: T): void {
const list = this.handlers.get(event);
if (!list || list.length === 0) {
return;
}
// Snapshot first: `once` handlers will mutate `list` via `off`.
const snapshot = list.slice();
for (const record of snapshot) {
try {
record.fn(payload as unknown);
} catch (err) {
this.errorHook(event, err);
}
if (record.once) {
this.off(event, record.fn);
}
}
}
/** Return the number of handlers registered for `event`. */
public listenerCount(event: string): number {
return this.handlers.get(event)?.length ?? 0;
}
/** Remove every handler of every event (used in unit tests / scene unload). */
public clear(): void {
this.handlers.clear();
}
private register(event: string, fn: EventHandler<any>, once: boolean): void {
const list = this.handlers.get(event) ?? [];
if (list.some((r) => r.fn === fn)) {
return;
}
list.push({ fn, once });
this.handlers.set(event, list);
}
}
/** Shared, process-wide event bus. Tests should create a fresh `new EventBus()`. */
export const globalEventBus = new EventBus();
+9
View File
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "459040db-2675-4865-969c-fba50c3a40e1",
"files": [],
"subMetas": {},
"userData": {}
}
+170
View File
@@ -0,0 +1,170 @@
/**
* Minimal structured logger + performance-metric emitter.
*
* Two responsibilities live here to keep the common layer thin:
*
* 1. **Leveled logging** — wraps `console.*` with a monotonically increasing
* severity threshold so we can downgrade chatty modules in production.
* 2. **Performance埋点** — records named samples (e.g. touch→response latency)
* and computes p50/p95/avg for QA validation of requirement 20.1-20.6.
*
* Both halves are deliberately kept independent: the `metric()` API can be
* routed to a BI endpoint later without touching the logging API.
*/
export enum LogLevel {
Debug = 0,
Info = 1,
Warn = 2,
Error = 3,
Silent = 4,
}
export type LogSink = (level: LogLevel, module: string, msg: string, ...rest: unknown[]) => void;
export interface MetricSample {
/** Metric name (e.g. 'touch_response_ms'). */
name: string;
/** Numeric value (ms, count, %, etc.). */
value: number;
/** Optional tags for slicing (e.g. `{ button: 'jump' }`). */
tags?: Record<string, string | number>;
/** `realTime` timestamp, ms since app start. */
ts: number;
}
export interface MetricAggregate {
count: number;
min: number;
max: number;
avg: number;
p50: number;
p95: number;
}
export class Logger {
private threshold: LogLevel = LogLevel.Debug;
private sink: LogSink = (level, module, msg, ...rest) => {
const prefix = `[${LogLevel[level]}][${module}]`;
switch (level) {
case LogLevel.Error:
// eslint-disable-next-line no-console
console.error(prefix, msg, ...rest);
break;
case LogLevel.Warn:
// eslint-disable-next-line no-console
console.warn(prefix, msg, ...rest);
break;
case LogLevel.Info:
// eslint-disable-next-line no-console
console.info(prefix, msg, ...rest);
break;
default:
// eslint-disable-next-line no-console
console.log(prefix, msg, ...rest);
}
};
private readonly metrics = new Map<string, number[]>();
private readonly startedTimers = new Map<string, number>();
/** Control verbosity globally (call once at boot). */
public setLevel(level: LogLevel): void {
this.threshold = level;
}
/** Redirect logs (used in tests to assert against messages). */
public setSink(sink: LogSink): void {
this.sink = sink;
}
public debug(mod: string, msg: string, ...rest: unknown[]): void {
this.dispatch(LogLevel.Debug, mod, msg, rest);
}
public info(mod: string, msg: string, ...rest: unknown[]): void {
this.dispatch(LogLevel.Info, mod, msg, rest);
}
public warn(mod: string, msg: string, ...rest: unknown[]): void {
this.dispatch(LogLevel.Warn, mod, msg, rest);
}
public error(mod: string, msg: string, ...rest: unknown[]): void {
this.dispatch(LogLevel.Error, mod, msg, rest);
}
// ---------- metric API ----------
/** Record a single metric sample. */
public metric(sample: Omit<MetricSample, 'ts'>): void {
const list = this.metrics.get(sample.name) ?? [];
list.push(sample.value);
this.metrics.set(sample.name, list);
}
/** Start a named stopwatch. */
public timerStart(name: string, now: number = Logger.now()): void {
this.startedTimers.set(name, now);
}
/**
* Stop a named stopwatch and record its elapsed time (ms) under `name`.
* Returns the elapsed value or `undefined` if the timer was not started.
*/
public timerEnd(name: string, now: number = Logger.now()): number | undefined {
const start = this.startedTimers.get(name);
if (start === undefined) {
return undefined;
}
this.startedTimers.delete(name);
const elapsed = now - start;
this.metric({ name, value: elapsed });
return elapsed;
}
/** Compute aggregate stats (used by QA dashboards and test assertions). */
public aggregate(name: string): MetricAggregate | undefined {
const list = this.metrics.get(name);
if (!list || list.length === 0) {
return undefined;
}
const sorted = list.slice().sort((a, b) => a - b);
const count = sorted.length;
const sum = sorted.reduce((s, v) => s + v, 0);
const pct = (p: number): number => {
// Inclusive nearest-rank definition (matches common QA tools).
const rank = Math.min(count - 1, Math.max(0, Math.ceil((p / 100) * count) - 1));
return sorted[rank];
};
return {
count,
min: sorted[0],
max: sorted[count - 1],
avg: sum / count,
p50: pct(50),
p95: pct(95),
};
}
/** Clear all recorded metrics (useful between unit tests). */
public resetMetrics(): void {
this.metrics.clear();
this.startedTimers.clear();
}
private dispatch(level: LogLevel, mod: string, msg: string, rest: unknown[]): void {
if (level < this.threshold) {
return;
}
this.sink(level, mod, msg, ...rest);
}
private static now(): number {
if (typeof performance !== 'undefined' && typeof performance.now === 'function') {
return performance.now();
}
return Date.now();
}
}
/** Shared project-wide logger. */
export const globalLogger = new Logger();
+9
View File
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "6d7e102a-3d02-4d19-b555-17a7b19c965b",
"files": [],
"subMetas": {},
"userData": {}
}
+115
View File
@@ -0,0 +1,115 @@
/**
* Generic object pool used by damage effects, bullets, enemies and VFX
* (requirement 18.5). Pure-TS, platform-agnostic, Jest-testable.
*
* Design notes:
* - `factory` creates a brand-new instance when the free list is empty.
* - `resetter` is invoked on every released object, letting the caller wipe
* transient state (position, timers, listeners) before it goes back to
* the pool.
* - `maxSize` caps the retained instances; objects released beyond the cap
* are dropped (letting the GC collect them) to bound memory usage
* (requirement 18.4: memory peak ≤ 200MB).
* - Double-release is silently ignored but reported through `onDoubleRelease`
* so tests / Logger can assert correctness.
*/
export type ObjectFactory<T> = () => T;
export type ObjectResetter<T> = (obj: T) => void;
export interface ObjectPoolOptions<T> {
/** Required creator invoked when the pool is empty. */
factory: ObjectFactory<T>;
/** Optional cleaner invoked on every `release`. */
resetter?: ObjectResetter<T>;
/** Max retained objects; excess releases are discarded. Default 128. */
maxSize?: number;
/** Optional pre-warm count (creates this many objects upfront). Default 0. */
preAlloc?: number;
/** Optional diagnostic hook. */
onDoubleRelease?: (obj: T) => void;
}
export class ObjectPool<T> {
private readonly free: T[] = [];
private readonly borrowed = new Set<T>();
private readonly factory: ObjectFactory<T>;
private readonly resetter?: ObjectResetter<T>;
private readonly maxSize: number;
private readonly onDoubleRelease?: (obj: T) => void;
// Diagnostics
private _acquiredTotal = 0;
private _recycledTotal = 0;
private _createdTotal = 0;
constructor(options: ObjectPoolOptions<T>) {
this.factory = options.factory;
this.resetter = options.resetter;
this.maxSize = options.maxSize ?? 128;
this.onDoubleRelease = options.onDoubleRelease;
const preAlloc = options.preAlloc ?? 0;
for (let i = 0; i < preAlloc; i++) {
const inst = this.factory();
this._createdTotal++;
this.free.push(inst);
}
}
/** Acquire an object from the pool (creates one if empty). */
public acquire(): T {
this._acquiredTotal++;
const inst = this.free.pop();
if (inst !== undefined) {
this.borrowed.add(inst);
return inst;
}
const created = this.factory();
this._createdTotal++;
this.borrowed.add(created);
return created;
}
/** Release an object back to the pool. Double-releases are ignored. */
public release(obj: T): void {
if (!this.borrowed.has(obj)) {
this.onDoubleRelease?.(obj);
return;
}
this.borrowed.delete(obj);
this.resetter?.(obj);
if (this.free.length < this.maxSize) {
this.free.push(obj);
this._recycledTotal++;
}
// else: drop the object so the GC can reclaim it.
}
/** Number of objects currently held in the free list. */
public get freeCount(): number {
return this.free.length;
}
/** Number of objects that are currently out on loan. */
public get borrowedCount(): number {
return this.borrowed.size;
}
/** Drop everything. Used on scene unload. */
public drain(): void {
this.free.length = 0;
this.borrowed.clear();
}
/** Diagnostic stats (used by Logger / perf BI埋点). */
public stats() {
return {
free: this.freeCount,
borrowed: this.borrowedCount,
acquired: this._acquiredTotal,
recycled: this._recycledTotal,
created: this._createdTotal,
};
}
}
+9
View File
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "42063546-8ae6-4255-be58-f1d3f180ea42",
"files": [],
"subMetas": {},
"userData": {}
}
+105
View File
@@ -0,0 +1,105 @@
import { Logger, MetricAggregate } from './Logger';
import {
PERF_TOUCH_RESPONSE_MAX_MS,
PERF_JUMP_STATE_TOGGLE_MAX_MS,
PERF_COMBO_RECOGNITION_MAX_MS,
PERF_PARABOLIC_ANGLE_ACCURACY_TARGET,
PERF_AIR_JUMP_BLOCK_RATE_TARGET,
MAX_FIRST_PACKAGE_BYTES,
MAX_AUDIO_BUNDLE_BYTES,
MAX_MEMORY_PEAK_BYTES,
} from './Constants';
/**
* Performance monitor (task 10.2, req 18 & 20).
*
* Aggregates all the KPI samples recorded through `Logger.metric(...)` and
* reports pass/fail against every threshold listed in the requirements doc.
* CI can run `collectReport()` and assert that `allPassing === true`.
*/
export interface IPerfThreshold {
metric: string;
/** Budget target (max for latency, min for rates). */
limit: number;
comparator: '<=' | '>=';
requirementId: string;
}
export const CORE_PERF_THRESHOLDS: ReadonlyArray<IPerfThreshold> = [
{ metric: 'input/touchStart', limit: PERF_TOUCH_RESPONSE_MAX_MS, comparator: '<=', requirementId: 'req 20.1' },
{ metric: 'jump/state_toggle_ms', limit: PERF_JUMP_STATE_TOGGLE_MAX_MS, comparator: '<=', requirementId: 'req 20.2' },
{ metric: 'input/parabolic_accuracy', limit: PERF_PARABOLIC_ANGLE_ACCURACY_TARGET, comparator: '>=', requirementId: 'req 20.3' },
{ metric: 'input/combo_recognition_ms', limit: PERF_COMBO_RECOGNITION_MAX_MS, comparator: '<=', requirementId: 'req 20.4' },
{ metric: 'jump/air_jump_block_rate', limit: PERF_AIR_JUMP_BLOCK_RATE_TARGET, comparator: '>=', requirementId: 'req 20.5' },
];
export interface IPerfCheckResult {
threshold: IPerfThreshold;
aggregate?: MetricAggregate;
passing: boolean;
reason: string;
}
export interface IPerfReport {
allPassing: boolean;
checks: IPerfCheckResult[];
/** Optional build/runtime sizes filled in by CI (bytes). */
firstPackageBytes?: number;
audioBundleBytes?: number;
memoryPeakBytes?: number;
/** Top-level pass/fail for the size budgets. */
sizeBudgetPassing?: boolean;
}
export class PerfMonitor {
constructor(
private readonly logger: Logger,
private readonly thresholds: ReadonlyArray<IPerfThreshold> = CORE_PERF_THRESHOLDS
) {}
public collectReport(buildSizes?: {
firstPackageBytes?: number;
audioBundleBytes?: number;
memoryPeakBytes?: number;
}): IPerfReport {
const checks: IPerfCheckResult[] = this.thresholds.map((t) => this.check(t));
let sizeBudgetPassing: boolean | undefined;
if (buildSizes) {
sizeBudgetPassing =
(buildSizes.firstPackageBytes ?? 0) <= MAX_FIRST_PACKAGE_BYTES &&
(buildSizes.audioBundleBytes ?? 0) <= MAX_AUDIO_BUNDLE_BYTES &&
(buildSizes.memoryPeakBytes ?? 0) <= MAX_MEMORY_PEAK_BYTES;
}
const allPassing = checks.every((c) => c.passing) && (sizeBudgetPassing ?? true);
return {
allPassing,
checks,
firstPackageBytes: buildSizes?.firstPackageBytes,
audioBundleBytes: buildSizes?.audioBundleBytes,
memoryPeakBytes: buildSizes?.memoryPeakBytes,
sizeBudgetPassing,
};
}
private check(t: IPerfThreshold): IPerfCheckResult {
const agg = this.logger.aggregate(t.metric);
if (!agg) {
return {
threshold: t,
passing: false,
reason: `no samples recorded for "${t.metric}"`,
};
}
// For latency, use p95. For rate metrics, use avg.
const isRate = t.comparator === '>=';
const observed = isRate ? agg.avg : agg.p95;
const passing = isRate ? observed >= t.limit : observed <= t.limit;
return {
threshold: t,
aggregate: agg,
passing,
reason: `${t.metric} ${isRate ? 'avg' : 'p95'}=${observed.toFixed(2)} vs limit ${t.limit} (${t.comparator})`,
};
}
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "7da5826e-75ae-4fe1-aad7-f9096222ce93",
"files": [],
"subMetas": {},
"userData": {}
}
+123
View File
@@ -0,0 +1,123 @@
/**
* Local-storage facade used by:
* - Level unlock state (req 17.1)
* - Floating control layout (req 17.2)
* - BGM / SFX volume (req 17.3, 16.4)
* - Tutorial completion flags (req 17.4)
* - Story-intro seen flag (req 17.5 / 19.5)
*
* Rationale for the thin facade:
* - The WeChat Mini Game runtime exposes `wx.setStorageSync` while the
* in-editor / browser preview exposes `sys.localStorage`. We isolate
* both behind a single `IStorageDriver` interface so that switching
* platforms is a single line.
* - On read, a failure never throws: it returns the provided default value
* (req 17.6 — "must not crash if local storage is unreadable").
* - All values go through JSON serialisation so that structured objects
* round-trip without callers having to remember to stringify.
*/
export interface IStorageDriver {
getItem(key: string): string | null;
setItem(key: string, value: string): void;
removeItem(key: string): void;
}
/** Selects the best available driver at runtime. */
function detectDriver(): IStorageDriver {
// 1. WeChat Mini Game global
const wxGlobal = (globalThis as any).wx;
if (wxGlobal && typeof wxGlobal.setStorageSync === 'function') {
return {
getItem(key) {
try {
const v = wxGlobal.getStorageSync(key);
return v === '' ? null : (v as string);
} catch {
return null;
}
},
setItem(key, value) {
try {
wxGlobal.setStorageSync(key, value);
} catch {
// swallow — req 17.6
}
},
removeItem(key) {
try {
wxGlobal.removeStorageSync(key);
} catch {
// swallow
}
},
};
}
// 2. Browser localStorage
if (typeof globalThis !== 'undefined' && (globalThis as any).localStorage) {
const ls = (globalThis as any).localStorage as Storage;
return {
getItem: (k) => ls.getItem(k),
setItem: (k, v) => ls.setItem(k, v),
removeItem: (k) => ls.removeItem(k),
};
}
// 3. In-memory fallback (Jest, Node-only unit tests).
const mem = new Map<string, string>();
return {
getItem: (k) => (mem.has(k) ? (mem.get(k) as string) : null),
setItem: (k, v) => {
mem.set(k, v);
},
removeItem: (k) => {
mem.delete(k);
},
};
}
export class StorageMgr {
private driver: IStorageDriver;
constructor(driver?: IStorageDriver) {
this.driver = driver ?? detectDriver();
}
/**
* Read a JSON-serialisable value. Returns `defaultValue` if the key is
* missing, unparseable, or the underlying driver throws (req 17.6).
*/
public get<T>(key: string, defaultValue: T): T {
try {
const raw = this.driver.getItem(key);
if (raw == null) {
return defaultValue;
}
return JSON.parse(raw) as T;
} catch {
return defaultValue;
}
}
/** Write a JSON-serialisable value. Silently ignores driver errors. */
public set<T>(key: string, value: T): void {
try {
this.driver.setItem(key, JSON.stringify(value));
} catch {
// req 17.6
}
}
public remove(key: string): void {
this.driver.removeItem(key);
}
/** Swap the driver at runtime. Used in unit tests and platform ports. */
public setDriver(driver: IStorageDriver): void {
this.driver = driver;
}
}
/** Shared project-wide storage manager. */
export const globalStorageMgr = new StorageMgr();
+9
View File
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "580d04ad-4749-4b5a-ba2c-5da77a73126a",
"files": [],
"subMetas": {},
"userData": {}
}
+76
View File
@@ -0,0 +1,76 @@
/**
* Centralised time manager. Game-logic should read deltas from here instead
* of consuming raw `dt` from Cocos Creator components so that a single
* `pause()` call freezes gameplay without freezing UI (menu / settlement).
*
* Used by:
* - Pause menu, settings overlay, "公主被带走" cutscene (requirement 14.1).
* - Story intro cutscene (requirement 19.x) — UI time keeps ticking while
* gameplay time is held at zero.
*
* Two independent clocks are exposed:
* - `gameTime` : respects pause / time-scale, used by AI, physics, weapons.
* - `realTime` : ignores pause, used by UI animation, typewriter text, and
* Logger timestamps.
*/
export class TimeMgr {
private _gameTime = 0;
private _realTime = 0;
private _timeScale = 1;
private _paused = false;
/** Should be called once per frame (e.g. from a root node's `update`). */
public update(rawDt: number): void {
this._realTime += rawDt;
if (this._paused) {
return;
}
this._gameTime += rawDt * this._timeScale;
}
public pause(): void {
this._paused = true;
}
public resume(): void {
this._paused = false;
}
public get paused(): boolean {
return this._paused;
}
/** 1.0 = normal, 0.5 = slow-mo, 0 = hard freeze. Negative values clamped. */
public setTimeScale(scale: number): void {
this._timeScale = Math.max(0, scale);
}
public get timeScale(): number {
return this._timeScale;
}
public get gameTime(): number {
return this._gameTime;
}
public get realTime(): number {
return this._realTime;
}
/** Produce the scaled dt to pass into logic `update(dt)` calls. */
public scaledDelta(rawDt: number): number {
return this._paused ? 0 : rawDt * this._timeScale;
}
/** Reset all clocks (used between scenes and in unit tests). */
public reset(): void {
this._gameTime = 0;
this._realTime = 0;
this._timeScale = 1;
this._paused = false;
}
}
/** Shared project-wide time manager. */
export const globalTimeMgr = new TimeMgr();
+9
View File
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "cefd7ec2-a0d2-4494-b87d-981dcda9d5b2",
"files": [],
"subMetas": {},
"userData": {}
}
+13
View File
@@ -0,0 +1,13 @@
/**
* Common (platform-agnostic) utilities. Anything imported from this barrel
* file must stay free of `cc` dependencies so that it can be unit-tested
* under Jest (see `tests/__mocks__/cc.ts`).
*/
export * from './Constants';
export * from './EventBus';
export * from './ObjectPool';
export * from './TimeMgr';
export * from './StorageMgr';
export * from './Logger';
export * from './PerfMonitor';
+9
View File
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "b4c9b9e1-0b45-41d2-a05d-eda1098969a1",
"files": [],
"subMetas": {},
"userData": {}
}