Skip to content
Open
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
121 changes: 121 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# Agent Guidelines for Kroz Development

## Getting Started

### Running the Project

```bash
npm install
npm start # Development server at http://localhost:5173
npm run build # Production build
```

### Key Files and Directories

- **`src/main.ts`** - Entry point
- **`src/modules/`** - Core game modules (engine, levels, world, screen, etc.)
- **`src/data/`** - Game data (levels, tilesets, colors)
- **`src/utils/`** - Utilities (procgen, kroz parsing, tiled)
- **`src/constants/`** - Game constants
- **`src/systems/`** - Game systems (player, mobs, effects)
- **`levels/`** - Level files (txt, json)

### Adding a New Level/Game

1. **Create level file** in `levels/<game>/level-name.txt`:

```text
[PF]
# Your ASCII map here (64x22)
# Characters: #=wall, space=floor, P=player, S=stairs, +=door, etc.

[ST]
##FlashEntity
Your intro message here!
```

2. **Create game module** in `src/data/<game>/index.ts`:

```typescript
export const title = 'Game Name';

export const LEVELS = [
async () => (await import('./level-1.txt?raw')).default
];

export async function readLevel(i: number): Promise<Level> { ... }
export function findNextLevel(i: number) { return ++i; }
export function findPrevLevel(i: number) { return Math.max(--i, 0); }
export async function start() { ... }
```

3. **Register game** in `src/modules/games.ts`:

- Import your game module
- Add to `Games` enum
- Add to `games` object

4. **Set starting game** in `src/constants/constants.ts`:

```typescript
export const STARTING_GAME = Games.YOUR_GAME;
```

### ASCII Tile Reference

| Char | Tile |
| ---- | ---------- |
| `#` | Wall |
| ` ` | Floor |
| `P` | Player |
| `>` | Stairs |
| `+` | Door |
| `K` | Key |
| `*` | Gem |
| `$` | Nugget |
| `W` | Whip |
| `C` | Chest |
| `T` | Teleport |
| `S` | Slow mob |
| `M` | Medium mob |
| `F` | Fast mob |

### Procedural Generation

The project uses BSP (Binary Space Partitioning) and Brogue algorithms in `src/utils/procgen.ts`. Levels are defined with layers:

- `LayerType.BSP` - Room generation
- `LayerType.Brogue` - Rogue-like room generation
- `LayerType.CA` - Cellular automata
- `LayerType.Generator` - Entity placement

### Common Tile Types (from `src/constants/types.ts`)

- `Type.Floor`, `Type.Wall`, `Type.Block`
- `Type.Player`, `Type.Stairs`, `Type.Door`, `Type.Key`
- `Type.Gem`, `Type.Nugget`, `Type.Whip`, `Type.Chest`
- `Type.Slow`, `Type.Medium`, `Type.Fast` (mobs)
- `Type.SlowTime`, `Type.SpeedTime`, `Type.Freeze` (spells)

### Testing

- Run `npm start` to test in browser
- Use debug mode (enabled in dev) to access debug controls
- Check console for errors

### Code Style

- ESLint + Prettier enforced on commit
- Use TypeScript types from `src/constants/types.ts`
- Follow existing patterns in similar modules

### Commit Process

```bash
git checkout -b feature-branch
# Make changes
git add .
git commit -m "Description"
git push -u origin feature-branch
gh pr create --title "..." --body "..."
```
43 changes: 43 additions & 0 deletions levels/custom/intro.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
[PF]


#######################
# #
# ####### ####### #
# # # # # #
# # $ # # * # # #######
# # # # # # # # # #
# ####### ####### # # $ #
# # # # #
########### ####### ####### #######
# # # # # #
# $* # # * # # $ # #########
# # # # # # # # # # #
# # ####### ####### # $ * #
########## # # #
# #########
##### #
# # ##### # #######
# $ # # # # # #
# # # # K # # # $ #
######### ####### # # # # # #
# ##### # ####### #
# $ * $ * $ * $ * #
# # # # # # # #
# # #
##########+##########################+########



[ST]
##FlashEntity
Welcome to THE FORGOTTEN ADVENTURES OF KROZ!

You are in the Entry Chamber. Find the KEY (K) to unlock the door.
Collect GEMS (*) and NUGGETS ($) for points.
Avoid the SLOW mobs (S) - they move every 3 turns.
Use WHIPS (W) to destroy enemies from a distance.

Your quest: Find the KEY, open the door, reach the STAIRWAY (>)!

Press any key to begin.
4 changes: 3 additions & 1 deletion src/constants/constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Games } from '../modules/games';

export const DEBUG = import.meta.env.DEV;
export const SHOW_STATS = false;
export const SHOW_DEBUG_CONTROLS = DEBUG;
Expand Down Expand Up @@ -36,4 +38,4 @@ export const BLINK = false; // !REDUCED;

export const FLOOR_CHAR = ' ';

export const STARTING_GAME = 'procgen';
export const STARTING_GAME = Games.CUSTOM;
196 changes: 196 additions & 0 deletions src/data/custom/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
// Custom Game - Intro level + Procedural levels

