# PawaScript

A TypeScript-based novel game script engine for chat-style visual novels.

## Features

- Event-driven storytelling with conditional skit selection
- Structured triggers and actions for game state management
- Character system with expressions and avatars
- Minigame integration with async result handling
- YAML-based scenario files for easy authoring
- UI-independent core engine with presentation events

## Installation

```bash
npm install
npm run build
```

## Quick Start

```typescript
import {
  StateStore,
  ScenarioRepository,
  CharacterRepository,
  EventRunner,
  createInitialGameState,
} from 'pawascript';

// Initialize game state
const state = new StateStore(createInitialGameState());

// Load characters
const characters = new CharacterRepository();
characters.loadFromYaml(`
characters:
  protagonist:
    default_display_name: 主人公
    chat_side: right
    expressions:
      neutral: /assets/protagonist/neutral.png
      happy: /assets/protagonist/happy.png
`);

// Load scenarios
const scenarios = new ScenarioRepository();
await scenarios.loadFromFile('./scenarios/opening.yaml');

// Run an event
const runner = new EventRunner(undefined, characters);
const result = runner.runEvent('opening_event', state, scenarios);

// Process presentation events
for (const event of result.presentationEvents) {
  console.log(event);
}
```

## Core Concepts

### Events

An **event** represents a timing point in game progression (e.g., "after lesson", "before exam"). Each event contains multiple skits.

```yaml
event_id: lesson_after_01
display_name: After Lesson Event 01
category: lesson_after
skits:
  - skit_id: teacher_praise
    # ...
```

### Skits

A **skit** is a single conversation/scene unit selected based on conditions. Only one skit is played per event execution.

```yaml
skit_id: teacher_praise
priority: 100
trigger:
  all:
    - flag: met_teacher
    - gte:
        path: stats.math
        value: 70
actions:
  - set: flags.praised_by_teacher
    value: true
script:
  steps:
    - say: "Great job on the test!"
      speaker: teacher
```

### Selection Rules

1. Filter skits by trigger conditions
2. Exclude already-seen skits (if `once_per_playthrough: true`)
3. Select highest priority skits
4. Random pick among ties

### Scripts & Steps

A **script** contains an array of **steps** - individual display or control commands.

## Step Types

### say

Display character dialogue.

```yaml
- say: "Hello!"
  speaker: teacher
  expression: happy
  display_name: "Ms. Tanaka"  # Optional override
```

### think

Display internal thoughts.

```yaml
- think: "I wonder what she means..."
  speaker: protagonist
  expression: troubled
```

### narrate

Display narration text (no speaker).

```yaml
- narrate: "The classroom fell silent."
```

### system

Display system messages.

```yaml
- system: "Affection +1"
```

### background

Change background image.

```yaml
- background: classroom_evening
  transition: fade
```

### time_pass

Display time passage.

```yaml
- time_pass: "A few hours later..."
```

### date_change

Display date change and update calendar.

```yaml
- date_change: "2026-04-15"
```

### speaker_intro

Update character display name.

```yaml
- speaker_intro: teacher
  display_name: "Ms. Tanaka"
```

### transition

Play screen transition effect.

```yaml
- transition: battle_like
  duration_ms: 500
```

### minigame

Launch external minigame.

```yaml
- minigame: quiz_game
  params:
    subject: math
    difficulty: normal
  result_key: minigame.quiz_result
```

## Triggers

Triggers determine when a skit is eligible for selection.

### Logical Operators

```yaml
# All conditions must be true
trigger:
  all:
    - flag: intro_completed
    - flag: met_teacher

# Any condition must be true
trigger:
  any:
    - flag: route_a
    - flag: route_b

# Negate a condition
trigger:
  not:
    flag: game_over
```

### Comparisons

```yaml
trigger:
  all:
    - path: stats.math
      gte: 70
    - path: affection.teacher
      lt: 50
```

Operators: `eq`, `ne`, `gt`, `gte`, `lt`, `lte`

### Flag Checks

```yaml
trigger:
  all:
    - flag: met_teacher       # Must be true
    - flag_false: seen_event  # Must be false/undefined
```

### Set Operations

```yaml
trigger:
  all:
    - path: progress.chapter
      in: [chapter1, chapter2]
    - path: inventory
      contains: key_item
```

## Actions

Actions modify game state after skit completion.

```yaml
actions:
  # Set a value
  - set: flags.event_seen
    value: true

  # Add to number
  - add: affection.teacher
    value: 5

  # Increment/decrement
  - inc: stats.lesson_count
  - dec: stamina.current

  # Array operations
  - append: inventory
    value: new_item
  - remove: inventory
    value: used_item

  # Event control
  - unlock_event: special_event
  - lock_event: old_event

  # Mark skit as seen
  - set_seen: skit_id_here
```

## Characters

Define characters in a separate YAML file.

```yaml
# characters.yaml
characters:
  protagonist:
    default_display_name: 主人公
    chat_side: right
    expressions:
      neutral: /assets/avatars/protagonist/neutral.png
      happy: /assets/avatars/protagonist/happy.png
      troubled: /assets/avatars/protagonist/troubled.png

  teacher:
    default_display_name: "???"
    chat_side: left
    expressions:
      neutral: /assets/avatars/teacher/neutral.png
      happy: /assets/avatars/teacher/happy.png
      stern: /assets/avatars/teacher/stern.png
```

