diff --git a/src/release-package-dialog.ts b/src/release-package-dialog.ts
index 8e0c16f..de5e459 100644
--- a/src/release-package-dialog.ts
+++ b/src/release-package-dialog.ts
@@ -27,6 +27,7 @@ export interface ReleasePackageDialogOptions {
}
interface StoredRelease {
+ target: ReleaseTarget;
metadata: ArenaMetadata;
readme: string;
license: string;
@@ -34,10 +35,13 @@ interface StoredRelease {
includeSource: boolean;
}
+export type ReleaseTarget = 'map' | 'game';
+
let releaseDialogRequest = 0;
function defaults(mapName: string): StoredRelease {
return {
+ target: 'map',
metadata: {
title: mapName, gameTypes: ['ffa'], botSupport: true,
recommendedPlayers: '', author: '', description: '',
@@ -49,6 +53,12 @@ function defaults(mapName: string): StoredRelease {
};
}
+export function gameReleaseBuildIssue(target: ReleaseTarget, build: BuildRecord | null): string | null {
+ if (target !== 'game' || !build) return null;
+ if (!build.aas) return 'Maker game levels require a successful AAS bot-navigation stage. Recompile with Generate AAS enabled.';
+ return null;
+}
+
function readStored(editor: Editor, mapName: string): StoredRelease {
const source = editor.worldspawn.properties[RELEASE_METADATA_KEY];
if (!source) return defaults(mapName);
@@ -253,6 +263,17 @@ export async function openReleasePackageDialog(options: ReleasePackageDialogOpti
const filesSection = document.createElement('section');
filesSection.innerHTML = '
Release files
';
+ const targetInput = document.createElement('select');
+ for (const [value, label] of [
+ ['map', 'Quake 3 map package'],
+ ['game', 'Maker game level (BSP + AAS + source)'],
+ ] as const) {
+ const option = document.createElement('option');
+ option.value = value;
+ option.textContent = label;
+ targetInput.appendChild(option);
+ }
+ targetInput.value = stored.target;
const readmeInput = textarea(stored.readme);
const licenseInput = textarea(stored.license);
const attributionInput = textarea(stored.attribution);
@@ -262,6 +283,7 @@ export async function openReleasePackageDialog(options: ReleasePackageDialogOpti
const fileGrid = document.createElement('div');
fileGrid.className = 'release-fields';
fileGrid.append(
+ field('Export target', targetInput),
field('README', readmeInput),
field('License', licenseInput),
field('Attribution', attributionInput),
@@ -298,7 +320,22 @@ export async function openReleasePackageDialog(options: ReleasePackageDialogOpti
);
body.append(metadataSection, levelshotSection, filesSection, auditSection, packageSection);
+ const applyTargetDefaults = () => {
+ const gameTarget = targetInput.value === 'game';
+ if (gameTarget) {
+ includeSource.checked = true;
+ botInput.checked = true;
+ }
+ includeSource.disabled = gameTarget;
+ const issue = gameReleaseBuildIssue(targetInput.value as ReleaseTarget, build);
+ packageStatus.textContent = issue ?? (build ? 'Ready to validate and build.' : 'A compiled BSP is required.');
+ packageStatus.className = issue ? 'error' : '';
+ };
+ targetInput.onchange = applyTargetDefaults;
+ applyTargetDefaults();
+
const collectStored = (): StoredRelease => ({
+ target: targetInput.value as ReleaseTarget,
metadata: {
title: titleInput.value.trim(),
gameTypes: gameTypesInput.value.split(/[\s,]+/).filter(Boolean),
@@ -323,6 +360,8 @@ export async function openReleasePackageDialog(options: ReleasePackageDialogOpti
if (!build?.bsp) return null;
saveMetadata();
try {
+ const gameBuildIssue = gameReleaseBuildIssue(targetInput.value as ReleaseTarget, build);
+ if (gameBuildIssue) throw new Error(gameBuildIssue);
const activeTextureManager = editor.textureManager ?? textureManager;
const packageResult = buildReleasePackage({
mapName,
diff --git a/tests/release-package-dialog.test.ts b/tests/release-package-dialog.test.ts
index 4801af2..9acb63a 100644
--- a/tests/release-package-dialog.test.ts
+++ b/tests/release-package-dialog.test.ts
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest';
import { buildSourceFingerprint, type BuildRecord } from '../src/build-history';
-import { selectReleaseBuild } from '../src/release-package-dialog';
+import { gameReleaseBuildIssue, selectReleaseBuild } from '../src/release-package-dialog';
function build(documentRevision: number, source?: string, region = false): BuildRecord {
return {
@@ -36,4 +36,11 @@ describe('release package build selection', () => {
expect(selectReleaseBuild([build(2, source)], 3, buildSourceFingerprint(`${source}// changed`))).toBeNull();
expect(selectReleaseBuild([build(3, source, true)], 3, buildSourceFingerprint(source))).toBeNull();
});
+
+ it('requires AAS for Maker game-level exports', () => {
+ const withoutAas = build(2);
+ expect(gameReleaseBuildIssue('map', withoutAas)).toBeNull();
+ expect(gameReleaseBuildIssue('game', withoutAas)).toMatch(/require.*AAS/i);
+ expect(gameReleaseBuildIssue('game', { ...withoutAas, aas: new Uint8Array([1]) })).toBeNull();
+ });
});
diff --git a/tests/release-package.test.ts b/tests/release-package.test.ts
index 474d775..1d48318 100644
--- a/tests/release-package.test.ts
+++ b/tests/release-package.test.ts
@@ -48,7 +48,7 @@ describe('release packaging', () => {
mapName: 'test_map', bsp: strToU8('bsp'), aas: strToU8('aas'), entities: [world],
assets, textures: textureAdapter(assets),
metadata: { title: 'Test Arena', gameTypes: ['ffa', 'team'], botSupport: true, recommendedPlayers: '2-4', author: 'Mapper', description: 'Test' },
- levelshot: strToU8('png'), files: { readme: 'read me' },
+ levelshot: strToU8('png'), files: { readme: 'read me' }, includeSourceMap: '{\n"classname" "worldspawn"\n}\n',
};
const first = buildReleasePackage(input);
const second = buildReleasePackage(input);
@@ -56,6 +56,8 @@ describe('release packaging', () => {
const files = unzipSync(first.pk3);
expect(Object.keys(files)).toEqual([...Object.keys(files)].sort((a, b) => a.localeCompare(b)));
expect(files['maps/test_map.bsp']).toBeTruthy();
+ expect(files['maps/test_map.aas']).toBeTruthy();
+ expect(files['maps/test_map.map']).toBeTruthy();
expect(files['scripts/test_map.arena']).toBeTruthy();
expect(files['textures/custom/wall.tga']).toBeTruthy();
expect(first.report.archiveValidation.valid).toBe(true);