add IFL texture animation

This commit is contained in:
Brian Beck 2025-12-01 22:33:12 -08:00
parent 25449af198
commit af17b43584
2506 changed files with 393603 additions and 6536 deletions

View file

@ -73,19 +73,11 @@ export function createBuiltins(
isobject(obj: any): boolean {
return runtime().$.isObject(obj);
},
typeof(obj: any): string {
if (obj == null) return "";
if (typeof obj === "object" && obj._class) return obj._className;
return typeof obj;
},
// Object lookup
nametoid(name: string): number {
return runtime().$.nameToId(name);
},
isfunction(name: string): boolean {
return runtime().$.isFunction(name);
},
// String functions
strlen(str: any): number {
@ -310,12 +302,6 @@ export function createBuiltins(
const max = Number(b) || 0;
return Math.floor(Math.random() * (max - min + 1)) + min;
},
getrandomseed(): number {
throw new Error("getRandomSeed() not implemented");
},
setrandomseed(_seed: any): void {
throw new Error("setRandomSeed() not implemented");
},
mdegtorad(deg: any): number {
return (Number(deg) || 0) * (Math.PI / 180);
},

View file

@ -1,5 +1,4 @@
import { describe, it, expect, vi } from "vitest";
import { readFileSync } from "node:fs";
import { createRuntime } from "./runtime";
import type { TorqueRuntimeOptions } from "./types";
import { parse, transpile } from "./index";
@ -7,9 +6,11 @@ import { parse, transpile } from "./index";
function run(script: string, options?: TorqueRuntimeOptions) {
const { $, $f, $g, state } = createRuntime(options);
const { code } = transpile(script);
const fn = new Function("$", "$f", "$g", code);
fn($, $f, $g);
return { $, $f, $g, state };
// Provide $l (locals) at module scope for top-level local variable access
const $l = $.locals();
const fn = new Function("$", "$f", "$g", "$l", code);
fn($, $f, $g, $l);
return { $, $f, $g, $l, state };
}
describe("TorqueScript Runtime", () => {
@ -78,6 +79,33 @@ describe("TorqueScript Runtime", () => {
`);
expect($g.get("result")).toBe(120);
});
it("handles top-level local variables", () => {
const { $g, $l } = run(`
%x = 5;
%y = 10;
$result = %x * %y;
%name = "test";
`);
expect($g.get("result")).toBe(50);
expect($l.get("x")).toBe(5);
expect($l.get("y")).toBe(10);
expect($l.get("name")).toBe("test");
});
it("top-level locals are separate from function locals", () => {
const { $g } = run(`
%x = 100;
function getX() {
%x = 42;
return %x;
}
$funcResult = getX();
$topResult = %x;
`);
expect($g.get("funcResult")).toBe(42);
expect($g.get("topResult")).toBe(100);
});
});
describe("string operations", () => {
@ -725,86 +753,95 @@ describe("TorqueScript Runtime", () => {
});
});
describe("real TorqueScript files", () => {
const emptyAST = parse("");
describe("complex scripts", () => {
it("sets globals and registers methods", () => {
const { $g, state } = run(`
$Game::numRoles = 3;
$Game::role0 = "Goalie";
$Game::role1 = "Defense";
$Game::role2 = "Offense";
async function runFile(filepath: string) {
const source = readFileSync(filepath, "utf8");
// Provide a loader that returns empty scripts for known dependencies
const runtime = createRuntime({
loadScript: async (path) => {
// Return empty script for known dependencies
if (path.toLowerCase().includes("aitdm")) return "";
return null;
},
});
const script = await runtime.loadFromSource(source);
script.execute();
return {
$: runtime.$,
$f: runtime.$f,
$g: runtime.$g,
state: runtime.state,
};
}
function MyGame::onStart(%game) {
return "started";
}
`);
it("transpiles and executes TR2Roles.cs", async () => {
const { $g, state } = await runFile(
"docs/base/@vl2/TR2final105-server.vl2/scripts/TR2Roles.cs",
);
// Verify globals were set
expect($g.get("TR2::numRoles")).toBe(3);
expect($g.get("TR2::role0")).toBe("Goalie");
expect($g.get("TR2::role1")).toBe("Defense");
expect($g.get("TR2::role2")).toBe("Offense");
// Verify methods were registered
expect(state.methods.has("TR2Game")).toBe(true);
expect($g.get("Game::numRoles")).toBe(3);
expect($g.get("Game::role0")).toBe("Goalie");
expect($g.get("Game::role1")).toBe("Defense");
expect($g.get("Game::role2")).toBe("Offense");
expect(state.methods.has("MyGame")).toBe(true);
});
it("transpiles and executes a .mis mission file", async () => {
const { $, state } = await runFile(
"docs/base/@vl2/4thGradeDropout.vl2/missions/4thGradeDropout.mis",
);
it("creates nested object hierarchies like mission files", () => {
const { $, state } = run(`
new SimGroup(MissionGroup) {
new TerrainBlock(Terrain) {
size = 1024;
};
new Sky(Sky) {
cloudSpeed = 1.5;
};
new SimGroup(PlayerDropPoints) {
new SpawnSphere(Spawn1) { position = "0 0 100"; };
new SpawnSphere(Spawn2) { position = "100 0 100"; };
};
};
`);
// Verify MissionGroup was created
const missionGroup = $.deref("MissionGroup");
expect(missionGroup).toBeDefined();
expect(missionGroup._className).toBe("SimGroup");
// Verify child objects were created
const terrain = $.deref("Terrain");
expect(terrain).toBeDefined();
const sky = $.deref("Sky");
expect(sky).toBeDefined();
// Verify object count
expect(state.objectsByName.size).toBeGreaterThan(10);
expect($.deref("Terrain")).toBeDefined();
expect($.deref("Sky")).toBeDefined();
expect($.deref("Spawn1")).toBeDefined();
expect($.deref("Spawn2")).toBeDefined();
expect(state.objectsByName.size).toBe(6);
});
it("transpiles TDMGame.cs with methods and parent calls", async () => {
const source = readFileSync(
"docs/base/@vl2/z_DMP2-V0.6.vl2/scripts/TDMGame.cs",
"utf-8",
);
const runtime = createRuntime({
loadScript: async (path) => {
if (path.toLowerCase().includes("aitdm")) return "";
return null;
},
});
const script = await runtime.loadFromSource(source);
script.execute();
it("supports namespace method calls", () => {
const { $g } = run(`
function BaseGame::getValue(%game) {
return 10;
}
// Verify methods were registered on TDMGame
expect(runtime.state.methods.has("TDMGame")).toBe(true);
const tdmMethods = runtime.state.methods.get("TDMGame");
expect(tdmMethods).toBeDefined();
function BaseGame::getDoubled(%game) {
return BaseGame::getValue(%game) * 2;
}
// Verify transpiled code contains parent calls
const { code } = transpile(source);
$base = BaseGame::getValue(0);
$doubled = BaseGame::getDoubled(0);
`);
expect($g.get("base")).toBe(10);
expect($g.get("doubled")).toBe(20);
});
it("supports parent:: in package overrides", () => {
const { $g } = run(`
function doSomething() {
return 10;
}
package MyOverride {
function doSomething() {
return Parent::doSomething() + 5;
}
};
$result = doSomething();
`);
expect($g.get("result")).toBe(15);
});
it("generates parent calls in transpiled code", () => {
const { code } = transpile(`
function MyGame::onEnd(%game) {
Parent::onEnd(%game);
}
`);
expect(code).toContain("$.parent(");
});
});

View file

@ -758,8 +758,10 @@ export function createRuntime(
function executeAST(ast: Program): void {
const code = getOrGenerateCode(ast);
const execFn = new Function("$", "$f", "$g", code);
execFn($, $f, $g);
// Provide $l (locals) at module scope for top-level local variable access
const $l = createLocals();
const execFn = new Function("$", "$f", "$g", "$l", code);
execFn($, $f, $g, $l);
}
function createLoadedScript(ast: Program, path?: string): LoadedScript {