### Display Name Resolution

1. Step's `display_name` field (if specified)
2. Current name in StateStore (set by `speaker_intro`)
3. Character's `default_display_name`

### Expression Resolution

1. Step's `expression` field (if specified)
2. Default to `neutral`
3. Fallback to first available expression

## Minigame Integration

For scripts with minigames, use async execution:

```typescript
const result = await runEventAsync('exam_event', state, scenarios, {
  characterRepository: characters,
  onMinigame: async (minigameId, params) => {
    // Launch your minigame UI and wait for result
    const score = await launchQuizGame(params);
    return { score, passed: score >= 60 };
  },
});

// Minigame result is now in state
const quizResult = state.get('minigame.quiz_result');
// { score: 85, passed: true }
```

Reference results in subsequent triggers:

```yaml
trigger:
  all:
    - path: minigame.quiz_result.score
      gte: 80
```

## API Reference

### StateStore

```typescript
const state = new StateStore(createInitialGameState());

state.get('path.to.value');           // Get value
state.set('path.to.value', newValue); // Set value
state.add('stats.hp', 10);            // Add to number
state.inc('counter');                 // Increment by 1
state.dec('counter');                 // Decrement by 1
state.append('array', item);          // Add to array
state.remove('array', item);          // Remove from array

state.hasSeenSkit('skit_id');         // Check if seen
state.markSeenSkit('skit_id');        // Mark as seen

state.getSpeakerDisplayName('id');    // Get display name
state.setSpeakerDisplayName('id', 'Name'); // Set display name

state.snapshot();                     // Get state copy
```

### ScenarioRepository

```typescript
const repo = new ScenarioRepository();

repo.loadFromYaml(yamlString);        // Load from string
await repo.loadFromFile(path);        // Load from file
await repo.loadFromDirectory(dir);    // Load all YAML in directory

repo.getEvent('event_id');            // Get event data
repo.hasEvent('event_id');            // Check if exists
repo.listEvents();                    // List all events
```

### CharacterRepository

```typescript
const chars = new CharacterRepository();

chars.loadFromYaml(yamlString);
await chars.loadFromFile(path);

chars.getCharacter('id');
chars.hasCharacter('id');
chars.getDefaultDisplayName('id');
chars.getChatSide('id');
chars.resolveExpression('id', 'happy');
chars.hasExpression('id', 'happy');
chars.listCharacters();
```

### EventRunner

```typescript
// Sync execution (minigames not executed)
const runner = new EventRunner(seed, characterRepository);
const result = runner.runEvent(eventId, state, scenarios);

// Async execution with minigame support
const result = await runner.runEventAsync(eventId, state, scenarios);
```

### EventRunResult

```typescript
interface EventRunResult {
  success: boolean;
  eventId: string;
  skitId?: string;
  presentationEvents: PresentationEvent[];
  requiresExternalFlow: boolean;
  minigameEvents: MinigameInfo[];
  actionResult?: ActionExecutionResult;
  error?: string;
}
```

### Presentation Events

```typescript
type PresentationEvent =
  | { type: 'say'; speaker: string; displayName: string; text: string; expression?: string; avatarUrl?: string; chatSide?: 'left' | 'right' }
  | { type: 'think'; speaker?: string; displayName?: string; text: string; expression?: string; avatarUrl?: string; chatSide?: 'left' | 'right' }
  | { type: 'narrate'; text: string }
  | { type: 'system'; text: string }
  | { type: 'background'; background: string; transition?: string }
  | { type: 'time_pass'; description: string }
  | { type: 'date_change'; date: string }
  | { type: 'speaker_intro'; speakerId: string; displayName: string }
  | { type: 'transition'; transitionId: string; durationMs?: number }
  | { type: 'minigame'; minigameId: string; params?: Record<string, unknown>; resultKey?: string }
```

### Validation

```typescript
import { validateEvent, validateCharacter } from 'pawascript';

const result = validateEvent(eventData);
if (!result.valid) {
  console.error(result.errors);
}
```

## Project Structure

```
scenario/
  characters.yaml
  opening/
    opening.yaml
  lesson/
    lesson_before_01.yaml
    lesson_after_01.yaml
  exam/
    mock_exam.yaml
```

## Complete Example

```yaml
# lesson_after_01.yaml
event_id: lesson_after_01
version: 1
display_name: After Lesson Event 01
category: lesson_after
skit_defaults:
  once_per_playthrough: true

skits:
  - skit_id: teacher_praise
    priority: 100
    trigger:
      all:
        - flag: met_teacher
        - path: stats.math
          gte: 70
    actions:
      - set: flags.praised
        value: true
      - add: affection.teacher
        value: 1
    script:
      steps:
        - background: classroom_evening

        - speaker_intro: teacher
          display_name: Ms. Tanaka

        - say: "Great job on today's quiz!"
          speaker: teacher
          expression: happy

        - think: "She praised me... I feel happy."
          speaker: protagonist
          expression: happy

        - narrate: "The teacher nodded with satisfaction."

        - system: "Affection +1"

  - skit_id: generic_lesson_end
    priority: 0
    once_per_playthrough: false
    trigger:
      all: []
    script:
      steps:
        - narrate: "The lesson ended quietly."
```

## License

MIT
