docs
This commit is contained in:
@@ -11,13 +11,22 @@ import { EquipmentSlot } from '../../items/equipment-slot.enum';
|
||||
import { ItemDefinition } from '../../items/entities/item-definition.entity';
|
||||
import { LootTableEntry } from '../../loot/entities/loot-table-entry.entity';
|
||||
import { LootTable } from '../../loot/entities/loot-table.entity';
|
||||
import { CharacterLootBag } from '../../loot-bags/entities/character-loot-bag.entity';
|
||||
import { LootBagDefinition } from '../../loot-bags/entities/loot-bag-definition.entity';
|
||||
import { EncounterType } from '../../monsters/entities/encounter-type.enum';
|
||||
import { MonsterCategory } from '../../monsters/monster-category.enum';
|
||||
import { LocationMonster } from '../../monsters/entities/location-monster.entity';
|
||||
import { MonsterDefinition } from '../../monsters/entities/monster-definition.entity';
|
||||
import { LocationConnection } from '../../world/entities/location-connection.entity';
|
||||
import { LocationDefinition } from '../../world/entities/location-definition.entity';
|
||||
import { ReputationFaction } from '../../reputation/entities/reputation-faction.entity';
|
||||
import { TurnInDefinition } from '../../turn-in/entities/turn-in-definition.entity';
|
||||
import { RenownMilestoneDefinition } from '../../renown/entities/renown-milestone-definition.entity';
|
||||
import { ExchangeRule } from '../../exchanges/entities/exchange-rule.entity';
|
||||
import { NpcExchangeProfile } from '../../exchanges/entities/npc-exchange-profile.entity';
|
||||
import { DialogueNode } from '../../npcs/entities/dialogue-node.entity';
|
||||
import { NpcDefinition } from '../../npcs/entities/npc-definition.entity';
|
||||
import { NpcShop } from '../../shops/entities/npc-shop.entity';
|
||||
import { ShopOffer } from '../../shops/entities/shop-offer.entity';
|
||||
import {
|
||||
ITEM_DEFINITIONS,
|
||||
LOOT_TABLES,
|
||||
@@ -25,15 +34,30 @@ import {
|
||||
} from './item-content';
|
||||
import {
|
||||
ASH_RAT_LOOT_TABLE_ID,
|
||||
CHARRED_LOOTER_LOOT_TABLE_ID,
|
||||
ITEM_IDS,
|
||||
ROAD_BANDIT_LOOT_TABLE_ID,
|
||||
WILD_ROAD_DOG_LOOT_TABLE_ID,
|
||||
} from './item.constants';
|
||||
import {
|
||||
BURNED_ROAD_LOCAL_CONTENT,
|
||||
SOUTH_GATE_LOCAL_CONTENT,
|
||||
} from './local-location.content';
|
||||
import {
|
||||
BASIC_HIDE_BAG_ID,
|
||||
BASIC_TROPHY_POUCH_ID,
|
||||
LOOT_BAG_DEFINITIONS,
|
||||
} from './loot-bag-content';
|
||||
import { REPUTATION_FACTIONS } from './reputation-content';
|
||||
import { TURN_IN_DEFINITIONS } from './turn-in-content';
|
||||
import {
|
||||
DIALOGUE_NODES,
|
||||
EXCHANGE_RULES,
|
||||
NPC_DEFINITIONS,
|
||||
NPC_EXCHANGE_PROFILES,
|
||||
NPC_SHOPS,
|
||||
RENOWN_MILESTONES,
|
||||
SHOP_OFFERS,
|
||||
} from './npc-content';
|
||||
import {
|
||||
ASH_RAT_MONSTER_ID,
|
||||
BURNED_ROAD_ID,
|
||||
@@ -54,9 +78,20 @@ export async function seedVisibleVerticalSlice(
|
||||
const itemRepository = dataSource.getRepository(ItemDefinition);
|
||||
const lootTableRepository = dataSource.getRepository(LootTable);
|
||||
const lootEntryRepository = dataSource.getRepository(LootTableEntry);
|
||||
const lootBagDefinitionRepository =
|
||||
dataSource.getRepository(LootBagDefinition);
|
||||
const reputationFactionRepository =
|
||||
dataSource.getRepository(ReputationFaction);
|
||||
const turnInDefinitionRepository = dataSource.getRepository(TurnInDefinition);
|
||||
const renownMilestoneRepository = dataSource.getRepository(
|
||||
RenownMilestoneDefinition,
|
||||
);
|
||||
const npcDefinitionRepository = dataSource.getRepository(NpcDefinition);
|
||||
const dialogueNodeRepository = dataSource.getRepository(DialogueNode);
|
||||
const npcShopRepository = dataSource.getRepository(NpcShop);
|
||||
const shopOfferRepository = dataSource.getRepository(ShopOffer);
|
||||
const npcExchangeProfileRepository =
|
||||
dataSource.getRepository(NpcExchangeProfile);
|
||||
const exchangeRuleRepository = dataSource.getRepository(ExchangeRule);
|
||||
|
||||
const locations = [
|
||||
{
|
||||
@@ -136,20 +171,27 @@ export async function seedVisibleVerticalSlice(
|
||||
'lootTableId',
|
||||
'itemDefinitionId',
|
||||
]);
|
||||
await lootBagDefinitionRepository.upsert(LOOT_BAG_DEFINITIONS, ['key']);
|
||||
await reputationFactionRepository.upsert(REPUTATION_FACTIONS, ['key']);
|
||||
await turnInDefinitionRepository.upsert(TURN_IN_DEFINITIONS, ['key']);
|
||||
await renownMilestoneRepository.upsert(RENOWN_MILESTONES, ['key']);
|
||||
|
||||
// Burned Road roster (Playable Slice 0.7 V2 §2, §4). Each enemy has to be
|
||||
// mechanically and economically distinct, so the differences live in
|
||||
// `abilities` (what it does in a fight) and `lootTableId` (what it is worth)
|
||||
// rather than in engine or UI branches.
|
||||
const monsters = [
|
||||
{
|
||||
id: ASH_RAT_MONSTER_ID,
|
||||
key: 'ash-rat',
|
||||
name: 'Ash Rat',
|
||||
monsterCategory: MonsterCategory.BEAST,
|
||||
level: 1,
|
||||
maxHp: 45,
|
||||
attack: 5,
|
||||
armor: 0,
|
||||
silverMin: 0,
|
||||
silverMax: 0,
|
||||
flavorText: 'Scrawny and quick, it feeds on whatever the fires left.',
|
||||
// Pure baseline combat: no telegraph, no status effect, short fight.
|
||||
abilities: {},
|
||||
artworkPath: '/images/monsters/ash-rat.png',
|
||||
iconPath: '/images/combat/icons/ash-rat-128.png',
|
||||
lootTableId: ASH_RAT_LOOT_TABLE_ID,
|
||||
@@ -158,28 +200,39 @@ export async function seedVisibleVerticalSlice(
|
||||
id: WILD_ROAD_DOG_MONSTER_ID,
|
||||
key: 'wild-road-dog',
|
||||
name: 'Feral Road Hound',
|
||||
monsterCategory: MonsterCategory.BEAST,
|
||||
level: 1,
|
||||
maxHp: 55,
|
||||
attack: 7,
|
||||
armor: 0,
|
||||
silverMin: 0,
|
||||
silverMax: 0,
|
||||
flavorText:
|
||||
'It stopped waiting for scraps a long time ago. Its bites do not close.',
|
||||
// Introduces Bleeding: every third round its bite also opens a wound
|
||||
// that ticks for two rounds. Windows of clean rounds in between keep
|
||||
// the effect readable instead of permanent.
|
||||
abilities: {
|
||||
bleed: { roundInterval: 3, damagePerRound: 5, durationRounds: 2 },
|
||||
},
|
||||
artworkPath: '/images/monsters/wild-road-dog.png',
|
||||
iconPath: '/images/combat/icons/wild-road-dog-128.png',
|
||||
// Shares the beast table: both are scorched road animals that leave a
|
||||
// pelt behind. A table of its own waits for content that differs.
|
||||
lootTableId: ASH_RAT_LOOT_TABLE_ID,
|
||||
lootTableId: WILD_ROAD_DOG_LOOT_TABLE_ID,
|
||||
},
|
||||
{
|
||||
id: ROAD_BANDIT_MONSTER_ID,
|
||||
key: 'road-bandit',
|
||||
name: 'Road Bandit',
|
||||
monsterCategory: MonsterCategory.HUMANOID,
|
||||
level: 2,
|
||||
maxHp: 75,
|
||||
attack: 9,
|
||||
armor: 5,
|
||||
silverMin: 0,
|
||||
silverMax: 0,
|
||||
flavorText:
|
||||
'This one has done it before, and winds up the swing where you can see it.',
|
||||
// Reinforces telegraphing: winds up a Heavy Strike every third round,
|
||||
// which Shield Bash can interrupt and Defend can blunt.
|
||||
abilities: {
|
||||
telegraph: { roundInterval: 3, damageMultiplier: 1.6 },
|
||||
},
|
||||
artworkPath: '/images/monsters/road-bandit.png',
|
||||
iconPath: '/images/combat/icons/road-bandit-128.png',
|
||||
lootTableId: ROAD_BANDIT_LOOT_TABLE_ID,
|
||||
@@ -188,16 +241,22 @@ export async function seedVisibleVerticalSlice(
|
||||
id: CHARRED_LOOTER_MONSTER_ID,
|
||||
key: 'charred-looter',
|
||||
name: 'Charred Raider',
|
||||
monsterCategory: MonsterCategory.HUMANOID,
|
||||
level: 2,
|
||||
maxHp: 85,
|
||||
attack: 11,
|
||||
armor: 6,
|
||||
silverMin: 0,
|
||||
silverMax: 0,
|
||||
flavorText:
|
||||
'Burned armor, a burned face, and no interest at all in your reasons.',
|
||||
// The rare encounter: no new subsystem, just the known telegraph on a
|
||||
// tighter cadence on top of higher stats. It is winding up almost
|
||||
// constantly, so the player has to interrupt or brace.
|
||||
abilities: {
|
||||
telegraph: { roundInterval: 2, damageMultiplier: 1.6 },
|
||||
},
|
||||
artworkPath: '/images/monsters/charred-looter.png',
|
||||
iconPath: '/images/combat/icons/charred-looter-128.png',
|
||||
// Shares the raider table: same gear, taken from the same caravans.
|
||||
lootTableId: ROAD_BANDIT_LOOT_TABLE_ID,
|
||||
lootTableId: CHARRED_LOOTER_LOOT_TABLE_ID,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -217,22 +276,30 @@ export async function seedVisibleVerticalSlice(
|
||||
monsterIds.set(key, existingMonster?.id ?? id);
|
||||
}
|
||||
|
||||
// Weights read as "how often you meet this on the road". They also drive the
|
||||
// location's danger rating, which is computed from the weighted average of
|
||||
// the pool rather than from its single worst entry.
|
||||
const encounterWeights: Readonly<Record<string, number>> = {
|
||||
'ash-rat': 40,
|
||||
'wild-road-dog': 30,
|
||||
'road-bandit': 20,
|
||||
'charred-looter': 10,
|
||||
};
|
||||
// Weights read as "how often you meet this on the road" (spec §3). They also
|
||||
// drive the location's danger rating, which is computed from the weighted
|
||||
// average of the pool rather than from its single worst entry.
|
||||
//
|
||||
// The Charred Raider is deliberately the odd one out at weight 2: it is a
|
||||
// rare find, and `EncounterType.RARE` is what the hunt card reads to mark it
|
||||
// as such.
|
||||
const encounterPool: ReadonlyArray<{
|
||||
key: string;
|
||||
weight: number;
|
||||
encounterType: EncounterType;
|
||||
}> = [
|
||||
{ key: 'ash-rat', weight: 50, encounterType: EncounterType.NORMAL },
|
||||
{ key: 'wild-road-dog', weight: 30, encounterType: EncounterType.NORMAL },
|
||||
{ key: 'road-bandit', weight: 18, encounterType: EncounterType.NORMAL },
|
||||
{ key: 'charred-looter', weight: 2, encounterType: EncounterType.RARE },
|
||||
];
|
||||
|
||||
await locationMonsterRepository.upsert(
|
||||
Object.entries(encounterWeights).map(([key, weight]) => ({
|
||||
encounterPool.map(({ key, weight, encounterType }) => ({
|
||||
locationId: burnedRoadId,
|
||||
monsterId: monsterIds.get(key) as string,
|
||||
weight,
|
||||
encounterType: EncounterType.NORMAL,
|
||||
encounterType,
|
||||
enabled: true,
|
||||
})),
|
||||
['locationId', 'monsterId'],
|
||||
@@ -294,4 +361,62 @@ export async function seedVisibleVerticalSlice(
|
||||
characterItemId: startingSwordItemId,
|
||||
});
|
||||
}
|
||||
|
||||
// ASSUMPTION (Playable Slice 0.7.5 §7 leaves acquisition to the merchant
|
||||
// and quest slices, so nothing in 0.7.5 hands out a bag).
|
||||
//
|
||||
// The demo character starts with both starter bags anyway. Without them the
|
||||
// bagless default of 1 applies, and since the merchant that empties a bag
|
||||
// only arrives in 0.8, the second kill of a hunt would leave its trade good
|
||||
// behind forever -- the hunting loop would be unplayable between these two
|
||||
// slices. Capacity 5 lets the fill-up actually be experienced.
|
||||
//
|
||||
// Smallest reversible choice: two seed rows, no acquisition mechanism.
|
||||
// Delete them once the merchant sells bags.
|
||||
const characterLootBagRepository = dataSource.getRepository(CharacterLootBag);
|
||||
for (const lootBagDefinitionId of [
|
||||
BASIC_HIDE_BAG_ID,
|
||||
BASIC_TROPHY_POUCH_ID,
|
||||
]) {
|
||||
const existingBag = await characterLootBagRepository.findOneBy({
|
||||
characterId: DEMO_CHARACTER_ID,
|
||||
lootBagDefinitionId,
|
||||
});
|
||||
if (!existingBag) {
|
||||
await characterLootBagRepository.insert({
|
||||
characterId: DEMO_CHARACTER_ID,
|
||||
lootBagDefinitionId,
|
||||
active: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// NPC content (NPC Specification V1 §32; Playable Slice 0.8).
|
||||
//
|
||||
// Seeded after the character so a fresh database has somewhere to stand and
|
||||
// someone to trade with in one pass. Every link uses a stable business key
|
||||
// rather than a generated id (spec §4), and every write is an upsert, so
|
||||
// re-running the seed re-tunes prices without duplicating Borin.
|
||||
for (const npc of NPC_DEFINITIONS) {
|
||||
const locationId = locationIds.get(npc.locationKey);
|
||||
if (!locationId) {
|
||||
throw new Error(
|
||||
`NPC ${npc.key} references unknown location ${npc.locationKey}.`,
|
||||
);
|
||||
}
|
||||
|
||||
const { locationKey, ...definition } = npc;
|
||||
await npcDefinitionRepository.upsert({ ...definition, locationId }, [
|
||||
'key',
|
||||
]);
|
||||
}
|
||||
|
||||
await dialogueNodeRepository.upsert(DIALOGUE_NODES, ['npcId', 'key']);
|
||||
await npcShopRepository.upsert(NPC_SHOPS, ['key']);
|
||||
await shopOfferRepository.upsert(SHOP_OFFERS, ['shopId', 'itemDefinitionId']);
|
||||
await npcExchangeProfileRepository.upsert(NPC_EXCHANGE_PROFILES, ['key']);
|
||||
await exchangeRuleRepository.upsert(EXCHANGE_RULES, [
|
||||
'profileId',
|
||||
'inputItemId',
|
||||
]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user