From 20e4facdc3a5775832f28e692b4be94f312fa713 Mon Sep 17 00:00:00 2001 From: bananu7 Date: Tue, 25 Jun 2024 22:39:10 +0200 Subject: [PATCH 1/4] Only check harvesting target resource when needed --- packages/server/src/game/command.ts | 29 +++++++++++++++++--------- packages/server/src/game/components.ts | 6 +++++- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/packages/server/src/game/command.ts b/packages/server/src/game/command.ts index ebd0c38..af8928d 100644 --- a/packages/server/src/game/command.ts +++ b/packages/server/src/game/command.ts @@ -9,7 +9,7 @@ import { import { isBuildPlacementOk } from '../shared.js' -import { getHpComponent, getMoveComponent, getAttackerComponent, getHarvesterComponent, getProducerComponent, getBuilderComponent, getVisionComponent, getBuildingComponent } from './components.js' +import { getHpComponent, getMoveComponent, getAttackerComponent, getHarvesterComponent, getProducerComponent, getBuilderComponent, getVisionComponent, getBuildingComponent, getResourceComponent } from './components.js' import * as V from '../vector.js' import { moveTowardsUnit, moveToPointOrCancelCommand, moveTowardsUnitById, moveTowardsMapPosition } from './unit/movement.js' @@ -113,15 +113,23 @@ export const harvestCommand = (ctx: CommandContext, cmd: CommandHarvest) => { throw new ComponentMissingError("Harvester"); } - const target = g.units.find(u => u.id === cmd.target); - if (!target) { - // TODO find other nearby resource - clearCurrentCommand(unit); - return; - } - + // If the unit doesn't have resources, we go into "obtain resources" + // branch, otherwise it's dropoff. We only check the target if we need to. if (!hc.resourcesCarried) { - // TODO - should resources use perimeter? + const target = g.units.find(u => u.id === cmd.target); + if (!target) { + // TODO find other nearby resource + clearCurrentCommand(unit); + return; + } + + const resource = getResourceComponent(target); + if (!resource) { + // TODO find other nearby resource + clearCurrentCommand(unit); + return; + } + switch(moveTowardsUnit(unit, target, HARVESTING_DISTANCE, ctx.gm, dt)) { case 'Unreachable': clearCurrentCommand(unit); @@ -129,6 +137,8 @@ export const harvestCommand = (ctx: CommandContext, cmd: CommandHarvest) => { case 'ReachedTarget': if (hc.harvestingProgress >= hc.harvestingTime) { hc.resourcesCarried = HARVESTING_RESOURCE_COUNT; + resource.value -= HARVESTING_RESOURCE_COUNT; + // TODO - reset harvesting at any other action // maybe i could use some "exit state function"? hc.harvestingProgress = 0; @@ -139,7 +149,6 @@ export const harvestCommand = (ctx: CommandContext, cmd: CommandHarvest) => { break; } } else { - // TODO include building&unit size in this distance const DROPOFF_DISTANCE = 1; // TODO cache the dropoff base // TODO - resource dropoff component diff --git a/packages/server/src/game/components.ts b/packages/server/src/game/components.ts index d78c44f..361386d 100644 --- a/packages/server/src/game/components.ts +++ b/packages/server/src/game/components.ts @@ -1,6 +1,6 @@ import { Unit, - Hp, Mover, Attacker, Harvester, ProductionFacility, Builder, Vision, Building, Component + Hp, Mover, Attacker, Harvester, ProductionFacility, Builder, Vision, Building, Resource, Component } from '../types' export const getHpComponent = (unit: Unit) => { @@ -34,3 +34,7 @@ export const getVisionComponent = (unit: Unit) => { export const getBuildingComponent = (unit: Unit) => { return unit.components.find(c => c.type === 'Building') as Building; }; + +export const getResourceComponent = (unit: Unit) => { + return unit.components.find(c => c.type === 'Resource') as Resource; +} From be87ce17594d27dccb62a43dc914b92e10ad6790 Mon Sep 17 00:00:00 2001 From: bananu7 Date: Tue, 25 Jun 2024 23:04:33 +0200 Subject: [PATCH 2/4] Refactored the entire harvesting logic to make it cleaner and more resilient --- packages/server/src/game/command.ts | 42 ++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/packages/server/src/game/command.ts b/packages/server/src/game/command.ts index af8928d..1d615cb 100644 --- a/packages/server/src/game/command.ts +++ b/packages/server/src/game/command.ts @@ -17,7 +17,7 @@ import { clearCurrentCommand, stopMoving, becomeIdleAtCurrentPosition } from './ import { detectNearbyEnemy, findClosestUnitBy, cancelProduction, aggro } from './unit/unit.js' import { HARVESTING_DISTANCE, HARVESTING_RESOURCE_COUNT, MAX_PLAYER_UNITS, UNIT_FOLLOW_DISTANCE } from './constants.js' import { createUnit, UnitData, getUnitDataByName } from './units.js' -import { findClosestEmptyTile } from './util.js' +import { findClosestEmptyTile, unitInteractionDistance } from './util.js' import { findPositionForProducedUnit } from './produce.js' import { buildPresenceAndBuildingMaps } from './presence.js' @@ -130,11 +130,32 @@ export const harvestCommand = (ctx: CommandContext, cmd: CommandHarvest) => { return; } - switch(moveTowardsUnit(unit, target, HARVESTING_DISTANCE, ctx.gm, dt)) { - case 'Unreachable': - clearCurrentCommand(unit); - break; - case 'ReachedTarget': + + if (unitInteractionDistance(unit, target) > HARVESTING_DISTANCE) { + // in case of displacement mid-harvesting, reset and retry + if (unit.state.action === 'Harvesting') { + hc.harvestingProgress = 0; + unit.state.action = 'Idle'; + return; + } else { + switch(moveTowardsUnit(unit, target, HARVESTING_DISTANCE, ctx.gm, dt)) { + case 'Unreachable': + clearCurrentCommand(unit); + break; + case 'ReachedTarget': + unit.state.action = 'Idle'; + break; + } + } + } else { + if (!resourceAvailable(target)) { + // just wait + unit.state.action = 'Idle'; + } + + unit.state.action = 'Harvesting' + hc.harvestingProgress += dt; + if (hc.harvestingProgress >= hc.harvestingTime) { hc.resourcesCarried = HARVESTING_RESOURCE_COUNT; resource.value -= HARVESTING_RESOURCE_COUNT; @@ -142,11 +163,7 @@ export const harvestCommand = (ctx: CommandContext, cmd: CommandHarvest) => { // TODO - reset harvesting at any other action // maybe i could use some "exit state function"? hc.harvestingProgress = 0; - } else { - unit.state.action = 'Harvesting'; - hc.harvestingProgress += dt; } - break; } } else { const DROPOFF_DISTANCE = 1; @@ -175,6 +192,11 @@ export const harvestCommand = (ctx: CommandContext, cmd: CommandHarvest) => { } } +function resourceAvailable(unit: Unit) { + // TODO implement + return true; +} + export const produceCommand = (ctx: CommandContext, cmd: CommandProduce) => { const unit = ctx.unit; const owner = ctx.owner; From 06df3d752ef3c0747d2cd40e37aaaa4f76cb7253 Mon Sep 17 00:00:00 2001 From: bananu7 Date: Tue, 25 Jun 2024 23:14:49 +0200 Subject: [PATCH 3/4] Check for other harvesters targetting the same resource --- packages/server/src/game/command.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/server/src/game/command.ts b/packages/server/src/game/command.ts index 1d615cb..6730412 100644 --- a/packages/server/src/game/command.ts +++ b/packages/server/src/game/command.ts @@ -125,12 +125,11 @@ export const harvestCommand = (ctx: CommandContext, cmd: CommandHarvest) => { const resource = getResourceComponent(target); if (!resource) { - // TODO find other nearby resource + console.warn("Unit received a harvest command to something that's not a resource."); clearCurrentCommand(unit); return; } - if (unitInteractionDistance(unit, target) > HARVESTING_DISTANCE) { // in case of displacement mid-harvesting, reset and retry if (unit.state.action === 'Harvesting') { @@ -148,7 +147,7 @@ export const harvestCommand = (ctx: CommandContext, cmd: CommandHarvest) => { } } } else { - if (!resourceAvailable(target)) { + if (!resourceAvailable(target.id, ctx)) { // just wait unit.state.action = 'Idle'; } @@ -192,9 +191,14 @@ export const harvestCommand = (ctx: CommandContext, cmd: CommandHarvest) => { } } -function resourceAvailable(unit: Unit) { - // TODO implement - return true; +function resourceAvailable(targetId: UnitId, ctx: CommandContext) { + // Check if any other harvester is competing for the same resource + return ctx.gm.game.units.filter(u => { + u.state.state === "active" && + u.state.action === "Harvesting" && + u.state.current.typ === "Harvest" && + u.state.current.target === targetId + }).length > 0; } export const produceCommand = (ctx: CommandContext, cmd: CommandProduce) => { From 401741e006761cac0dd887dd6fbfeaad8700e752 Mon Sep 17 00:00:00 2001 From: bananu7 Date: Wed, 26 Jun 2024 00:17:06 +0200 Subject: [PATCH 4/4] Prevent more than one harvesting from harvesting the same node --- packages/server/src/game/command.ts | 27 +++++++++++++--------- packages/server/src/game/units.ts | 2 +- packages/server/test/behaviour.test.ts | 32 ++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 12 deletions(-) diff --git a/packages/server/src/game/command.ts b/packages/server/src/game/command.ts index 6730412..194e8a1 100644 --- a/packages/server/src/game/command.ts +++ b/packages/server/src/game/command.ts @@ -147,21 +147,23 @@ export const harvestCommand = (ctx: CommandContext, cmd: CommandHarvest) => { } } } else { - if (!resourceAvailable(target.id, ctx)) { + if (!resourceAvailableFor(ctx.unit.id, target.id, ctx)) { // just wait unit.state.action = 'Idle'; + return; } unit.state.action = 'Harvesting' hc.harvestingProgress += dt; if (hc.harvestingProgress >= hc.harvestingTime) { - hc.resourcesCarried = HARVESTING_RESOURCE_COUNT; - resource.value -= HARVESTING_RESOURCE_COUNT; + hc.resourcesCarried = hc.harvestingValue; + resource.value -= hc.resourcesCarried; // TODO - reset harvesting at any other action // maybe i could use some "exit state function"? hc.harvestingProgress = 0; + unit.state.action = 'Idle'; } } } else { @@ -191,14 +193,17 @@ export const harvestCommand = (ctx: CommandContext, cmd: CommandHarvest) => { } } -function resourceAvailable(targetId: UnitId, ctx: CommandContext) { - // Check if any other harvester is competing for the same resource - return ctx.gm.game.units.filter(u => { - u.state.state === "active" && - u.state.action === "Harvesting" && - u.state.current.typ === "Harvest" && - u.state.current.target === targetId - }).length > 0; +function resourceAvailableFor(unitId: UnitId, targetId: UnitId, ctx: CommandContext) { + // Check if any other harvester is competing for the same resource + const competingHarvesters = ctx.gm.game.units.filter(u => + u.id !== unitId + && u.state.state === "active" + && u.state.action === "Harvesting" + && u.state.current.typ === "Harvest" + && u.state.current.target === targetId + ); + + return competingHarvesters.length === 0; } export const produceCommand = (ctx: CommandContext, cmd: CommandProduce) => { diff --git a/packages/server/src/game/units.ts b/packages/server/src/game/units.ts index b722972..9556e1b 100644 --- a/packages/server/src/game/units.ts +++ b/packages/server/src/game/units.ts @@ -14,7 +14,7 @@ const UNIT_CATALOG : Catalog = { { type: 'Hp', maxHp: 50, hp: 50 }, { type: 'Mover', speed: 10 }, { type: 'Attacker', damage: 5, attackRate: 1000, range: 2, cooldown: 0 }, - { type: 'Harvester', harvestingTime: 1000, harvestingValue: 20, harvestingProgress: 0 }, + { type: 'Harvester', harvestingTime: 2000, harvestingValue: 8, harvestingProgress: 0 }, { type: 'Builder', buildingsProduced: [ { buildingType: 'Base', buildTime: 5000, buildCost: 400 }, { buildingType: 'Barracks', buildTime: 5000, buildCost: 150}, diff --git a/packages/server/test/behaviour.test.ts b/packages/server/test/behaviour.test.ts index d60d54e..63f40c2 100644 --- a/packages/server/test/behaviour.test.ts +++ b/packages/server/test/behaviour.test.ts @@ -287,6 +287,38 @@ describe('harvest action', () => { expect(game.units[1].state.action).toBe('Moving'); expect(game.players[0].resources).toBe(8); }); + + test('compete for one resource', () => { + const game = createBasicGame({}, 40); + + spawnUnit(game, 0, "ResourceNode", {x: 15, y: 15}); + // the first one is closer + spawnUnit(game, 1, "Harvester", {x: 10, y: 14 }); // id 2 + spawnUnit(game, 1, "Harvester", {x: 25, y: 16 }); // id 3 + + command({ + command: { typ: 'Harvest', target: 1 }, + unitIds: [2, 3], + shift: false, + }, + game, + 1 + ); + + for (let i = 0; i < 2 * 10; i++) { + tick(TICK_MS, game); + } + + expect(game.units[1].state.action).toBe('Harvesting'); + expect(game.units[2].state.action).toBe('Idle'); + + for (let i = 0; i < 4 * 10; i++) { + tick(TICK_MS, game); + } + + expect(game.units[2].state.action).toBe('Harvesting'); + expect(game.units[1].state.action).toBe('Idle'); + }); }); describe('build action', () => {