Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
8 changes: 4 additions & 4 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: oven-sh/setup-bun@v2
with:
node-version: 22
- run: npm ci
- run: npm run lint
bun-version: 1.3.14
- run: bun install --frozen-lockfile
- run: bun run lint
10 changes: 6 additions & 4 deletions README-JP.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ BF6 Portal の TypeScript で、UI とスポーンオブジェクトをタイム

アニメーションスクリプト本体は、 `mods/bf6-*.ts` です。サンプル実装はこのリポジトリでは `mods/Samples.ts`、Portal から呼ばれるイベント関数は `mods/Script.ts` にあります。

Portal SDK 1.2.3.0 の型情報で、`mod.Wait`、UI 作成/更新 API、ボタンイベント、`mod.SpawnObject`、`mod.SetObjectTransform` などを確認しています。
Portal SDK 1.4.1.0 の型情報で、`mod.Wait`、UI 作成/更新 API、ボタンイベント、`mod.SpawnObject`、`mod.SetObjectTransform` などを確認しています。

## Questions / Support

Expand All @@ -19,7 +19,7 @@ https://discord.gg/Zy65k8AxH2
## できること

- `uiTimeline()` で UIWidget の位置、サイズ、背景色、透明度、文字、画像、ボタン色などを順番にアニメーションできます。
- `objectTimeline()` で `mod.Object` の位置、回転、有効状態をアニメーションできます。
- `objectTimeline()` で `mod.SetObjectTransform` が受け取るオブジェクト型の位置と回転をアニメーションできます。
- `to()` に配列を渡すと、複数の UI やオブジェクトを同じ時間で同時に動かせます。
- `wait()` で待ち時間、`call()` で任意処理、`loop` でループ再生を挟めます。
- `RuntimeObject` で親子関係を持つ複合オブジェクトを作り、親の移動や回転を子へ伝播できます。
Expand All @@ -42,7 +42,7 @@ mods/
Script.ts Portal のイベント関数。サンプルメニューを作ってボタン入力を受け取る
Samples.ts UI、オブジェクト、重力のサンプルアニメーション集
bf6-ui-animation.ts UIWidget 用タイムライン
bf6-object-animation.ts mod.Object と RuntimeObject 用タイムライン
bf6-object-animation.ts TransformableObject と RuntimeObject 用タイムライン
bf6-gravity.ts 簡易重力シミュレーション
bf6-easings.ts easing 関数
dist/
Expand Down Expand Up @@ -82,6 +82,8 @@ UI は `mod.AddUIContainer`、`mod.AddUIText`、`mod.AddUIImage`、`mod.AddUIBut

オブジェクトは `mod.SpawnObject(prefab, position, rotation, scale)` で生成し、`mod.SetObjectTransform(object, mod.CreateTransform(position, rotation))` で移動や回転を反映します。サンプルでは `mod.RuntimeSpawn_Common.Crate_01_A` を使っています。

SDK 1.4.1.0 では `mod.SetObjectTransform` がすべての `mod.Object` を受け取らなくなり、特に `Player` と `Vehicle` は対象外です。そのため、オブジェクトアニメーションと重力ヘルパーは SDK 関数の引数型から直接導出した `TransformableObject` を受け取ります。また、`mod.EnableSpatialObject` が SDK から削除されたため、従来の `enabled` トゥイーンプロパティも削除しました。

プレイヤー基準の位置計算では `mod.GetSoldierState(eventPlayer, mod.SoldierStateVector.GetPosition)` と `mod.GetSoldierState(eventPlayer, mod.SoldierStateVector.GetFacingDirection)` を使い、プレイヤーの前方に箱を出すようにしています。

## 基本コード
Expand Down Expand Up @@ -362,7 +364,7 @@ const timeline = objectTimeline()

![image9](./docs/image/sample9.gif)

通常の `mod.Object` に `GravityWorld` を接続して、前方へ投げるような放物線を作ります。ワールド座標の Y は上方向として扱っているため、重力は負の Y です。
`TransformableObject` に `GravityWorld` を接続して、前方へ投げるような放物線を作ります。ワールド座標の Y は上方向として扱っているため、重力は負の Y です。

```ts
const start = sampleObjectPoint(eventPlayer, -1.4, 1.1, 3);
Expand Down
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Animation helpers for BF6 Portal TypeScript. They let you animate UI and spawned