import * as tiles from '../../modules/tiles';
import * as display from '../../modules/display';
import * as controls from '../../modules/controls';
import * as screen from '../../modules/screen';

import type { Level } from '../../modules/levels';
import { Type } from '../../constants/types';
import {
BrogueLayer,
generateMap,
GeneratorLayer,
LayerType,
LevelDefinition
} from '../../utils/procgen';
import { HEIGHT } from '../../constants/constants';
import { Color } from '../../modules/colors';
import dedent from 'ts-dedent';
import { delay } from '../../utils/utils';

export const title = 'Custom Adventure';

const LEVELS = [async () => (await import('./intro.txt?raw')).default];

export async function readLevel(i: number): Promise<Level> {
if (i === 0) {
const levelData = await LEVELS[0]();
return parseKrozTxt(levelData);
}
const level = Levels[i - 1] as LevelDefinition;
const { mapData } = await generateMap(level, i);

return {
id: '' + i,
data: mapData.toArray(),
startTrigger: '##FlashEntity\nPress any key to begin this level.',
properties: {}
};
}

export function findNextLevel(i: number) {
if (i === 0) return 1;
return ++i;
}

export function findPrevLevel(i: number) {
return Math.max(i - 1, 0);
}

function parseKrozTxt(level: string): Level {
let data: tiles.Entity[] = [];
let startTrigger = '';
let lineIndex = 0;

const lines = level
.split(/\r?\n/)
.filter((line) => line.length > 0)
.map((line) => line.trimEnd());

while (lineIndex < lines.length) {
const line = lines[lineIndex++].trim();
if (line === '[PF]') {
data = readLevelMap();
} else if (line === '[ST]') {
startTrigger = readStartTrigger();
}
}

const WIDTH = 64;
const HEIGHT = 22;
const SIZE = WIDTH * HEIGHT;

for (let i = data.length; i < SIZE; i++) {
if (!data[i]) {
data[i] = tiles.createEntityOfType(Type.Floor);
}
}

return {
id: '0',
data,
startTrigger: startTrigger || undefined,
properties: {}
};

function readLevelMap() {
const result: tiles.Entity[] = [];
for (let y = 0; y < HEIGHT; y++) {
const line = (lines[lineIndex++] || '').trimEnd().padEnd(WIDTH, ' ');
for (let x = 0; x < line.length; x++) {
const char = line.charAt(x) || ' ';
const tileId = char.charCodeAt(0);
result.push(tiles.createEntityFromTileId(tileId, x, y));
}
}
return result;
}

function readStartTrigger() {
let trigger = '';
while (lineIndex < lines.length) {
const line = lines[lineIndex++];
if (line.startsWith('[')) {
lineIndex--;
break;
}
trigger += line + '\n';
}
return trigger;
}
}

const randomPlayer = {
type: LayerType.Generator,
count: 1,
generator: (map, x, y) =>
map.set(x, y, tiles.createEntityOfType(Type.Player, x, y))
} satisfies GeneratorLayer;

const randomStairs = {
type: LayerType.Generator,
count: 1,
generator: (map, x, y) =>
map.set(x, y, tiles.createEntityOfType(Type.Stairs, x, y))
} satisfies GeneratorLayer;

const gemGenerator = {
type: LayerType.Generator,
count: ({ depth }) => Math.max(3, 8 - depth),
generator: (map, x, y) =>
map.set(x, y, tiles.createEntityOfType(Type.Gem, x, y))
} satisfies GeneratorLayer;

const slowMobGenerator = {
type: LayerType.Generator,
count: ({ depth }) => Math.max(1, 3 - Math.floor(depth / 5)),
generator: (map, x, y) =>
map.set(x, y, tiles.createEntityOfType(Type.Slow, x, y))
} satisfies GeneratorLayer;

const Levels = [
{
layers: [
{
type: LayerType.BSP,
min_width: 6,
min_height: 4,
max_width: 12,
max_height: 8,
keys: 0
} as BrogueLayer,
randomPlayer,
randomStairs,
gemGenerator,
slowMobGenerator
]
}
] satisfies LevelDefinition[];

export async function start() {
await introScreen();
await screen.renderTitle();
}

export async function introScreen() {
display.clear(Color.Black);

display.writeCenter(HEIGHT - 1, 'Custom Adventure', Color.HighIntensityWhite);

display.writeCenter(HEIGHT - 1, 'Press any key to continue', Color.White);

await controls.repeatUntilKeyPressed(async () => {
display.drawText(
8,
6,
dedent`
╔═══════════════════════════════════════════╗
║ THE CUSTOM ADVENTURE ║
║ ║
║ Level 1: The Entry Chamber ║
║ - Find the key to unlock the door ║
║ - Collect gems and nuggets ║
║ - Reach the stairs to continue ║
║ ║
║ Level 2+: Procedural Challenges ║
║ - Randomly generated dungeons ║
║ - Increasing difficulty ║
╚═══════════════════════════════════════════╝
`,
Color.Cyan
);

await delay(500);
});
}
Loading