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
1 change: 1 addition & 0 deletions src/event/EventKeyEnum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export enum EventKey {
// Local events
SELECTION_CHANGED = 'selection-changed',
DESELECT_ALL = 'deselect-all',
TASK_WITHOUT_DESELECTING = 'task-without-deselecting',
BUILDINGS_CHANGED = 'buildings-changed',
RAIDER_AMOUNT_CHANGED = 'raider-amount-changed',
RAIDER_TRAINING_COMPLETE = 'raider-training-complete',
Expand Down
3 changes: 2 additions & 1 deletion src/event/EventTypeMap.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AdvanceAfterRewardsEvent, BuildingsChangedEvent, DeselectAll, FollowerSetCanvasEvent, FollowerSetLookAtEvent, GuiBackButtonClicked, GuiBuildButtonClicked, GuiButtonBlinkEvent, GuiGetToolButtonClicked, GuiTrainRaiderButtonClicked, InitRadarMap, RaidersAmountChangedEvent, RaiderTrainingCompleteEvent, SelectionChanged, SelectionFrameChangeEvent, SetSpaceToContinueEvent, ShowGameResultEvent, ShowMissionAdvisorEvent, ShowMissionBriefingEvent, ShowOptionsEvent, UpdateRadarCamera, UpdateRadarEntityEvent, UpdateRadarSurface, UpdateRadarTerrain, VehicleUpgradeCompleteEvent } from './LocalEvents'
import { AdvanceAfterRewardsEvent, BuildingsChangedEvent, DeselectAll, TaskWithoutDeselecting, FollowerSetCanvasEvent, FollowerSetLookAtEvent, GuiBackButtonClicked, GuiBuildButtonClicked, GuiButtonBlinkEvent, GuiGetToolButtonClicked, GuiTrainRaiderButtonClicked, InitRadarMap, RaidersAmountChangedEvent, RaiderTrainingCompleteEvent, SelectionChanged, SelectionFrameChangeEvent, SetSpaceToContinueEvent, ShowGameResultEvent, ShowMissionAdvisorEvent, ShowMissionBriefingEvent, ShowOptionsEvent, UpdateRadarCamera, UpdateRadarEntityEvent, UpdateRadarSurface, UpdateRadarTerrain, VehicleUpgradeCompleteEvent } from './LocalEvents'
import { AirLevelChanged, CavernDiscovered, DynamiteExplosionEvent, GameResultEvent, JobCreateEvent, LevelSelectedEvent, MaterialAmountChanged, MonsterEmergeEvent, MonsterLaserHitEvent, NerpMessageEvent, NerpSuppressArrowEvent, OreFoundEvent, RequestedRaidersChanged, RequestedVehiclesChanged, RestartGameEvent, ShootLaserEvent, ToggleAlarmEvent, UpdatePriorities, UsedCrystalsChanged, WorldLocationEvent } from './WorldEvents'
import { CameraControl, CancelBuilding, CancelSurfaceJobs, ChangeBuildingPowerState, ChangeCameraEvent, ChangePreferences, ChangeTooltip, CreateClearRubbleJob, CreateDrillJob, CreateDynamiteJob, CreatePowerPath, CreateReinforceJob, DropBirdScarer, HideTooltip, MakeRubble, PickTool, PlaceFence, PlaySoundEvent, RaiderBeamUp, RaiderDrop, RaiderEat, RaiderUpgrade, RepairBuilding, RepairLava, SelectBuildMode, TrainRaider, UpgradeBuilding, UpgradeVehicle, VehicleBeamUp, VehicleCallMan, VehicleDriverGetOut, VehicleLoad, VehicleUnload } from './GuiCommand'