The animation script files are `mods/bf6-*.ts`. In this repository, the sample implementations are in `mods/Samples.ts`, and the Portal event entry points are in `mods/Script.ts`.

The API notes in this README were checked against Portal SDK 1.2.3.0 types, including `mod.Wait`, UI creation/update APIs, button events, `mod.SpawnObject`, and `mod.SetObjectTransform`.
The API notes in this README were checked against Portal SDK 1.4.1.0 types, including `mod.Wait`, UI creation/update APIs, button events, `mod.SpawnObject`, and `mod.SetObjectTransform`.

## Questions / Support

Expand All @@ -19,7 +19,7 @@ https://discord.gg/Zy65k8AxH2
## Features

- `uiTimeline()` animates UIWidget position, size, background color, alpha, text, image, and button color properties in sequence.
- `objectTimeline()` animates `mod.Object` position, rotation, and enabled state.
- `objectTimeline()` animates the position and rotation of object types accepted by `mod.SetObjectTransform`.
- Passing an array to `to()` animates multiple UI widgets or objects during the same timeline step.
- `wait()` adds a delay, `call()` runs custom logic, and `loop` repeats a timeline.
- `RuntimeObject` creates composite objects with parent-child relationships and propagates parent movement/rotation to children.
Expand All @@ -42,7 +42,7 @@ mods/
Script.ts Portal event functions. Creates the sample menu and handles button input
Samples.ts Sample UI, object, and gravity animations
bf6-ui-animation.ts Timeline implementation for UIWidget
bf6-object-animation.ts Timeline implementation for mod.Object and RuntimeObject
bf6-object-animation.ts Timeline implementation for TransformableObject and RuntimeObject
bf6-gravity.ts Lightweight gravity simulation
bf6-easings.ts Easing functions
dist/
Expand Down Expand Up @@ -82,6 +82,8 @@ UI is created with `mod.AddUIContainer`, `mod.AddUIText`, `mod.AddUIImage`, and

Objects are spawned with `mod.SpawnObject(prefab, position, rotation, scale)` and moved/rotated with `mod.SetObjectTransform(object, mod.CreateTransform(position, rotation))`. The sample uses `mod.RuntimeSpawn_Common.Crate_01_A`.

In SDK 1.4.1.0, `mod.SetObjectTransform` no longer accepts every `mod.Object`. In particular, `Player` and `Vehicle` are not transformable through this API. The object animation and gravity helpers therefore accept `TransformableObject`, derived directly from the SDK function parameter. The former `enabled` tween property was also removed because `mod.EnableSpatialObject` is no longer present in the SDK.

Player-relative object positions are calculated with `mod.GetSoldierState(eventPlayer, mod.SoldierStateVector.GetPosition)` and `mod.GetSoldierState(eventPlayer, mod.SoldierStateVector.GetFacingDirection)` so boxes can spawn in front of the player.

## Basic Code
Expand Down Expand Up @@ -359,7 +361,7 @@ const timeline = objectTimeline()

![image9](./docs/image/sample9.gif)

A normal `mod.Object` is connected to `GravityWorld` to create a forward throwing arc. In world coordinates, Y is treated as upward here, so gravity is negative Y.
A `TransformableObject` is connected to `GravityWorld` to create a forward throwing arc. In world coordinates, Y is treated as upward here, so gravity is negative Y.

