feat(seed): open the abandoned watchpost and the route beyond it
This commit is contained in:
@@ -36,12 +36,18 @@ import {
|
||||
} from './item-content';
|
||||
import {
|
||||
ASH_RAT_LOOT_TABLE_ID,
|
||||
BURNED_HOUND_LOOT_TABLE_ID,
|
||||
CHARRED_LOOTER_LOOT_TABLE_ID,
|
||||
ITEM_IDS,
|
||||
RAIDER_CAPTAIN_LOOT_TABLE_ID,
|
||||
RAIDER_SCOUT_LOOT_TABLE_ID,
|
||||
RAIDER_VETERAN_LOOT_TABLE_ID,
|
||||
ROAD_BANDIT_LOOT_TABLE_ID,
|
||||
WILD_ROAD_DOG_LOOT_TABLE_ID,
|
||||
} from './item.constants';
|
||||
import {
|
||||
ABANDONED_WATCHPOST_LOCAL_CONTENT,
|
||||
ASH_PIT_LOCAL_CONTENT,
|
||||
BURNED_ROAD_LOCAL_CONTENT,
|
||||
SOUTH_GATE_LOCAL_CONTENT,
|
||||
} from './local-location.content';
|
||||
@@ -62,9 +68,15 @@ import {
|
||||
SHOP_OFFERS,
|
||||
} from './npc-content';
|
||||
import {
|
||||
ABANDONED_WATCHPOST_ID,
|
||||
ASH_PIT_ID,
|
||||
ASH_RAT_MONSTER_ID,
|
||||
BURNED_HOUND_MONSTER_ID,
|
||||
BURNED_ROAD_ID,
|
||||
CHARRED_LOOTER_MONSTER_ID,
|
||||
RAIDER_CAPTAIN_MONSTER_ID,
|
||||
RAIDER_SCOUT_MONSTER_ID,
|
||||
RAIDER_VETERAN_MONSTER_ID,
|
||||
ROAD_BANDIT_MONSTER_ID,
|
||||
SOUTH_GATE_ID,
|
||||
WILD_ROAD_DOG_MONSTER_ID,
|
||||
@@ -131,6 +143,36 @@ export async function seedVisibleVerticalSlice(
|
||||
artworkPath: '/images/backgrounds/Aschestrasse.png',
|
||||
...BURNED_ROAD_LOCAL_CONTENT,
|
||||
},
|
||||
{
|
||||
id: ABANDONED_WATCHPOST_ID,
|
||||
key: 'abandoned-watchpost',
|
||||
name: 'Abandoned Watchpost',
|
||||
description:
|
||||
'A border tower the Watch gave up on. Whoever holds it now is watching the road, not guarding it.',
|
||||
regionKey: 'ashen-fields',
|
||||
minRecommendedLevel: 2,
|
||||
maxRecommendedLevel: 3,
|
||||
dangerLevel: 2,
|
||||
isSafe: false,
|
||||
huntingEnabled: true,
|
||||
artworkPath: '/images/backgrounds/Wachturm.png',
|
||||
...ABANDONED_WATCHPOST_LOCAL_CONTENT,
|
||||
},
|
||||
{
|
||||
id: ASH_PIT_ID,
|
||||
key: 'ash-pit',
|
||||
name: 'Ash Pit',
|
||||
description:
|
||||
'The old ash excavation east of the watchpost, cut in terraces and still being worked by someone.',
|
||||
regionKey: 'ashen-fields',
|
||||
minRecommendedLevel: 3,
|
||||
maxRecommendedLevel: 4,
|
||||
dangerLevel: 3,
|
||||
isSafe: false,
|
||||
huntingEnabled: false,
|
||||
artworkPath: '/images/backgrounds/Aschengrube.png',
|
||||
...ASH_PIT_LOCAL_CONTENT,
|
||||
},
|
||||
];
|
||||
|
||||
const locationIds = new Map<string, string>();
|
||||
@@ -149,6 +191,9 @@ export async function seedVisibleVerticalSlice(
|
||||
|
||||
const southGateId = locationIds.get('south-gate') ?? SOUTH_GATE_ID;
|
||||
const burnedRoadId = locationIds.get('burned-road') ?? BURNED_ROAD_ID;
|
||||
const watchpostId =
|
||||
locationIds.get('abandoned-watchpost') ?? ABANDONED_WATCHPOST_ID;
|
||||
const ashPitId = locationIds.get('ash-pit') ?? ASH_PIT_ID;
|
||||
|
||||
await connectionRepository.upsert(
|
||||
[
|
||||
@@ -158,6 +203,7 @@ export async function seedVisibleVerticalSlice(
|
||||
travelDurationSeconds: 10,
|
||||
ambushChance: '0.0500',
|
||||
enabled: true,
|
||||
requiresDiscovery: false,
|
||||
},
|
||||
{
|
||||
fromLocationId: burnedRoadId,
|
||||
@@ -165,6 +211,42 @@ export async function seedVisibleVerticalSlice(
|
||||
travelDurationSeconds: 10,
|
||||
ambushChance: '0.0500',
|
||||
enabled: true,
|
||||
requiresDiscovery: false,
|
||||
},
|
||||
{
|
||||
fromLocationId: burnedRoadId,
|
||||
toLocationId: watchpostId,
|
||||
travelDurationSeconds: 15,
|
||||
ambushChance: '0.1000',
|
||||
enabled: true,
|
||||
requiresDiscovery: false,
|
||||
},
|
||||
{
|
||||
fromLocationId: watchpostId,
|
||||
toLocationId: burnedRoadId,
|
||||
travelDurationSeconds: 15,
|
||||
ambushChance: '0.1000',
|
||||
enabled: true,
|
||||
requiresDiscovery: false,
|
||||
},
|
||||
// The one gated route in the game (slice §9). Discovery, not a level,
|
||||
// is what opens it -- and the way back is never gated, so a character
|
||||
// who walked in can always walk out.
|
||||
{
|
||||
fromLocationId: watchpostId,
|
||||
toLocationId: ashPitId,
|
||||
travelDurationSeconds: 20,
|
||||
ambushChance: '0.1500',
|
||||
enabled: true,
|
||||
requiresDiscovery: true,
|
||||
},
|
||||
{
|
||||
fromLocationId: ashPitId,
|
||||
toLocationId: watchpostId,
|
||||
travelDurationSeconds: 20,
|
||||
ambushChance: '0.1500',
|
||||
enabled: true,
|
||||
requiresDiscovery: false,
|
||||
},
|
||||
],
|
||||
['fromLocationId', 'toLocationId'],
|
||||
@@ -265,6 +347,89 @@ export async function seedVisibleVerticalSlice(
|
||||
iconPath: '/images/combat/icons/charred-looter-128.png',
|
||||
lootTableId: CHARRED_LOOTER_LOOT_TABLE_ID,
|
||||
},
|
||||
{
|
||||
id: RAIDER_SCOUT_MONSTER_ID,
|
||||
key: 'raider-scout',
|
||||
name: 'Raider Scout',
|
||||
monsterCategory: MonsterCategory.HUMANOID,
|
||||
level: 2,
|
||||
maxHp: 70,
|
||||
attack: 10,
|
||||
armor: 3,
|
||||
flavorText:
|
||||
'Light on their feet and already backing away from the fight they started.',
|
||||
// The farming target: no mechanic at all, so the pool has somewhere
|
||||
// for a player to breathe between the harder fights (slice §5).
|
||||
abilities: {},
|
||||
artworkPath: '/images/monsters/raider-scout.png',
|
||||
iconPath: '/images/combat/icons/raider-scout-128.png',
|
||||
lootTableId: RAIDER_SCOUT_LOOT_TABLE_ID,
|
||||
},
|
||||
{
|
||||
id: BURNED_HOUND_MONSTER_ID,
|
||||
key: 'burned-hound',
|
||||
name: 'Burned Hound',
|
||||
monsterCategory: MonsterCategory.BEAST,
|
||||
level: 3,
|
||||
maxHp: 80,
|
||||
attack: 12,
|
||||
armor: 2,
|
||||
flavorText:
|
||||
'The fire took its coat and left the cracks glowing underneath.',
|
||||
// Bleeding on a tighter cadence than the road hound, plus a rage that
|
||||
// punishes a player who lets the fight run long (slice §5).
|
||||
abilities: {
|
||||
bleed: { roundInterval: 2, damagePerRound: 6, durationRounds: 2 },
|
||||
enrage: { hpThresholdPercent: 35, damageMultiplier: 1.4 },
|
||||
},
|
||||
artworkPath: '/images/monsters/burned-hound.png',
|
||||
iconPath: '/images/combat/icons/burned-hound-128.png',
|
||||
lootTableId: BURNED_HOUND_LOOT_TABLE_ID,
|
||||
},
|
||||
{
|
||||
id: RAIDER_VETERAN_MONSTER_ID,
|
||||
key: 'raider-veteran',
|
||||
name: 'Raider Veteran',
|
||||
monsterCategory: MonsterCategory.HUMANOID,
|
||||
level: 3,
|
||||
maxHp: 120,
|
||||
attack: 14,
|
||||
armor: 10,
|
||||
flavorText:
|
||||
'Plated, patient, and entirely willing to wait behind their guard.',
|
||||
// The fight this location is built around: the telegraph the player
|
||||
// already knows, plus a guard that answers to the same Shield Bash.
|
||||
// Intervals 3 and 4 so the two only collide every twelfth round.
|
||||
abilities: {
|
||||
telegraph: { roundInterval: 3, damageMultiplier: 1.6 },
|
||||
guard: { roundInterval: 4, armorBonus: 10, durationRounds: 2 },
|
||||
},
|
||||
artworkPath: '/images/monsters/raider-veteran.png',
|
||||
iconPath: '/images/combat/icons/raider-veteran-128.png',
|
||||
lootTableId: RAIDER_VETERAN_LOOT_TABLE_ID,
|
||||
},
|
||||
{
|
||||
id: RAIDER_CAPTAIN_MONSTER_ID,
|
||||
key: 'raider-captain',
|
||||
name: 'Raider Captain',
|
||||
monsterCategory: MonsterCategory.HUMANOID,
|
||||
level: 4,
|
||||
maxHp: 160,
|
||||
attack: 17,
|
||||
armor: 12,
|
||||
flavorText:
|
||||
'Whoever gave the order to watch this road is standing in front of you.',
|
||||
// The elite (slice §5): the veteran's two mechanics on tighter
|
||||
// cadences and better stats, not a third subsystem. Deliberately not
|
||||
// the Captain of the Ashen Band -- that boss belongs to Slice 0.11.
|
||||
abilities: {
|
||||
telegraph: { roundInterval: 2, damageMultiplier: 1.7 },
|
||||
guard: { roundInterval: 3, armorBonus: 12, durationRounds: 2 },
|
||||
},
|
||||
artworkPath: '/images/monsters/raider-captain.png',
|
||||
iconPath: '/images/combat/icons/raider-captain-128.png',
|
||||
lootTableId: RAIDER_CAPTAIN_LOOT_TABLE_ID,
|
||||
},
|
||||
];
|
||||
|
||||
const monsterIds = new Map<string, string>();
|
||||
@@ -312,6 +477,32 @@ export async function seedVisibleVerticalSlice(
|
||||
['locationId', 'monsterId'],
|
||||
);
|
||||
|
||||
// Watchpost roster (slice §4). The Road Bandit is reused deliberately: it
|
||||
// bridges the two locations and keeps the Raider Insignia economy
|
||||
// connected. Its definition and loot table are untouched.
|
||||
const watchpostPool: ReadonlyArray<{
|
||||
key: string;
|
||||
weight: number;
|
||||
encounterType: EncounterType;
|
||||
}> = [
|
||||
{ key: 'raider-scout', weight: 35, encounterType: EncounterType.NORMAL },
|
||||
{ key: 'burned-hound', weight: 28, encounterType: EncounterType.NORMAL },
|
||||
{ key: 'road-bandit', weight: 20, encounterType: EncounterType.NORMAL },
|
||||
{ key: 'raider-veteran', weight: 14, encounterType: EncounterType.NORMAL },
|
||||
{ key: 'raider-captain', weight: 3, encounterType: EncounterType.RARE },
|
||||
];
|
||||
|
||||
await locationMonsterRepository.upsert(
|
||||
watchpostPool.map(({ key, weight, encounterType }) => ({
|
||||
locationId: watchpostId,
|
||||
monsterId: monsterIds.get(key) as string,
|
||||
weight,
|
||||
encounterType,
|
||||
enabled: true,
|
||||
})),
|
||||
['locationId', 'monsterId'],
|
||||
);
|
||||
|
||||
const existing = await characterRepository.findOneBy({
|
||||
id: DEMO_CHARACTER_ID,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user