mirror of
https://gitea.wildfiregames.com/0ad/0ad.git
synced 2026-07-26 04:51:29 +00:00
Use JS classes for QueuePlan
Classes derifed from `QueuePlan` used manual inherintance. Refs: #6285
This commit is contained in:
@@ -5,64 +5,62 @@ import { aiWarn } from "simulation/ai/common-api/utils.js";
|
||||
* Common functions and variables to all queue plans.
|
||||
*/
|
||||
|
||||
export function QueuePlan(gameState, type, metadata)
|
||||
export class QueuePlan
|
||||
{
|
||||
this.type = gameState.applyCiv(type);
|
||||
this.metadata = metadata;
|
||||
|
||||
this.template = gameState.getTemplate(this.type);
|
||||
if (!this.template)
|
||||
constructor(gameState, type, metadata)
|
||||
{
|
||||
this.type = gameState.applyCiv(type);
|
||||
this.metadata = metadata;
|
||||
|
||||
this.template = gameState.getTemplate(this.type);
|
||||
if (!this.template)
|
||||
throw new Error("Tried to add the inexisting template " + this.type + " to Petra.");
|
||||
this.ID = gameState.ai.uniqueIDs.plans++;
|
||||
this.cost = new ResourcesManager(this.template.cost());
|
||||
this.number = 1;
|
||||
this.category = "";
|
||||
}
|
||||
|
||||
/** Check the content of this queue */
|
||||
isInvalid(gameState)
|
||||
{
|
||||
aiWarn("Tried to add the inexisting template " + this.type + " to Petra.");
|
||||
return false;
|
||||
}
|
||||
this.ID = gameState.ai.uniqueIDs.plans++;
|
||||
this.cost = new ResourcesManager(this.template.cost());
|
||||
this.number = 1;
|
||||
this.category = "";
|
||||
|
||||
return true;
|
||||
/** if true, the queue manager will begin increasing this plan's account. */
|
||||
isGo(gameState)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/** can we start this plan immediately? */
|
||||
canStart(gameState)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/** process the plan. */
|
||||
start(gameState)
|
||||
{
|
||||
// should call onStart.
|
||||
}
|
||||
|
||||
getCost()
|
||||
{
|
||||
const costs = new ResourcesManager();
|
||||
costs.add(this.cost);
|
||||
if (this.number !== 1)
|
||||
costs.multiply(this.number);
|
||||
return costs;
|
||||
}
|
||||
|
||||
/**
|
||||
* On Event functions.
|
||||
* Can be used to do some specific stuffs
|
||||
* Need to be updated to actually do something if you want them to.
|
||||
* this is called by "Start" if it succeeds.
|
||||
*/
|
||||
onStart(gameState)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/** Check the content of this queue */
|
||||
QueuePlan.prototype.isInvalid = function(gameState)
|
||||
{
|
||||
return false;
|
||||
};
|
||||
|
||||
/** if true, the queue manager will begin increasing this plan's account. */
|
||||
QueuePlan.prototype.isGo = function(gameState)
|
||||
{
|
||||
return true;
|
||||
};
|
||||
|
||||
/** can we start this plan immediately? */
|
||||
QueuePlan.prototype.canStart = function(gameState)
|
||||
{
|
||||
return false;
|
||||
};
|
||||
|
||||
/** process the plan. */
|
||||
QueuePlan.prototype.start = function(gameState)
|
||||
{
|
||||
// should call onStart.
|
||||
};
|
||||
|
||||
QueuePlan.prototype.getCost = function()
|
||||
{
|
||||
const costs = new ResourcesManager();
|
||||
costs.add(this.cost);
|
||||
if (this.number !== 1)
|
||||
costs.multiply(this.number);
|
||||
return costs;
|
||||
};
|
||||
|
||||
/**
|
||||
* On Event functions.
|
||||
* Can be used to do some specific stuffs
|
||||
* Need to be updated to actually do something if you want them to.
|
||||
* this is called by "Start" if it succeeds.
|
||||
*/
|
||||
QueuePlan.prototype.onStart = function(gameState)
|
||||
{
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,110 +1,108 @@
|
||||
import { ResourcesManager } from "simulation/ai/common-api/resources.js";
|
||||
import { QueuePlan } from "simulation/ai/petra/queueplan.js";
|
||||
|
||||
export function ResearchPlan(gameState, type, rush = false)
|
||||
export class ResearchPlan extends QueuePlan
|
||||
{
|
||||
if (!QueuePlan.call(this, gameState, type, {}))
|
||||
return false;
|
||||
|
||||
if (this.template.researchTime === undefined)
|
||||
return false;
|
||||
|
||||
// Refine the estimated cost
|
||||
const researchers = this.getBestResearchers(gameState, true);
|
||||
if (researchers)
|
||||
this.cost = new ResourcesManager(this.template.cost(researchers[0]));
|
||||
|
||||
this.category = "technology";
|
||||
this.rush = rush;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
ResearchPlan.prototype = Object.create(QueuePlan.prototype);
|
||||
|
||||
ResearchPlan.prototype.canStart = function(gameState)
|
||||
{
|
||||
this.researchers = this.getBestResearchers(gameState);
|
||||
if (!this.researchers)
|
||||
return false;
|
||||
this.cost = new ResourcesManager(this.template.cost(this.researchers[0]));
|
||||
return true;
|
||||
};
|
||||
|
||||
ResearchPlan.prototype.getBestResearchers = function(gameState, noRequirementCheck = false)
|
||||
{
|
||||
const allResearchers = gameState.findResearchers(this.type, noRequirementCheck);
|
||||
if (!allResearchers || !allResearchers.hasEntities())
|
||||
return undefined;
|
||||
|
||||
// Keep only researchers with smallest cost
|
||||
let costMin = Math.min();
|
||||
let researchers;
|
||||
for (const ent of allResearchers.values())
|
||||
constructor(gameState, type, rush = false)
|
||||
{
|
||||
const cost = this.template.costSum(ent);
|
||||
if (cost === costMin)
|
||||
researchers.push(ent);
|
||||
else if (cost < costMin)
|
||||
super(gameState, type, {});
|
||||
|
||||
if (this.template.researchTime === undefined)
|
||||
throw new Error();
|
||||
|
||||
// Refine the estimated cost
|
||||
const researchers = this.getBestResearchers(gameState, true);
|
||||
if (researchers)
|
||||
this.cost = new ResourcesManager(this.template.cost(researchers[0]));
|
||||
|
||||
this.category = "technology";
|
||||
this.rush = rush;
|
||||
}
|
||||
|
||||
canStart(gameState)
|
||||
{
|
||||
this.researchers = this.getBestResearchers(gameState);
|
||||
if (!this.researchers)
|
||||
return false;
|
||||
this.cost = new ResourcesManager(this.template.cost(this.researchers[0]));
|
||||
return true;
|
||||
}
|
||||
|
||||
getBestResearchers(gameState, noRequirementCheck = false)
|
||||
{
|
||||
const allResearchers = gameState.findResearchers(this.type, noRequirementCheck);
|
||||
if (!allResearchers || !allResearchers.hasEntities())
|
||||
return undefined;
|
||||
|
||||
// Keep only researchers with smallest cost
|
||||
let costMin = Math.min();
|
||||
let researchers;
|
||||
for (const ent of allResearchers.values())
|
||||
{
|
||||
costMin = cost;
|
||||
researchers = [ent];
|
||||
const cost = this.template.costSum(ent);
|
||||
if (cost === costMin)
|
||||
researchers.push(ent);
|
||||
else if (cost < costMin)
|
||||
{
|
||||
costMin = cost;
|
||||
researchers = [ent];
|
||||
}
|
||||
}
|
||||
return researchers;
|
||||
}
|
||||
|
||||
isInvalid(gameState)
|
||||
{
|
||||
return gameState.isResearched(this.type) || gameState.isResearching(this.type);
|
||||
}
|
||||
|
||||
start(gameState)
|
||||
{
|
||||
// Prefer researcher with shortest queues (no need to serialize this.researchers
|
||||
// as the functions canStart and start are always called on the same turn)
|
||||
this.researchers.sort((a, b) => a.trainingQueueTime() - b.trainingQueueTime());
|
||||
// Drop anything in the queue if we rush it.
|
||||
if (this.rush)
|
||||
this.researchers[0].stopAllProduction(0.45);
|
||||
this.researchers[0].research(this.type);
|
||||
this.onStart(gameState);
|
||||
}
|
||||
|
||||
onStart(gameState)
|
||||
{
|
||||
if (this.queueToReset)
|
||||
gameState.ai.queueManager.changePriority(this.queueToReset, gameState.ai.Config.priorities[this.queueToReset]);
|
||||
|
||||
for (let i = gameState.getNumberOfPhases(); i > 0; --i)
|
||||
{
|
||||
if (this.type != gameState.getPhaseName(i))
|
||||
continue;
|
||||
gameState.ai.HQ.phasing = 0;
|
||||
gameState.ai.HQ.OnPhaseUp(gameState, i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return researchers;
|
||||
};
|
||||
|
||||
ResearchPlan.prototype.isInvalid = function(gameState)
|
||||
{
|
||||
return gameState.isResearched(this.type) || gameState.isResearching(this.type);
|
||||
};
|
||||
|
||||
ResearchPlan.prototype.start = function(gameState)
|
||||
{
|
||||
// Prefer researcher with shortest queues (no need to serialize this.researchers
|
||||
// as the functions canStart and start are always called on the same turn)
|
||||
this.researchers.sort((a, b) => a.trainingQueueTime() - b.trainingQueueTime());
|
||||
// Drop anything in the queue if we rush it.
|
||||
if (this.rush)
|
||||
this.researchers[0].stopAllProduction(0.45);
|
||||
this.researchers[0].research(this.type);
|
||||
this.onStart(gameState);
|
||||
};
|
||||
|
||||
ResearchPlan.prototype.onStart = function(gameState)
|
||||
{
|
||||
if (this.queueToReset)
|
||||
gameState.ai.queueManager.changePriority(this.queueToReset, gameState.ai.Config.priorities[this.queueToReset]);
|
||||
|
||||
for (let i = gameState.getNumberOfPhases(); i > 0; --i)
|
||||
Serialize()
|
||||
{
|
||||
if (this.type != gameState.getPhaseName(i))
|
||||
continue;
|
||||
gameState.ai.HQ.phasing = 0;
|
||||
gameState.ai.HQ.OnPhaseUp(gameState, i);
|
||||
break;
|
||||
return {
|
||||
"category": this.category,
|
||||
"type": this.type,
|
||||
"ID": this.ID,
|
||||
"metadata": this.metadata,
|
||||
"cost": this.cost.Serialize(),
|
||||
"number": this.number,
|
||||
"rush": this.rush,
|
||||
"queueToReset": this.queueToReset || undefined
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
ResearchPlan.prototype.Serialize = function()
|
||||
{
|
||||
return {
|
||||
"category": this.category,
|
||||
"type": this.type,
|
||||
"ID": this.ID,
|
||||
"metadata": this.metadata,
|
||||
"cost": this.cost.Serialize(),
|
||||
"number": this.number,
|
||||
"rush": this.rush,
|
||||
"queueToReset": this.queueToReset || undefined
|
||||
};
|
||||
};
|
||||
Deserialize(gameState, data)
|
||||
{
|
||||
for (const key in data)
|
||||
this[key] = data[key];
|
||||
|
||||
ResearchPlan.prototype.Deserialize = function(gameState, data)
|
||||
{
|
||||
for (const key in data)
|
||||
this[key] = data[key];
|
||||
|
||||
this.cost = new ResourcesManager();
|
||||
this.cost.Deserialize(data.cost);
|
||||
};
|
||||
this.cost = new ResourcesManager();
|
||||
this.cost.Deserialize(data.cost);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,162 +4,157 @@ import { aiWarn } from "simulation/ai/common-api/utils.js";
|
||||
import { QueuePlan } from "simulation/ai/petra/queueplan.js";
|
||||
import { Worker } from "simulation/ai/petra/worker.js";
|
||||
|
||||
export function TrainingPlan(gameState, type, metadata, number = 1, maxMerge = 5)
|
||||
export class TrainingPlan extends QueuePlan
|
||||
{
|
||||
if (!QueuePlan.call(this, gameState, type, metadata))
|
||||
constructor(gameState, type, metadata, number = 1, maxMerge = 5)
|
||||
{
|
||||
aiWarn(" Plan training " + type + " canceled");
|
||||
return false;
|
||||
super(gameState, type, metadata);
|
||||
|
||||
// Refine the estimated cost and add pop cost
|
||||
const trainers = this.getBestTrainers(gameState);
|
||||
const trainer = trainers ? trainers[0] : undefined;
|
||||
this.cost = new ResourcesManager(this.template.cost(trainer), +this.template._template.Cost.Population);
|
||||
|
||||
this.category = "unit";
|
||||
this.number = number;
|
||||
this.maxMerge = maxMerge;
|
||||
}
|
||||
|
||||
// Refine the estimated cost and add pop cost
|
||||
const trainers = this.getBestTrainers(gameState);
|
||||
const trainer = trainers ? trainers[0] : undefined;
|
||||
this.cost = new ResourcesManager(this.template.cost(trainer), +this.template._template.Cost.Population);
|
||||
|
||||
this.category = "unit";
|
||||
this.number = number;
|
||||
this.maxMerge = maxMerge;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
TrainingPlan.prototype = Object.create(QueuePlan.prototype);
|
||||
|
||||
TrainingPlan.prototype.canStart = function(gameState)
|
||||
{
|
||||
this.trainers = this.getBestTrainers(gameState);
|
||||
if (!this.trainers)
|
||||
return false;
|
||||
this.cost = new ResourcesManager(this.template.cost(this.trainers[0]), +this.template._template.Cost.Population);
|
||||
return true;
|
||||
};
|
||||
|
||||
TrainingPlan.prototype.getBestTrainers = function(gameState)
|
||||
{
|
||||
if (this.metadata && this.metadata.trainer)
|
||||
canStart(gameState)
|
||||
{
|
||||
const trainer = gameState.getEntityById(this.metadata.trainer);
|
||||
if (trainer)
|
||||
return [trainer];
|
||||
this.trainers = this.getBestTrainers(gameState);
|
||||
if (!this.trainers)
|
||||
return false;
|
||||
this.cost = new ResourcesManager(this.template.cost(this.trainers[0]), +this.template._template.Cost.Population);
|
||||
return true;
|
||||
}
|
||||
|
||||
let allTrainers = gameState.findTrainers(this.type);
|
||||
if (this.metadata && this.metadata.sea)
|
||||
allTrainers = allTrainers.filter(filters.byMetadata(PlayerID, "sea", this.metadata.sea));
|
||||
if (this.metadata && this.metadata.base)
|
||||
allTrainers = allTrainers.filter(filters.byMetadata(PlayerID, "base", this.metadata.base));
|
||||
if (!allTrainers || !allTrainers.hasEntities())
|
||||
return undefined;
|
||||
|
||||
// Keep only trainers with smallest cost
|
||||
let costMin = Math.min();
|
||||
let trainers;
|
||||
for (const ent of allTrainers.values())
|
||||
getBestTrainers(gameState)
|
||||
{
|
||||
const cost = this.template.costSum(ent);
|
||||
if (cost === costMin)
|
||||
trainers.push(ent);
|
||||
else if (cost < costMin)
|
||||
if (this.metadata && this.metadata.trainer)
|
||||
{
|
||||
costMin = cost;
|
||||
trainers = [ent];
|
||||
const trainer = gameState.getEntityById(this.metadata.trainer);
|
||||
if (trainer)
|
||||
return [trainer];
|
||||
}
|
||||
}
|
||||
return trainers;
|
||||
};
|
||||
|
||||
TrainingPlan.prototype.start = function(gameState)
|
||||
{
|
||||
if (this.metadata && this.metadata.trainer)
|
||||
{
|
||||
const metadata = {};
|
||||
for (const key in this.metadata)
|
||||
if (key !== "trainer")
|
||||
metadata[key] = this.metadata[key];
|
||||
this.metadata = metadata;
|
||||
}
|
||||
let allTrainers = gameState.findTrainers(this.type);
|
||||
if (this.metadata && this.metadata.sea)
|
||||
allTrainers = allTrainers.filter(filters.byMetadata(PlayerID, "sea", this.metadata.sea));
|
||||
if (this.metadata && this.metadata.base)
|
||||
allTrainers = allTrainers.filter(filters.byMetadata(PlayerID, "base", this.metadata.base));
|
||||
if (!allTrainers || !allTrainers.hasEntities())
|
||||
return undefined;
|
||||
|
||||
if (this.trainers.length > 1)
|
||||
{
|
||||
let wantedIndex;
|
||||
if (this.metadata && this.metadata.index)
|
||||
wantedIndex = this.metadata.index;
|
||||
const workerUnit = this.metadata && this.metadata.role &&
|
||||
this.metadata.role === Worker.ROLE_WORKER;
|
||||
const supportUnit = this.template.hasClass("Support");
|
||||
this.trainers.sort(function(a, b)
|
||||
// Keep only trainers with smallest cost
|
||||
let costMin = Math.min();
|
||||
let trainers;
|
||||
for (const ent of allTrainers.values())
|
||||
{
|
||||
// Prefer training buildings with short queues
|
||||
let aa = a.trainingQueueTime();
|
||||
let bb = b.trainingQueueTime();
|
||||
// Give priority to support units in the cc
|
||||
if (a.hasClass("Civic") && !supportUnit)
|
||||
aa += 10;
|
||||
if (b.hasClass("Civic") && !supportUnit)
|
||||
bb += 10;
|
||||
// And support units should not be too near to dangerous place
|
||||
if (supportUnit)
|
||||
const cost = this.template.costSum(ent);
|
||||
if (cost === costMin)
|
||||
trainers.push(ent);
|
||||
else if (cost < costMin)
|
||||
{
|
||||
if (gameState.ai.HQ.isNearInvadingArmy(a.position()))
|
||||
aa += 50;
|
||||
if (gameState.ai.HQ.isNearInvadingArmy(b.position()))
|
||||
bb += 50;
|
||||
costMin = cost;
|
||||
trainers = [ent];
|
||||
}
|
||||
// Give also priority to buildings with the right accessibility
|
||||
const aBase = a.getMetadata(PlayerID, "base");
|
||||
const bBase = b.getMetadata(PlayerID, "base");
|
||||
if (wantedIndex)
|
||||
{
|
||||
if (!aBase || gameState.ai.HQ.getBaseByID(aBase).accessIndex != wantedIndex)
|
||||
aa += 30;
|
||||
if (!bBase || gameState.ai.HQ.getBaseByID(bBase).accessIndex != wantedIndex)
|
||||
bb += 30;
|
||||
}
|
||||
// Then, if workers, small preference for bases with less workers
|
||||
if (workerUnit && aBase && bBase && aBase != bBase)
|
||||
{
|
||||
const apop = gameState.ai.HQ.getBaseByID(aBase).workers.length;
|
||||
const bpop = gameState.ai.HQ.getBaseByID(bBase).workers.length;
|
||||
if (apop > bpop)
|
||||
aa++;
|
||||
else if (bpop > apop)
|
||||
bb++;
|
||||
}
|
||||
return aa - bb;
|
||||
});
|
||||
}
|
||||
return trainers;
|
||||
}
|
||||
|
||||
if (this.metadata && this.metadata.base !== undefined && this.metadata.base === 0)
|
||||
this.metadata.base = this.trainers[0].getMetadata(PlayerID, "base");
|
||||
this.trainers[0].train(gameState.getPlayerCiv(), this.type, this.number, this.metadata);
|
||||
start(gameState)
|
||||
{
|
||||
if (this.metadata && this.metadata.trainer)
|
||||
{
|
||||
const metadata = {};
|
||||
for (const key in this.metadata)
|
||||
if (key !== "trainer")
|
||||
metadata[key] = this.metadata[key];
|
||||
this.metadata = metadata;
|
||||
}
|
||||
|
||||
this.onStart(gameState);
|
||||
};
|
||||
if (this.trainers.length > 1)
|
||||
{
|
||||
let wantedIndex;
|
||||
if (this.metadata && this.metadata.index)
|
||||
wantedIndex = this.metadata.index;
|
||||
const workerUnit = this.metadata && this.metadata.role &&
|
||||
this.metadata.role === Worker.ROLE_WORKER;
|
||||
const supportUnit = this.template.hasClass("Support");
|
||||
this.trainers.sort(function(a, b)
|
||||
{
|
||||
// Prefer training buildings with short queues
|
||||
let aa = a.trainingQueueTime();
|
||||
let bb = b.trainingQueueTime();
|
||||
// Give priority to support units in the cc
|
||||
if (a.hasClass("Civic") && !supportUnit)
|
||||
aa += 10;
|
||||
if (b.hasClass("Civic") && !supportUnit)
|
||||
bb += 10;
|
||||
// And support units should not be too near to dangerous place
|
||||
if (supportUnit)
|
||||
{
|
||||
if (gameState.ai.HQ.isNearInvadingArmy(a.position()))
|
||||
aa += 50;
|
||||
if (gameState.ai.HQ.isNearInvadingArmy(b.position()))
|
||||
bb += 50;
|
||||
}
|
||||
// Give also priority to buildings with the right accessibility
|
||||
const aBase = a.getMetadata(PlayerID, "base");
|
||||
const bBase = b.getMetadata(PlayerID, "base");
|
||||
if (wantedIndex)
|
||||
{
|
||||
if (!aBase || gameState.ai.HQ.getBaseByID(aBase).accessIndex != wantedIndex)
|
||||
aa += 30;
|
||||
if (!bBase || gameState.ai.HQ.getBaseByID(bBase).accessIndex != wantedIndex)
|
||||
bb += 30;
|
||||
}
|
||||
// Then, if workers, small preference for bases with less workers
|
||||
if (workerUnit && aBase && bBase && aBase != bBase)
|
||||
{
|
||||
const apop = gameState.ai.HQ.getBaseByID(aBase).workers.length;
|
||||
const bpop = gameState.ai.HQ.getBaseByID(bBase).workers.length;
|
||||
if (apop > bpop)
|
||||
aa++;
|
||||
else if (bpop > apop)
|
||||
bb++;
|
||||
}
|
||||
return aa - bb;
|
||||
});
|
||||
}
|
||||
|
||||
TrainingPlan.prototype.addItem = function(amount = 1)
|
||||
{
|
||||
this.number += amount;
|
||||
};
|
||||
if (this.metadata && this.metadata.base !== undefined && this.metadata.base === 0)
|
||||
this.metadata.base = this.trainers[0].getMetadata(PlayerID, "base");
|
||||
this.trainers[0].train(gameState.getPlayerCiv(), this.type, this.number, this.metadata);
|
||||
|
||||
TrainingPlan.prototype.Serialize = function()
|
||||
{
|
||||
return {
|
||||
"category": this.category,
|
||||
"type": this.type,
|
||||
"ID": this.ID,
|
||||
"metadata": this.metadata,
|
||||
"cost": this.cost.Serialize(),
|
||||
"number": this.number,
|
||||
"maxMerge": this.maxMerge
|
||||
};
|
||||
};
|
||||
this.onStart(gameState);
|
||||
}
|
||||
|
||||
TrainingPlan.prototype.Deserialize = function(gameState, data)
|
||||
{
|
||||
for (const key in data)
|
||||
this[key] = data[key];
|
||||
addItem(amount = 1)
|
||||
{
|
||||
this.number += amount;
|
||||
}
|
||||
|
||||
this.cost = new ResourcesManager();
|
||||
this.cost.Deserialize(data.cost);
|
||||
};
|
||||
Serialize()
|
||||
{
|
||||
return {
|
||||
"category": this.category,
|
||||
"type": this.type,
|
||||
"ID": this.ID,
|
||||
"metadata": this.metadata,
|
||||
"cost": this.cost.Serialize(),
|
||||
"number": this.number,
|
||||
"maxMerge": this.maxMerge
|
||||
};
|
||||
}
|
||||
|
||||
Deserialize(gameState, data)
|
||||
{
|
||||
for (const key in data)
|
||||
this[key] = data[key];
|
||||
|
||||
this.cost = new ResourcesManager();
|
||||
this.cost.Deserialize(data.cost);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user