Update Healer Man server
This commit is contained in:
1 parent
6cc1b9217f
commit
6114a2295f
67 files changed
+6241
-347
No files matched your search
@@ -72,6 +72,15 @@ const selected: CharacterAppearance = {
|
||||
};
|
||||
|
||||
describe("resolveAvatarAppearance", () => {
|
||||
it("keeps authentic RoM weapons and clothing when the optional native body falls back to WoW", () => {
|
||||
const resolved = resolveAvatarAppearance(manifest, entry, selected, "rom-scout");
|
||||
expect(resolved.equipmentModels.filter((model) => model.attachment === "handLeft")).toEqual([
|
||||
expect.objectContaining({ sourceFrame: "runewaker", url: expect.stringContaining("starter-bow.glb") }),
|
||||
]);
|
||||
expect(resolved.bodyLayers.length).toBeGreaterThan(9);
|
||||
expect(resolved.equipmentModels.some((model) => model.attachment === "shoulderLeft")).toBe(true);
|
||||
});
|
||||
|
||||
it("composes body layers in WoW replacement-texture order", () => {
|
||||
const resolved = resolveAvatarAppearance(manifest, entry, selected, "priest");
|
||||
expect(resolved.bodyLayers.slice(0, 9).map((layer) => layer.url)).toEqual([
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { CharacterAppearance, ClassId } from "../app/characterCatalog";
|
||||
import { isRomClassId, type CharacterAppearance, type ClassId } from "../app/characterCatalog";
|
||||
import type { InventoryItem } from "../game/lootTypes";
|
||||
import type {
|
||||
AvatarManifest,
|
||||
@@ -60,9 +60,22 @@ export function resolveAvatarAppearance(
|
||||
const featureTextures = tripletAt(feature?.byColor ?? [], hairColorIndex);
|
||||
const regions = manifest.textureLayout.regions;
|
||||
const layers: AvatarTextureLayer[] = [];
|
||||
const equipped = entry.rendererStrategy?.kind === "runewaker-modular" && classId
|
||||
const nativeBody = entry.rendererStrategy?.kind === "runewaker-modular";
|
||||
const bodyEquipment = nativeBody && classId
|
||||
? resolveRunewakerEquipment(classId, equipment)
|
||||
: resolveEquipmentAppearance(equipment, entry.gender, classId);
|
||||
// Class identity survives the optional character pack's basic-human fallback.
|
||||
const romWeapons = classId && isRomClassId(classId)
|
||||
? resolveRunewakerEquipment(classId, equipment)
|
||||
: null;
|
||||
const equipped = romWeapons && !nativeBody ? {
|
||||
...bodyEquipment,
|
||||
cacheKey: `${bodyEquipment.cacheKey}:${romWeapons.cacheKey}`,
|
||||
models: [
|
||||
...bodyEquipment.models.filter((model) => model.attachment === "shoulderLeft" || model.attachment === "shoulderRight"),
|
||||
...romWeapons.models,
|
||||
],
|
||||
} : bodyEquipment;
|
||||
|
||||
if (skin?.base) addLayer(layers, skin.base, regions.base);
|
||||
addLayer(layers, skin?.underwear?.[0] ?? "", regions.pelvisUpper);
|
||||
|
||||
@@ -36,7 +36,7 @@ export interface EquipmentModelVisual {
|
||||
readonly attachment: EquipmentAttachmentName;
|
||||
readonly fallbackKind?: "bow" | "dagger" | "mace" | "shield" | "staff" | "sword";
|
||||
readonly weaponStance?: CharacterWeaponStance;
|
||||
/** Native RoM props already use the authored RoM socket frame. */
|
||||
/** Original RoM prop frame, adapted separately from the body's socket frame. */
|
||||
readonly sourceFrame?: "runewaker";
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
avatarWeaponSocketRotation,
|
||||
findAvatarHandBone,
|
||||
findAvatarShieldBone,
|
||||
runewakerWeaponSocketRotation,
|
||||
StarterWeaponVisual,
|
||||
type AvatarHand,
|
||||
type StarterWeaponRotation,
|
||||
@@ -42,6 +43,34 @@ const EQUIPMENT_ATTACHMENT_NAMES: readonly EquipmentAttachmentName[] = Object.fr
|
||||
]);
|
||||
const NO_ROTATION: StarterWeaponRotation = Object.freeze([0, 0, 0]);
|
||||
|
||||
/** Native props need the inverse socket adaptation when the optional rig is absent. */
|
||||
export function equipmentWeaponSocketRotation(
|
||||
entry: AvatarManifestEntry,
|
||||
itemModel: EquipmentModelVisual,
|
||||
): StarterWeaponRotation {
|
||||
if (!itemModel.fallbackKind || !itemModel.weaponStance) return NO_ROTATION;
|
||||
if (itemModel.sourceFrame !== "runewaker") {
|
||||
return avatarWeaponSocketRotation(entry, itemModel.fallbackKind, itemModel.weaponStance);
|
||||
}
|
||||
if (entry.rendererStrategy?.kind === "runewaker-modular") return NO_ROTATION;
|
||||
const nativeRotation = runewakerWeaponSocketRotation(
|
||||
itemModel.fallbackKind,
|
||||
itemModel.weaponStance,
|
||||
);
|
||||
return [-nativeRotation[0], -nativeRotation[1], -nativeRotation[2]];
|
||||
}
|
||||
|
||||
export function equipmentWeaponModelRotation(
|
||||
entry: AvatarManifestEntry,
|
||||
itemModel: EquipmentModelVisual,
|
||||
): StarterWeaponRotation {
|
||||
// The original bow is Y-long; the basic human bow socket expects the adapted Z axis.
|
||||
return itemModel.sourceFrame === "runewaker" && itemModel.fallbackKind === "bow"
|
||||
&& entry.rendererStrategy?.kind !== "runewaker-modular"
|
||||
? [Math.PI / 2, 0, 0]
|
||||
: NO_ROTATION;
|
||||
}
|
||||
|
||||
export type EquipmentAttachmentTransforms = Readonly<
|
||||
Partial<Record<EquipmentAttachmentName, AvatarAttachmentTransform>>
|
||||
>;
|
||||
@@ -144,9 +173,7 @@ function AttachedEquipmentModel({
|
||||
materials.forEach((material) => material.dispose());
|
||||
}, [scene]);
|
||||
|
||||
const socketRotation = itemModel.sourceFrame !== "runewaker" && itemModel.fallbackKind && itemModel.weaponStance
|
||||
? avatarWeaponSocketRotation(entry, itemModel.fallbackKind, itemModel.weaponStance)
|
||||
: NO_ROTATION;
|
||||
const socketRotation = equipmentWeaponSocketRotation(entry, itemModel);
|
||||
|
||||
return createPortal(
|
||||
<group
|
||||
@@ -154,7 +181,9 @@ function AttachedEquipmentModel({
|
||||
rotation={socketRotation}
|
||||
scale={transform.scale}
|
||||
>
|
||||
<primitive object={scene} />
|
||||
<group rotation={equipmentWeaponModelRotation(entry, itemModel)}>
|
||||
<primitive object={scene} />
|
||||
</group>
|
||||
</group>,
|
||||
transform.target,
|
||||
);
|
||||
|
||||
@@ -1,11 +1,45 @@
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { Euler, Quaternion, Vector3 } from "three";
|
||||
import { CLASSES } from "../app/characterCatalog";
|
||||
import type { InventoryItem } from "../game/lootTypes";
|
||||
import { resolveRunewakerEquipment } from "./runewakerEquipment";
|
||||
import { equipmentWeaponModelRotation, equipmentWeaponSocketRotation } from "./equipmentAttachments";
|
||||
import { avatarWeaponSocketRotation } from "./starterWeapons";
|
||||
import type { AvatarManifest } from "./characterManifest";
|
||||
|
||||
describe("original RoM equipment", () => {
|
||||
it("aligns the original bow to the basic body's grip without reversing its native facing", () => {
|
||||
const native = (JSON.parse(readFileSync("public/assets/characters/runewaker-v1/manifest.json", "utf8")) as AvatarManifest).entries["rom-human:male"];
|
||||
const basic = { ...native, rendererStrategy: undefined };
|
||||
const bow = resolveRunewakerEquipment("rom-scout", []).models[0];
|
||||
expect(equipmentWeaponModelRotation(native, bow)).toEqual([0, 0, 0]);
|
||||
const axis = new Vector3(0, 1, 0)
|
||||
.applyEuler(new Euler(...equipmentWeaponModelRotation(basic, bow)))
|
||||
.applyEuler(new Euler(...equipmentWeaponSocketRotation(basic, bow)));
|
||||
expect(axis.distanceTo(new Vector3(1, 0, 0))).toBeLessThan(1e-6);
|
||||
const stringSide = new Vector3(0, 0, -1).applyEuler(new Euler(...equipmentWeaponModelRotation(basic, bow)));
|
||||
expect(stringSide.y).toBeCloseTo(1);
|
||||
for (const model of resolveRunewakerEquipment("rom-knight", []).models) {
|
||||
expect(equipmentWeaponModelRotation(native, model)).toEqual([0, 0, 0]);
|
||||
expect(equipmentWeaponModelRotation(basic, model)).toEqual([0, 0, 0]);
|
||||
}
|
||||
});
|
||||
|
||||
it("converts native props back to the basic body's socket frame when its pack is absent", () => {
|
||||
const native = (JSON.parse(readFileSync("public/assets/characters/runewaker-v1/manifest.json", "utf8")) as AvatarManifest).entries["rom-human:male"];
|
||||
const basic = { ...native, rendererStrategy: undefined };
|
||||
for (const classId of ["rom-knight", "rom-mage", "rom-scout", "rom-rogue"] as const) {
|
||||
for (const model of resolveRunewakerEquipment(classId, []).models) {
|
||||
expect(equipmentWeaponSocketRotation(native, model)).toEqual([0, 0, 0]);
|
||||
const nativeRotation = new Quaternion().setFromEuler(new Euler(...avatarWeaponSocketRotation(native, model.fallbackKind!, model.weaponStance!)));
|
||||
const fallbackRotation = new Quaternion().setFromEuler(new Euler(...equipmentWeaponSocketRotation(basic, model)));
|
||||
expect(nativeRotation.multiply(fallbackRotation).angleTo(new Quaternion())).toBeLessThan(1e-6);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("equips every RoM class with source-client props in its native socket frame", () => {
|
||||
for (const characterClass of CLASSES.filter((entry) => entry.id.startsWith("rom-"))) {
|
||||
const resolved = resolveRunewakerEquipment(characterClass.id, []);
|
||||
|
||||
@@ -329,6 +329,14 @@ export function avatarWeaponSocketRotation(
|
||||
if (entry?.rendererStrategy?.kind !== "runewaker-modular" || kind === "shield") {
|
||||
return ZERO_ROTATION;
|
||||
}
|
||||
return runewakerWeaponSocketRotation(kind, stance);
|
||||
}
|
||||
|
||||
export function runewakerWeaponSocketRotation(
|
||||
kind: StarterWeaponKind,
|
||||
stance: StarterWeaponStance,
|
||||
): StarterWeaponRotation {
|
||||
if (kind === "shield") return ZERO_ROTATION;
|
||||
return stance === "two-handed" || stance === "two-handed-left"
|
||||
? RUNEWAKER_TWO_HAND_SOCKET_ROTATION
|
||||
: RUNEWAKER_ONE_HAND_SOCKET_ROTATION;
|
||||
|
||||
Reference in new issue
Block a user