Expand All @@ -14,6 +14,7 @@ export class BaseEvent {
export interface DefaultEventMap {
'selection-changed': SelectionChanged
'deselect-all': DeselectAll
'task-without-deselecting': TaskWithoutDeselecting
'buildings-changed': BuildingsChangedEvent
'raider-amount-changed': RaidersAmountChangedEvent
'raider-training-complete': RaiderTrainingCompleteEvent
Expand Down
6 changes: 6 additions & 0 deletions src/event/LocalEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,12 @@ export class DeselectAll extends BaseEvent {
}
}

export class TaskWithoutDeselecting extends BaseEvent {
constructor() {
super(EventKey.TASK_WITHOUT_DESELECTING)
}
}

export class BuildingsChangedEvent extends BaseEvent {
readonly placedVisibleBuildings: Set<GameEntity> = new Set()
readonly poweredBuildings: Set<GameEntity> = new Set()
Expand Down
4 changes: 4 additions & 0 deletions src/game/EntityManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ export class EntityManager {
this.selection.deselectAll()
EventBroker.publish(new SelectionChanged(this))
})
EventBroker.subscribe(EventKey.TASK_WITHOUT_DESELECTING, () => {
this.selection.assignAllToTaskWithoutDeselect()
EventBroker.publish(new SelectionChanged(this))
})
EventBroker.subscribe(EventKey.MATERIAL_AMOUNT_CHANGED, () => {
this.buildings.forEach((b) => b.updateEnergyState())
})
Expand Down
223 changes: 223 additions & 0 deletions src/game/LevelGen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import { LevelConfData } from './LevelLoader'
import { EntityType } from './model/EntityType'
import { NerpScript } from '../nerp/NerpScript'
import { LevelObjectiveTextEntry } from '../resource/fileparser/ObjectiveTextParser'
import { GameConfig } from '../cfg/GameConfig'
import { LevelRewardConfig, ObjectiveImageCfg } from '../cfg/LevelsCfg'
import { ObjectListEntryCfg } from '../cfg/ObjectListEntryCfg'
import { DEV_MODE } from '../params'
import { SeededRandomGenerator } from '../core/SeededRandomGenerator'

function generateCave(prng, width, height, crystalsMin, crystalsMax, oresMax) {
if (width < 15 || height < 15) {
throw new Error('Width and height must be at least 15.')
}

const wallDensity = 0.60
const iterations = 3

// Initialize the predugMap with walls (0)
let predugMap = Array.from({length: height}, () => Array(width).fill(0))

// Create cavern randomly based on wall density
const wallMargin = 6
for (let y = wallMargin; y < height - wallMargin; y++) {
for (let x = wallMargin; x < width - wallMargin; x++) {
predugMap[y][x] = prng.random() < wallDensity ? 0 : 2 // 0 = wall, 2 = ground
}
}

// Smooth the cave structure
for (let i = 0; i < iterations; i++) {
predugMap = smoothCave(predugMap)
}

// Pick random start position
const grounds = []
const walls = []
const startMargin = 7
for (let y = startMargin; y < height - startMargin; y++) {
for (let x = startMargin; x < width - startMargin; x++) {
if (predugMap[y][x] === 2) {
grounds.push({x, y})
} else {
walls.push({x, y})
}
}
}
let startPosition = prng.sample(grounds)

// Perform BFS to mark connected hidden grounds (2s) as exposed (1s)
floodFill(predugMap, startPosition.x, startPosition.y)

// Initialize terrainMap to track durability of walls
let terrainMap = Array.from({length: height}, () => Array(width).fill(1))

// Randomly assign durability to walls
for (let y = wallMargin; y < height - wallMargin; y++) {
for (let x = wallMargin; x < width - wallMargin; x++) {
if (predugMap[y][x] === 0) {
let rand = prng.random()
if (rand < 0.15) terrainMap[y][x] = 2
else if (rand < 0.50) terrainMap[y][x] = 3
else if (rand < 0.75) terrainMap[y][x] = 4
}
}
}

// Initialize cryOreMap and fill with resources
let cryOreMap = Array.from({length: height}, () => Array(width).fill(0))

if (crystalsMax < crystalsMin) throw new Error(`Invalid number of crystals given; ${crystalsMax} < ${crystalsMin}`)
const numCrystals = crystalsMin + prng.randInt(crystalsMax - crystalsMin)
for (let c = 0; c < numCrystals; c++) {
const targetWall = prng.sample(walls)
if (!targetWall) throw new Error('No wall to place crystal')
const prev = cryOreMap[targetWall.y][targetWall.x]
cryOreMap[targetWall.y][targetWall.x] = prev > 0 ? prev + 2 : 1
}

function findEmptyWall(walls) {
for (let t = 0; t < 20; t++) {
const result = prng.sample(walls)
if (cryOreMap[result.y][result.x] === 0) return result
}
throw new Error('Could not find empty wall to place ores')
}

if (oresMax < 0) throw new Error('Invalid number of ores given')
for (let c = 0; c < oresMax; c++) {
const targetWall = findEmptyWall(walls)
cryOreMap[targetWall.y][targetWall.x] += 2
}

// Add challenges to the map
// FIXME Add emerge map
// FIXME Add erosion map and lava streams
// FIXME Add slugs
// FIXME Add bats and sleeping rockies

return {
terrainMap,
startPosition,
predugMap,
cryOreMap,
mapWidth: width,
mapHeight: height
}
}

function smoothCave(map) {
const newMap = map.map(arr => arr.slice())
for (let y = 1; y < map.length - 1; y++) {
for (let x = 1; x < map[0].length - 1; x++) {
let wallCount = 0
// Count walls around the current cell
for (let ny = -1; ny <= 1; ny++) {
for (let nx = -1; nx <= 1; nx++) {
if (map[y + ny][x + nx] === 0) wallCount++
}
}
if (wallCount > 4) {
newMap[y][x] = 0 // Set to wall
} else {
newMap[y][x] = 2 // Keep as ground
}
}
}
return newMap
}

function floodFill(map, x, y) {
// Use a queue for BFS
const queue = [[x, y]]
while (queue.length > 0) {
const [cx, cy] = queue.shift()
if (map[cy][cx] === 2) {
map[cy][cx] = 1; // Mark as traversable
// Check 4 possible directions
[[1, 0], [-1, 0], [0, 1], [0, -1]].forEach(([dx, dy]) => {
const nx = cx + dx, ny = cy + dy
if (map[ny] && map[ny][nx] === 2) {
queue.push([nx, ny])
}
})
}
}
}

export class LevelGen {
static fromSeed(seed: string): LevelConfData {
console.log(`Generating level from seed "${seed}"`)
const prng = new SeededRandomGenerator().setSeed(seed)

const objectiveImageCfg = new ObjectiveImageCfg()
objectiveImageCfg.filename = 'Interface/BriefingPanel/BriefingPanel.bmp'
objectiveImageCfg.x = 76
objectiveImageCfg.y = 100
const cave = generateCave(prng, 50, 50, 15, 50, 100)

// Display the cave structure
// console.log('Terrain Map:')
// console.log(cave.terrainMap.map(row => row.join(' ')).join('\n'))
// console.log('Start Position:', cave.startPosition)
// console.log('Predug Map:')
// console.log(cave.predugMap.map(row => row.join(' ')).join('\n'))
// console.log('Cry Ore Map:')
// console.log(cave.cryOreMap.map(row => row.join(' ')).join('\n'))

const objectList = new Map<string, ObjectListEntryCfg>()
objectList.set('Object1', {
type: 'TVCamera',
xPos: cave.startPosition.x, // TODO Move camera back to actual start
yPos: cave.startPosition.y,
// xPos: Math.round(cave.mapWidth / 2),
// yPos: Math.round(cave.mapHeight / 2),
heading: 0,
})
objectList.set('Object2', {
type: 'Toolstation',
xPos: cave.startPosition.x + 0.5,
yPos: cave.startPosition.y + 0.5,
heading: 90 * prng.randInt(3),
})
return {
blockPointersMap: undefined,
cryOreMap: cave.cryOreMap,
disableEndTeleport: DEV_MODE,
disableStartTeleport: true,
emergeCreature: EntityType.ROCK_MONSTER,
emergeMap: undefined,
emergeTimeOutMs: 0,
erodeErodeTimeMs: 0,
erodeLockTimeMs: 0,
erodeMap: undefined,
erodeTriggerTimeMs: 0,
fallinMap: undefined,
fallinMultiplier: 0,
fogColor: [110, 110, 155].map((c) => Math.round(c / 255)) as [number, number, number],
fullName: `Level from seed ${seed}`,
generateSpiders: false,
levelName: 'levelseed',
mapHeight: cave.mapHeight,
mapWidth: cave.mapWidth,
nerpMessages: [],
nerpScript: new NerpScript(),
noMultiSelect: false,
objectList: objectList,
objectiveImage: objectiveImageCfg,
objectiveTextCfg: new LevelObjectiveTextEntry(),
oxygenRate: 0,
pathMap: undefined,
predugMap: cave.predugMap,
priorities: [],
reward: new LevelRewardConfig(),
rockFallStyle: GameConfig.instance.rockFallStyles['rock'],
roofTexture: 'World/WorldTextures/rockroof.bmp',
surfaceMap: undefined,
terrainMap: cave.terrainMap,
textureBasename: 'World/WorldTextures/RockSplit/Rock',
video: ''
}
}
}
14 changes: 14 additions & 0 deletions src/game/model/GameSelection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,20 @@ export class GameSelection {
this.primarySelect = undefined
}

assignAllToTaskWithoutDeselect() {
this.raiders.forEach((r) => r.taskingWhileSelected = true)
this.vehicles.forEach((v) => v.taskingWhileSelected = true)

this.building?.deselect()
this.building = undefined
this.surface?.deselect()
this.surface = undefined
this.doubleSelect = undefined
this.fence?.worldMgr.ecs.getComponents(this.fence.entity).get(SelectionFrameComponent)?.deselect()
this.fence = undefined
this.primarySelect = undefined
}

canMove(): boolean {
return this.raiders.length > 0 || this.vehicles.some((v) => !!v.driver)
}
Expand Down
5 changes: 4 additions & 1 deletion src/game/model/raider/Raider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export class Raider implements Updatable, JobFulfiller {
weaponCooldown: number = 0
resting: boolean = false
idleCounter: number = PRNG.animation.randInt(3000)
taskingWhileSelected: boolean = false

constructor(worldMgr: WorldManager) {
this.worldMgr = worldMgr
Expand Down Expand Up @@ -96,7 +97,7 @@ export class Raider implements Updatable, JobFulfiller {
this.sceneEntity.setAnimation(this.vehicle.getDriverActivity())
return
}
if (this.slipped || this.isInBeam() || this.thrown || this.selected || this.resting) return
if (this.slipped || this.isInBeam() || this.thrown || (this.selected && !this.taskingWhileSelected) || this.resting) return
if (GameState.alarmMode && this.hasWeapon() && !this.job?.doOnAlarm) {
this.fight(elapsedMs)
return
Expand Down Expand Up @@ -299,6 +300,7 @@ export class Raider implements Updatable, JobFulfiller {
components.get(SelectionNameComponent)?.setVisible(true)
this.sceneEntity.setAnimation(this.getDefaultAnimationName())
this.workAudioId = SoundManager.stopAudio(this.workAudioId)
this.taskingWhileSelected = false
return true
}

Expand All @@ -308,6 +310,7 @@ export class Raider implements Updatable, JobFulfiller {

deselect() {
const components = this.worldMgr.ecs.getComponents(this.entity)
this.taskingWhileSelected = false
components.get(SelectionFrameComponent)?.deselect()
components.get(SelectionNameComponent)?.setVisible(false)
}
Expand Down
5 changes: 4 additions & 1 deletion src/game/model/vehicle/VehicleEntity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export class VehicleEntity implements Updatable, JobFulfiller {
upgrading: boolean = false
portering: boolean = false
carriedBy?: GameEntity
taskingWhileSelected: boolean = false

constructor(entityType: EntityType, worldMgr: WorldManager, stats: VehicleEntityStats, aeNames: string[], readonly driverActivityStand: RaiderActivity | AnimEntityActivity.Stand = AnimEntityActivity.Stand, readonly driverActivityRoute: RaiderActivity | AnimEntityActivity.Stand = AnimEntityActivity.Stand) {
this.entityType = entityType
Expand Down Expand Up @@ -100,7 +101,7 @@ export class VehicleEntity implements Updatable, JobFulfiller {
}

update(elapsedMs: number) {
if (!this.job || this.selected || this.isInBeam()) return
if (!this.job || (this.selected && ! this.taskingWhileSelected) || this.isInBeam()) return
if (this.job.jobState !== JobState.INCOMPLETE) {
this.stopJob()
return
Expand Down Expand Up @@ -281,11 +282,13 @@ export class VehicleEntity implements Updatable, JobFulfiller {
primary ? selectionFrameComponent?.select() : selectionFrameComponent?.selectSecondary()
this.sceneEntity.setAnimation(AnimEntityActivity.Stand)
this.workAudioId = SoundManager.stopAudio(this.workAudioId)
this.taskingWhileSelected = false
return true
}

deselect() {
this.worldMgr.ecs.getComponents(this.entity).get(SelectionFrameComponent)?.deselect()
this.taskingWhileSelected = false
}

isSelectable(): boolean {
Expand Down
10 changes: 8 additions & 2 deletions src/screen/MainMenuScreen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ import { ScaledLayer } from './layer/ScaledLayer'
import { RockWipeLayer } from '../menu/RockWipeLayer'
import { GameConfig } from '../cfg/GameConfig'
import { EventBroker } from '../event/EventBroker'
import { LevelLoader } from '../game/LevelLoader'
import { LevelConfData, LevelLoader } from '../game/LevelLoader'
import { SoundManager } from '../audio/SoundManager'
import { PRNG } from '../game/factory/PRNG'
import { LevelGen } from '../game/LevelGen'

export class MainMenuScreen {
readonly menuLayers: ScaledLayer[] = []
Expand Down Expand Up @@ -131,7 +132,12 @@ export class MainMenuScreen {
if (!levelName) return
this.sfxAmbientLoop?.stop()
this.sfxAmbientLoop = undefined
const levelConf = LevelLoader.fromName(levelName) // Get config first in case of error
let levelConf: LevelConfData
if (levelName === 'levelseed') {
levelConf = LevelGen.fromSeed('random') // FIXME Replace hardcoded seed
} else {
levelConf = LevelLoader.fromName(levelName) // Get config first in case of error
}
this.menuLayers.forEach((m) => m.hide())
this.rockWipeLayer.hide()
if (SaveGameManager.preferences.playVideos) await this.screenMaster.videoLayer.playVideo(levelConf.video)
Expand Down
Loading