New weapon management, including shotgun

This commit is contained in:
2025-07-14 21:33:32 +02:00
parent 61b8da8ccf
commit cdefecdeba
43 changed files with 651 additions and 133 deletions

View File

@@ -225,6 +225,7 @@ export default class CthulhuEternalProtagonistSheet extends CthulhuEternalActorS
case "damage":
li = $(event.currentTarget).parents(".item");
item = this.actor.items.get(li.data("item-id"));
item.damageFormula = $(event.currentTarget).data("roll-value") || item.system.damage
item.damageBonus = this.actor.system.damageBonus
break
case "san":

View File

@@ -154,7 +154,8 @@ export const WEAPON_SKILL_MAPPING = {
"rangedprimitive": "CTHULHUETERNAL.Skill.Firearms",
"rangedthrown": "CTHULHUETERNAL.Skill.Athletics",
"rangedfirearm": "CTHULHUETERNAL.Skill.Firearms",
"unarmed": "CTHULHUETERNAL.Skill.UnarmedCombat"
"unarmed": "CTHULHUETERNAL.Skill.UnarmedCombat",
"rangedexplosive": "CTHULHUETERNAL.Skill.MilitaryTrainingExplosive"
},
victorian: {
"melee": "CTHULHUETERNAL.Skill.Melee",
@@ -245,6 +246,45 @@ export const WEAPON_SELECTIVE_FIRE_CHOICES = {
"longspray": { id: "longspray", label: "CTHULHUETERNAL.Weapon.SelectiveFire.longspray", ammoUsed: 20, lethality: 10, killRadius: 3},
}
// Melee stuff
export const WEAPON_MELEE_TARGET_MOVE = {
"normal": { id: "normal", label: "CTHULHUETERNAL.Weapon.Target.Normal", modifier: 0 },
"stationary": { id: "stationary", label: "CTHULHUETERNAL.Weapon.Target.Stationary", modifier: 20 },
"movingfast": { id: "movingfast", label: "CTHULHUETERNAL.Weapon.Target.MovingFast", modifier: -20 },
"movingveryfast": { id: "movingveryfast", label: "CTHULHUETERNAL.Weapon.Target.MovingVeryFast", modifier: -40 },
}
// Ranged stuff
export const WEAPON_RANGED_RANGE = {
"pointblank": { id: "pointblank", label: "CTHULHUETERNAL.Weapon.Range.PointBlank", modifier: +20 },
"normal": { id: "normal", label: "CTHULHUETERNAL.Weapon.Range.Normal", modifier: 0 },
"range2x": { id: "range2x", label: "CTHULHUETERNAL.Weapon.Range.Range2x", modifier: -20 },
"range5x": { id: "range5x", label: "CTHULHUETERNAL.Weapon.Range.Range5x", modifier: -40 }
}
export const WEAPON_RANGED_TARGET_MOVE = {
"normal": { id: "normal", label: "CTHULHUETERNAL.Weapon.Target.Normal", modifier: 0 },
"stationary": { id: "stationary", label: "CTHULHUETERNAL.Weapon.Target.Stationary", modifier: 20 },
"movingfast": { id: "movingfast", label: "CTHULHUETERNAL.Weapon.Target.MovingRange", modifier: -20 },
"movingveryfast": { id: "movingveryfast", label: "CTHULHUETERNAL.Weapon.Target.MovingVeryFast", modifier: -40 },
}
// Common stuff
export const WEAPON_ATTACKER_STATE = {
"normal": { id: "normal", label: "CTHULHUETERNAL.Weapon.Target.Normal", modifier: 0 },
"irritated": { id: "irritated", label: "CTHULHUETERNAL.Weapon.Attacker.Irritated", modifier: -20 },
"corrosive": { id: "corrosive", label: "CTHULHUETERNAL.Weapon.Attacker.Corrosive", modifier: -40 },
}
export const WEAPON_TARGET_SIZE = {
"normal": { id: "normal", label: "CTHULHUETERNAL.Weapon.Target.Normal", modifier: 0 },
"halfcovered": { id: "halfcovered", label: "CTHULHUETERNAL.Weapon.Target.HalfCovered", modifier: -20 },
"covered": { id: "covered", label: "CTHULHUETERNAL.Weapon.Target.Covered", modifier: -40 },
}
export const WEAPON_VISIBILITY = {
"clear": { id: "clear", label: "CTHULHUETERNAL.Weapon.Visibility.Clear", modifier: 0 },
"obscured": { id: "obscured", label: "CTHULHUETERNAL.Weapon.Visibility.Obscured", modifier: -20 },
"darkness": { id: "darkness", label: "CTHULHUETERNAL.Weapon.Visibility.Darkness", modifier: -40 },
}
export const RITUAL_TYPES = {
"simple": "CTHULHUETERNAL.Ritual.Simple",
"difficult": "CTHULHUETERNAL.Ritual.Difficult",
@@ -277,5 +317,11 @@ export const SYSTEM = {
MULTIPLIER_CHOICES,
ASCII,
DAMAGE_BONUS,
RITUAL_TYPES
RITUAL_TYPES,
WEAPON_MELEE_TARGET_MOVE,
WEAPON_RANGED_RANGE,
WEAPON_RANGED_TARGET_MOVE,
WEAPON_ATTACKER_STATE,
WEAPON_TARGET_SIZE,
WEAPON_VISIBILITY
}

View File

@@ -3,7 +3,8 @@ export const WEAPON_TYPE = {
"rangedprimitive": "CTHULHUETERNAL.Weapon.WeaponType.rangedprimitive",
"rangedthrown": "CTHULHUETERNAL.Weapon.WeaponType.rangedthrown",
"rangedfirearm": "CTHULHUETERNAL.Weapon.WeaponType.rangedfirearm",
"unarmed": "CTHULHUETERNAL.Weapon.WeaponType.unarmed"
"unarmed": "CTHULHUETERNAL.Weapon.WeaponType.unarmed",
"rangedexplosive": "CTHULHUETERNAL.Weapon.WeaponType.rangedexplosive",
}
export const WEAPON_SUBTYPE = {

View File

@@ -106,7 +106,7 @@ export default class CthulhuEternalRoll extends Roll {
}
static buildSelectiveFireChoices(actor, weapon) {
if (!weapon || !weapon?.system?.hasSelectiveFire) {
if (!weapon?.system?.hasSelectiveFire) {
return {}
}
// Loop thru the selective fire choices and build the choices object when enough ammo in the weapon
@@ -144,6 +144,8 @@ export default class CthulhuEternalRoll extends Roll {
ammoUsed = choice.ammoUsed // Override ammo used
}
ammoUsed = Number(ammoUsed)
if (weapon.system.lethality > 0) {
let lethalityRoll = new Roll("1d100")
await lethalityRoll.evaluate()
@@ -175,8 +177,8 @@ export default class CthulhuEternalRoll extends Roll {
}
// If the weapon is not lethal, we can proceed with the regular damage roll
let formula = weapon.system.damage
if (weapon.system.weaponType === "melee" || weapon.system.weaponType === "unarmed") {
let formula = weapon?.damageFormula || weapon.system.damage || "0"
if (weapon.system.applyDamageBonus) {
formula += ` + ${actor.system?.damageBonus}`
}
if (options?.previousResultType === "successCritical") {
@@ -205,6 +207,19 @@ export default class CthulhuEternalRoll extends Roll {
}, { rollMode: options.rollMode, create: true })
}
static computeWeaponModifiers(rollData) {
let modifier = SYSTEM.WEAPON_MELEE_TARGET_MOVE[rollData.meleeTargetMoveChoice]?.modifier || 0
modifier += SYSTEM.WEAPON_RANGED_RANGE[rollData.rangedRangeChoice]?.modifier || 0
modifier += SYSTEM.WEAPON_RANGED_TARGET_MOVE[rollData.rangedTargetMoveChoice]?.modifier || 0
modifier += SYSTEM.WEAPON_VISIBILITY[rollData.visibilityChoice]?.modifier || 0
modifier += SYSTEM.WEAPON_ATTACKER_STATE[rollData.attackerStateChoice]?.modifier || 0
modifier += SYSTEM.WEAPON_TARGET_SIZE[rollData.targetSizeChoice]?.modifier || 0
modifier += (rollData.aimingLastRound) ? 20 : 0
modifier += (rollData.aimingWithSight) ? 20 : 0
return modifier
}
/**
* Prompt the user with a dialog to configure and execute a roll.
*
@@ -311,6 +326,7 @@ export default class CthulhuEternalRoll extends Roll {
rollType: options.rollType,
rollItem: foundry.utils.duplicate(options.rollItem), // Object only, no class
weapon: options?.weapon,
isRangedWeapon: options?.weapon?.system?.isRanged(),
initialScore: options.initialScore,
targetScore: options.initialScore,
isLowWP: options.isLowWP,
@@ -324,12 +340,26 @@ export default class CthulhuEternalRoll extends Roll {
choiceModifier,
choiceMultiplier,
choiceSelectiveFire,
choiceMeleeTargetMove: SYSTEM.WEAPON_MELEE_TARGET_MOVE,
choiceRangedRange: SYSTEM.WEAPON_RANGED_RANGE,
choiceRangedTargetMove: SYSTEM.WEAPON_RANGED_TARGET_MOVE,
choiceVisibility: SYSTEM.WEAPON_VISIBILITY,
choiceAttackerState: SYSTEM.WEAPON_ATTACKER_STATE,
choiceTargetSize: SYSTEM.WEAPON_TARGET_SIZE,
selectiveFireChoice: "shortburst",
meleeTargetMoveChoice: "normal",
rangedRangeChoice: "normal",
rangedTargetMoveChoice: "normal",
visibilityChoice: "clear",
attackerStateChoice: "normal",
targetSizeChoice: "normal",
aimingLastRound: false,
aimingWithSight: false,
modifier,
formula,
hasTarget: options.hasTarget,
hasModifier,
hasMultiplier,
modifier,
selectiveFireChoice: "shortburst",
multiplier
}
const content = await foundry.applications.handlebars.renderTemplate("systems/fvtt-cthulhu-eternal/templates/roll-dialog.hbs", dialogContext)
@@ -386,7 +416,9 @@ export default class CthulhuEternalRoll extends Roll {
if (options.rollType === "resource") {
rollData.targetScore = options.initialScore * Number(rollContext.multiplier)
} else {
rollData.targetScore = Math.min(Math.max(options.initialScore + Number(rollData.modifier), 0), 100)
let totalModifier = this.computeWeaponModifiers(rollData) + Number(rollData.modifier)
rollData.totalModifier = Math.min(totalModifier, 40)
rollData.targetScore = Math.min(Math.max(options.initialScore + Number(rollData.totalModifier), 0), 100)
if (rollData.isLowWP || rollData.isExhausted) {
rollData.targetScore -= 20
}
@@ -395,7 +427,7 @@ export default class CthulhuEternalRoll extends Roll {
}
rollData.targetScore = Math.min(Math.max(rollData.targetScore, 0), 100)
}
if (!rollData.targetScore) {
if (rollData.targetScore === undefined || rollData.targetScore === null) {
rollData.targetScore = options.initialScore
rollData.modifier = "0"
}
@@ -456,6 +488,7 @@ export default class CthulhuEternalRoll extends Roll {
this.options.isCritical = resultType === "successCritical" || resultType === "failureCritical"
}
rollData.resultType = resultType
this.options.isLowWP = rollData.isLowWP
this.options.isZeroWP = rollData.isZeroWP
this.options.isExhausted = rollData.isExhausted
@@ -601,12 +634,14 @@ export default class CthulhuEternalRoll extends Roll {
rollItem: rollItem,
rollData: rollData
}
// Get array of gamemaster ID
let msg = await foundry.applications.handlebars.renderTemplate("systems/fvtt-cthulhu-eternal/templates/chat-san-request.hbs", msgData)
let chatMsg = await ChatMessage.create({
user: game.user.id,
content: msg,
speaker: ChatMessage.getSpeaker({ actor: rollData.actor })
}, { rollMode: rollData.rollMode, create: true })
speaker: ChatMessage.getSpeaker({ actor: rollData.actor }),
whisper: game.users.filter(u => u.isGM).map(u => u.id),
})
await chatMsg.setFlag("fvtt-cthulhu-eternal", "rollData", rollData)
}
}

View File

@@ -90,6 +90,10 @@ export default class CthulhuEternalProtagonist extends foundry.abstract.TypeData
prepareDerivedData() {
super.prepareDerivedData();
if (!game.user.isGM ) {
return
}
let updates = {}
if (this.wp.max !== this.characteristics.pow.value) {
updates[`system.wp.max`] = this.characteristics.pow.value
@@ -123,8 +127,6 @@ export default class CthulhuEternalProtagonist extends foundry.abstract.TypeData
dmgBonus = -2
} else if (this.characteristics.str.value <= 8) {
dmgBonus = -1
} else if (this.characteristics.str.value <= 12) {
dmgBonus = 0
} else if (this.characteristics.str.value <= 16) {
dmgBonus = 1
} else if (this.characteristics.str.value <= 20) {
@@ -137,9 +139,12 @@ export default class CthulhuEternalProtagonist extends foundry.abstract.TypeData
// BP (Breaking Point) management
if (!this.san.breakingPointReached && this.san.value <= this.san.breakingPoint) {
updates[`system.san.breakingPointReached`] = true
this.san.breakingPointReached = true // Force local update to true
ChatMessage.create({
content: `<p>${game.i18n.format("CTHULHUETERNAL.Label.breakingPointReached", { bp: this.san.breakingPoint, san: this.san.value })}</p>`,
speaker: ChatMessage.getSpeaker({ actor: this.parent })
speaker: ChatMessage.getSpeaker({ actor: this.parent }),
// Get the user id of the actor owner
whisper: [game.users.find(u => u.character?.name === this.parent?.name).id ]
})
}
@@ -195,10 +200,6 @@ export default class CthulhuEternalProtagonist extends foundry.abstract.TypeData
}
async applySANConsequences(rollData) {
// If sanType is "non", do nothing
if (rollData.sanType === "none") {
return
}
let msgData = {
sanType: rollData.sanType,
sanLoss: rollData.sanLoss,
@@ -265,14 +266,21 @@ export default class CthulhuEternalProtagonist extends foundry.abstract.TypeData
updates[`system.san.helplessness`] = [false, false, false]
msgData.adaptedToHelplessness = true
}
} else if (rollData.sanType === "unnatural" ) {
template = "systems/fvtt-cthulhu-eternal/templates/chat-san-loss-unnatural.hbs"
} else {
template = "systems/fvtt-cthulhu-eternal/templates/chat-san-loss-none.hbs"
}
console.log("CthulhuEternalProtagonist.applySANConsequences", rollData, updates, template)
let content = await foundry.applications.handlebars.renderTemplate(template, msgData)
let msg = await ChatMessage.create({
content: content,
speaker: ChatMessage.getSpeaker({ actor: this.parent })
speaker: ChatMessage.getSpeaker({ actor: this.parent }),
whisper: game.users.filter(u => u.isGM).map(u => u.id),
})
msg.setFlag("fvtt-cthulhu-eternal", "rollData", msgData)
await msg.setFlag("fvtt-cthulhu-eternal", "rollData", msgData)
if (Object.keys(updates).length > 0) {
this.parent.update(updates)
}
@@ -284,12 +292,14 @@ export default class CthulhuEternalProtagonist extends foundry.abstract.TypeData
let san = Math.max(Math.min(this.san.value + rollData.sanLoss, this.san.max), 0)
if (this.san.value !== san) {
updates[`system.san.value`] = san
rollData.sanValue = san
const content = await foundry.applications.handlebars.renderTemplate("systems/fvtt-cthulhu-eternal/templates/chat-san-type-request.hbs", rollData)
let msg = await ChatMessage.create({
content: content,
speaker: ChatMessage.getSpeaker({ actor: this.parent })
speaker: ChatMessage.getSpeaker({ actor: this.parent }),
whisper: game.users.filter(u => u.isGM).map(u => u.id)
})
msg.setFlag("fvtt-cthulhu-eternal", "rollData", rollData)
await msg.setFlag("fvtt-cthulhu-eternal", "rollData", rollData)
}
if (Object.keys(updates).length > 0) {
this.parent.update(updates)

View File

@@ -13,12 +13,23 @@ export default class CthulhuEternalWeapon extends foundry.abstract.TypeDataModel
schema.weaponType = new fields.StringField({ required: true, initial: "melee", choices: SYSTEM.WEAPON_TYPE })
schema.hasDirectSkill = new fields.BooleanField({ required: true, initial: false })
schema.directSkillValue = new fields.NumberField({ required: true, initial: 0, min: 0, max:99 })
schema.directSkillValue = new fields.NumberField({ required: true, initial: 0, min: 0, max: 99 })
schema.hasDamageDistance = new fields.BooleanField({ required: true, initial: false })
schema.damageDistance = new fields.SchemaField(Array.fromRange(6, 1).reduce((damageDistance, i) => {
damageDistance[`dist${i}`] = new fields.SchemaField({
damage: new fields.StringField({ required: true, initial: "1d6" }),
distance: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 })
})
return damageDistance
}, {}));
schema.hasSelectiveFire = new fields.BooleanField({ required: true, initial: false })
schema.damage = new fields.StringField({required: true, initial: "1d6"})
schema.hasSight = new fields.BooleanField({ required: true, initial: false })
schema.isStunning = new fields.BooleanField({ required: true, initial: false })
schema.damage = new fields.StringField({ required: true, initial: "1d6" })
schema.applyDamageBonus = new fields.BooleanField({ required: true, initial: false })
schema.baseRange = new fields.StringField({required: true, initial: ""})
schema.baseRange = new fields.StringField({ required: true, initial: "" })
schema.rangeUnit = new fields.StringField({ required: true, initial: "yard", choices: SYSTEM.WEAPON_RANGE_UNIT })
schema.lethality = new fields.NumberField({ required: true, initial: 0, min: 0 })
schema.killRadius = new fields.NumberField({ required: true, initial: 0, min: 0 })
@@ -43,7 +54,8 @@ export default class CthulhuEternalWeapon extends foundry.abstract.TypeDataModel
}
isRanged() {
return this.weaponType.includes("ranged")
console.log("isRanged", this.weaponType, this)
return this.weaponType.match("ranged")
}
isFireArm() {

View File

@@ -196,11 +196,6 @@ export default class CthulhuEternalUtils {
ui.notifications.error(game.i18n.localize("CTHULHUETERNAL.Notifications.noSanTypeFound"))
return
}
// If the sanType is "none", we don't apply any SAN processing
if (sanType === "none") {
ui.notifications.info(game.i18n.localize("CTHULHUETERNAL.Notifications.noSanLossApplied"))
return
}
rollData.sanType = sanType
await actor.system.applySANConsequences(rollData)
// Delete the roll message
@@ -253,7 +248,7 @@ export default class CthulhuEternalUtils {
})
}
static async damageRoll(rollMessage) {
static async damageRoll(rollMessage, formula = null) {
let rollData = rollMessage.rolls[0]?.options?.rollData
let actor = game.actors.get(rollData.actorId)
if (!actor) {
@@ -263,6 +258,7 @@ export default class CthulhuEternalUtils {
console.log("Damage roll data", rollData)
rollData.weapon.resultType = rollData.resultType // Keep the result type from the roll message
rollData.weapon.selectiveFireChoice = rollData.selectiveFireChoice // Keep the selected fire choice from the roll message
rollData.weapon.damageFormula = formula || rollData.weapon.system.damage
actor.system.roll("damage", rollData.weapon)
}