```ts
const start = sampleObjectPoint(eventPlayer, -1.4, 1.1, 3);
Expand Down
6 changes: 3 additions & 3 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions mods/Samples.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { GravityWorld, objectGravityBody, runtimeObjectGravityBody, uiGravityBody } from "./bf6-gravity";
import { RuntimeObject, RuntimeObjectPrefab, objectTimeline } from "./bf6-object-animation";
import { RuntimeObject, RuntimeObjectPrefab, objectTimeline, type TransformableObject } from "./bf6-object-animation";
import { type VectorLike } from "./bf6-easings";
import { uiTimeline, type TweenProps, type UITimelineItem } from "./bf6-ui-animation";

Expand Down Expand Up @@ -138,13 +138,13 @@ function addText(name: string, parent: mod.UIWidget, messageKey: string, positio
}

// Spawns a sample object when a prefab is available.
function spawnSampleObject(prefab: SampleObjectPrefab | undefined, position: mod.Vector, rotation: mod.Vector, scale: mod.Vector): mod.Object | undefined {
function spawnSampleObject(prefab: SampleObjectPrefab | undefined, position: mod.Vector, rotation: mod.Vector, scale: mod.Vector): TransformableObject | undefined {
if (prefab === undefined) {
console.log("LOG> BF6 animation object sample skipped. Pass a RuntimeSpawn prefab to run this sample.");
return undefined;
}

return mod.SpawnObject(prefab, position, rotation, scale) as mod.Object;
return mod.SpawnObject(prefab, position, rotation, scale) as TransformableObject;
}

// Demonstrates object position tweening through several player-relative points.
Expand Down
6 changes: 3 additions & 3 deletions mods/bf6-gravity.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { VectorLike } from "./bf6-easings";
import { RuntimeObject } from "./bf6-object-animation";
import { RuntimeObject, type TransformableObject } from "./bf6-object-animation";

// Common interface used by gravity code to read and write positions.
export interface GravityTarget {
Expand Down Expand Up @@ -160,8 +160,8 @@ export function uiGravityBody(widget: mod.UIWidget, options?: GravityBodyOptions
}, options);
}

// Creates an adapter that treats a normal mod.Object as a GravityBody target.
export function objectGravityBody(object: mod.Object, options?: GravityBodyOptions): GravityBody {
// Creates an adapter that treats an SDK-transformable object as a GravityBody target.
export function objectGravityBody(object: TransformableObject, options?: GravityBodyOptions): GravityBody {
return new GravityBody({
getPosition: () => mod.GetObjectPosition(object),
setPosition: (position) => mod.SetObjectTransform(object, mod.CreateTransform(position, mod.GetObjectRotation(object))),
Expand Down
31 changes: 17 additions & 14 deletions mods/bf6-object-animation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { VectorLike, EaseFunction, EaseName, eases } from "./bf6-easings";
// Alias for the prefab type accepted by mod.SpawnObject.
export type RuntimeObjectPrefab = Parameters<typeof mod.SpawnObject>[0];

// Object types accepted by the SDK transform API. Player and Vehicle are intentionally excluded in SDK 1.4.1.0.
export type TransformableObject = Parameters<typeof mod.SetObjectTransform>[0];

// Internal quaternion tuple stored as [w, x, y, z].
type RuntimeObjectQuaternion = [number, number, number, number];

Expand Down Expand Up @@ -52,8 +55,8 @@ export interface RuntimeObjectTimelineItem {

// Runtime object that handles parent-child links, relative movement, and quaternion rotation.
export class RuntimeObject {
// Spawned mod.Object; undefined when no prefab was provided.
readonly object: mod.Object | undefined;
// Spawned transformable object; undefined when no prefab was provided.
readonly object: TransformableObject | undefined;
// ID of the mod.Object; undefined when no prefab was provided.
readonly id: number | undefined;
// Prefab used for spawning.
Expand Down Expand Up @@ -131,7 +134,7 @@ export class RuntimeObject {
mod.Add(this.runtimeObjectPos, RuntimeObject.runtimeObjectQRotateVector(this.offset, this.runtimeObjectRotState)),
this.runtimeObjectRotEulerOverride ?? RuntimeObject.runtimeObjectQToEuler(this.runtimeObjectRotState),
runtimeObjectToVector(scale),
) as mod.Object;
) as TransformableObject;
this.id = this.object === undefined ? undefined : mod.GetObjId(this.object);
} else {
this.object = undefined;
Expand Down Expand Up @@ -430,7 +433,7 @@ export class RuntimeObject {
}
}

// Properties that can be animated on a normal mod.Object.
// Properties that can be animated on an SDK-transformable object.
export interface ObjectTweenProps {
// Changes only the X coordinate.
x?: number;
Expand Down Expand Up @@ -482,15 +485,15 @@ export interface ObjectPhysicsOptions {

// Target and properties used when tweening multiple objects together.
export interface ObjectTimelineItem {
// mod.Object targeted by the animation.
target: mod.Object;
// SDK-transformable object targeted by the animation.
target: TransformableObject;
// Tween properties applied to the target.
props: ObjectTweenProps;
}

// API for queueing and playing Object and RuntimeObject animation steps in order.
export interface ObjectTimeline {
to(object: mod.Object, props: ObjectTweenProps, options?: ObjectTweenOptions): ObjectTimeline;
to(object: TransformableObject, props: ObjectTweenProps, options?: ObjectTweenOptions): ObjectTimeline;
to(items: ObjectTimelineItem[], options?: ObjectTweenOptions): ObjectTimeline;
to(object: RuntimeObject, props: RuntimeObjectTweenProps, options?: ObjectTweenOptions): ObjectTimeline;
to(items: RuntimeObjectTimelineItem[], options?: ObjectTweenOptions): ObjectTimeline;
Expand All @@ -513,7 +516,7 @@ type ObjectVectorTween = {

// Internal type for one queued ObjectTimeline step.
type ObjectTimelineStep =
| { type: "to"; object: mod.Object; props: ObjectTweenProps; options?: ObjectTweenOptions }
| { type: "to"; object: TransformableObject; props: ObjectTweenProps; options?: ObjectTweenOptions }
| { type: "toMany"; items: ObjectTimelineItem[]; options?: ObjectTweenOptions }
| { type: "runtimeTo"; items: RuntimeObjectTimelineItem[]; options?: ObjectTweenOptions }
| { type: "physics"; world: ObjectPhysicsStepper; options?: ObjectPhysicsOptions }
Expand Down Expand Up @@ -630,7 +633,7 @@ function buildObjectRotationTarget(current: mod.Vector, props: ObjectTweenProps)
}

// Converts ObjectTweenProps into vector tweens with current values.
function buildObjectTweens(object: mod.Object, props: ObjectTweenProps): ObjectVectorTween[] {
function buildObjectTweens(object: TransformableObject, props: ObjectTweenProps): ObjectVectorTween[] {
// List of vector tweens for position, rotation, and similar values.
const vectors: ObjectVectorTween[] = [];
// Current transform.
Expand Down Expand Up @@ -680,8 +683,8 @@ function applyObjectTweens(vectors: ObjectVectorTween[], amount: number): void {
}
}

// Runs a tween for one mod.Object.
async function runObjectTween(object: mod.Object, props: ObjectTweenProps, options: ObjectTweenOptions | undefined, shouldStop: () => boolean): Promise<void> {
// Runs a tween for one SDK-transformable object.
async function runObjectTween(object: TransformableObject, props: ObjectTweenProps, options: ObjectTweenOptions | undefined, shouldStop: () => boolean): Promise<void> {
// Playback duration in seconds.
const duration = options?.duration ?? objectDefaultTweenOptions.duration;
// Time interval between value updates.
Expand Down Expand Up @@ -713,7 +716,7 @@ async function runObjectTween(object: mod.Object, props: ObjectTweenProps, optio
}
}

// Runs tweens for multiple mod.Objects on the same timeline.
// Runs tweens for multiple SDK-transformable objects on the same timeline.
async function runManyObjectTweens(items: ObjectTimelineItem[], options: ObjectTweenOptions | undefined, shouldStop: () => boolean): Promise<void> {
// Playback duration in seconds.
const duration = options?.duration ?? objectDefaultTweenOptions.duration;
Expand Down Expand Up @@ -859,7 +862,7 @@ async function runObjectPhysics(world: ObjectPhysicsStepper, options: ObjectPhys
}

// Returns a small helper API for tweening a single object.
export function objectAnimate(object: mod.Object): { to(props: ObjectTweenProps, options?: ObjectTweenOptions): Promise<void> } {
export function objectAnimate(object: TransformableObject): { to(props: ObjectTweenProps, options?: ObjectTweenOptions): Promise<void> } {
return {
to(props: ObjectTweenProps, options?: ObjectTweenOptions) {
return runObjectTween(object, props, options, () => false);
Expand All @@ -878,7 +881,7 @@ export function objectTimeline(options?: ObjectTimelineOptions): ObjectTimeline

// ObjectTimeline control object returned to callers.
const api: ObjectTimeline = {
to(target: mod.Object | RuntimeObject | ObjectTimelineItem[] | RuntimeObjectTimelineItem[], propsOrOptions?: ObjectTweenProps | RuntimeObjectTweenProps | ObjectTweenOptions, tweenOptions?: ObjectTweenOptions) {
to(target: TransformableObject | RuntimeObject | ObjectTimelineItem[] | RuntimeObjectTimelineItem[], propsOrOptions?: ObjectTweenProps | RuntimeObjectTweenProps | ObjectTweenOptions, tweenOptions?: ObjectTweenOptions) {
if (Array.isArray(target)) {
if (target.length > 0 && target[0].target instanceof RuntimeObject) {
steps.push({ type: "runtimeTo", items: target as RuntimeObjectTimelineItem[], options: { ...options, ...(propsOrOptions as ObjectTweenOptions | undefined) } });
Expand Down
5 changes: 1 addition & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,5 @@
"bfportal-vitest-mock": "^0.0.12",
"typescript": "^5.9.3",
"typescript-eslint": "^8.61.1"
},
"dependencies": {
"modlib": "./code/modlib"
}
}
}
12 changes: 6 additions & 6 deletions test/animation.test.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { GravityBody, GravityWorld, objectGravityBody, runtimeObjectGravityBody, uiGravityBody } from "../mods/bf6-gravity";
import { uiAnimate, uiTimeline } from "../mods/bf6-ui-animation";
import { RuntimeObject, objectAnimate, objectTimeline } from "../mods/bf6-object-animation";
import { RuntimeObject, objectAnimate, objectTimeline, type TransformableObject } from "../mods/bf6-object-animation";
import { setupBfPortalMock, type BfPortalModMock } from "../test-support/bfportal-vitest-mock.generated";

type TestVector = { x: number; y: number; z: number };

let modMock: BfPortalModMock;
let widget: mod.UIWidget;
let secondWidget: mod.UIWidget;
let object: mod.Object;
let secondObject: mod.Object;
let object: TransformableObject;
let secondObject: TransformableObject;
let spawnedObjects: mod.Object[];

function fakeWidget(): mod.UIWidget {
return { __test: true } as unknown as mod.UIWidget;
}

function fakeObject(): mod.Object {
return { __test: true } as unknown as mod.Object;
function fakeObject(): TransformableObject {
return { __test: true } as unknown as TransformableObject;
}

function vector(x: number, y: number, z = 0): mod.Vector {
Expand Down Expand Up @@ -425,7 +425,7 @@ describe("objectTimeline", () => {
.play();

const transform = modMock.SetObjectTransform.mock.calls[0][1] as unknown as { position: mod.Vector; rotation: mod.Vector };
expect(vectorData(transform.rotation)).toEqual({ x: 0, y: visualYaw, z: 0 });
expect(vectorData(transform.rotation)).toEqual({ x: 0, y: expect.closeTo(visualYaw), z: 0 });
});

it("qRotateTo normalizes direct yaw euler before sending it to the game", async () => {
Expand Down
Loading
Loading