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
Original file line number Diff line number Diff line change
Expand Up @@ -842,11 +842,13 @@ function (Rulebook $_value, string $key) use ($selectedBooks): bool {
]
);
case 'review':
$character->validate();
return view(
'shadowrun5e::character',
[
'character' => $character,
'currentStep' => 'review',
// @phpstan-ignore argument.type
'errors' => new MessageBag($character->errors ?? []),
'nextStep' => $this->nextStep('review', $character),
'previousStep' => $this->previousStep('review', $character),
Expand Down
8 changes: 4 additions & 4 deletions Modules/Shadowrun5e/app/Models/AdeptPowerArray.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ final class AdeptPowerArray extends ArrayObject
{
/**
* Add a power to the array.
* @param AdeptPower $power
* @param AdeptPower $value
* @throws TypeError
*/
#[Override]
public function offsetSet(mixed $index = null, $power = null): void
public function offsetSet(mixed $key = null, $value = null): void
{
if ($power instanceof AdeptPower) {
parent::offsetSet($index, $power);
if ($value instanceof AdeptPower) {
parent::offsetSet($key, $value);
return;
}
throw new TypeError('AdeptPowerArray only accepts AdeptPower objects');
Expand Down
18 changes: 17 additions & 1 deletion Modules/Shadowrun5e/app/Models/Armor.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,23 @@ final class Armor implements Stringable

/**
* List of all armor.
* @var ?array<mixed>
* @var ?array<string, array{
* availability: string,
* capacity?: int,
* chummer-id?: string,
* container-type: array<int, string>,
* cost: int,
* description: string,
* effects?: array<string, int>,
* features: array<int, string>,
* id: string,
* name: string,
* page: int,
* rating?: int,
* ruleset: string,
* stack-rating: int,
* wireless-effects?: array<string, int>
* }>
*/
public static ?array $armor;

Expand Down
12 changes: 6 additions & 6 deletions Modules/Shadowrun5e/app/Models/Character.php
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ protected static function booted(): void
{
static::addGlobalScope(
'shadowrun5e',
function (Builder $builder): void {
static function (Builder $builder): void {
$builder->where('system', 'shadowrun5e');
}
);
Expand Down Expand Up @@ -550,7 +550,7 @@ public function getMartialArtsStyles(): MartialArtsStyleArray
public function getMartialArtsTechniques(): MartialArtsTechniqueArray
{
$techniques = new MartialArtsTechniqueArray();
if (!isset($this->martialArts, $this->martialArts['techniques'])) {
if (!isset($this->martialArts['techniques'])) {
return $techniques;
}
foreach ($this->martialArts['techniques'] as $technique) {
Expand Down Expand Up @@ -837,7 +837,7 @@ public function socialLimit(): Attribute
public function getSpells(): SpellArray
{
$spells = new SpellArray();
if (!isset($this->magics, $this->magics['spells'])) {
if (!isset($this->magics['spells'])) {
return $spells;
}
foreach ($this->magics['spells'] as $spell) {
Expand All @@ -860,7 +860,7 @@ public function getSpells(): SpellArray
public function getSpirits(): SpiritArray
{
$spirits = new SpiritArray();
if (!isset($this->magics, $this->magics['spirits'])) {
if (!isset($this->magics['spirits'])) {
return $spirits;
}
foreach ($this->magics['spirits'] as $spirit) {
Expand All @@ -886,7 +886,7 @@ public function getSpirits(): SpiritArray
public function getSprites(): SpriteArray
{
$sprites = new SpriteArray();
if (!isset($this->technomancer, $this->technomancer['sprites'])) {
if (!isset($this->technomancer['sprites'])) {
return $sprites;
}
foreach ($this->technomancer['sprites'] as $sprite) {
Expand All @@ -911,7 +911,7 @@ public function getSprites(): SpriteArray
*/
public function getTradition(): ?Tradition
{
if (!isset($this->magics, $this->magics['tradition'])) {
if (!isset($this->magics['tradition'])) {
return null;
}
try {
Expand Down
203 changes: 197 additions & 6 deletions Modules/Shadowrun5e/app/Models/PartialCharacter.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,34 @@
namespace Modules\Shadowrun5e\Models;

use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Model;
use Modules\Shadowrun5e\Database\Factories\PartialCharacterFactory;
use Override;
use Stringable;

use function sprintf;

/**
* Representation of a character currently being built.
* @method static self create(array<string, mixed> $attributes)
* @mixin Model
*/
class PartialCharacter extends Character implements Stringable
{
protected const int DEFAULT_MAX_ATTRIBUTE = 6;
protected const string PRIORITY_STANDARD = 'standard';
protected const string PRIORITY_SUM_TO_TEN = 'sum-to-ten';
protected const string PRIORITY_KARMA = 'karma';

/** @var string */
protected $connection = 'mongodb';
protected const int DEFAULT_MAX_ATTRIBUTE = 6;

/** @var string */
protected $table = 'characters-partial';
/** @var array<string, array<int, string>> */

/** @var array<int|string, array<int, string>|string> */
public array $errors = [];

protected string $priority_method;

/**
* Return the starting maximum for a character based on their metatype and
* qualities.
Expand Down Expand Up @@ -75,7 +83,7 @@ public function getStartingMaximumAttribute(string $attribute): int
*/
public function isMagicallyActive(): bool
{
return isset($this->priorities, $this->priorities['magic'])
return isset($this->priorities['magic'])
&& 'technomancer' !== $this->priorities['magic'];
}

Expand All @@ -84,7 +92,7 @@ public function isMagicallyActive(): bool
*/
public function isTechnomancer(): bool
{
return isset($this->priorities, $this->priorities['magic'])
return isset($this->priorities['magic'])
&& 'technomancer' === $this->priorities['magic'];
}

Expand All @@ -109,4 +117,187 @@ public function newFromBuilder(
// @phpstan-ignore return.type
return $character;
}

/**
* Validate the character against Shadowrun 5E's rules.
*
* Stores any errors or warnings in the errors property, similar to how
* HeroLab or Chummer import does.
*/
public function validate(): void
{
$this->errors = array_merge(
$this->errors ?? [],
$this->validatePriorities(),
$this->validateNativeLanguage(),
$this->validateAttributes(),
);
}

/**
* @return array<int, string>
*/
protected function validatePriorities(): array
{
$errors = [];
if (!isset($this->priorities['a']) && !isset($this->priorities['metatypePriority'])) {
$errors[] = 'You must choose <a href="/characters/shadowrun5e/create/priorities">priorities</a>.';
return $errors;
}
if (!isset($this->priorities['metatype'])) {
$errors[] = 'You must choose a <a href="/characters/shadowrun5e/create/priorities">metatype</a>.';
}

if (isset($this->priorities['a'])) {
$this->priority_method = self::PRIORITY_STANDARD;
if (
!isset($this->priorities['b'])
|| !isset($this->priorities['c'])
|| !isset($this->priorities['d'])
|| !isset($this->priorities['e'])
) {
$errors[] = 'You must allocate all <a href="/characters/shadowrun5e/create/priorities">priorities</a>.';
}
} else {
$this->priority_method = self::PRIORITY_SUM_TO_TEN;
$sumToTen = 10;
$priorities = [
'metatypePriority',
'magicPriority',
'attributePriority',
'skillPriority',
'resourcePriority',
];
foreach ($priorities as $priority) {
if (!isset($this->priorities[$priority])) {
$errors[] = 'You must allocate the '
. str_replace('P', ' p', $priority)
. ' on the <a href="/characters/shadowrun5e/create/priorities">'
. 'priorities page</a>.';
continue;
}
switch ($this->priorities[$priority]) {
case 'E':
// E priority is worth zero.
break;
case 'D':
--$sumToTen;
break;
case 'C':
$sumToTen -= 2;
break;
case 'B':
$sumToTen -= 3;
break;
case 'A':
$sumToTen -= 4;
break;
}
}
if ($sumToTen > 0) {
$errors[] = 'You haven\'t allocated all sum-to-ten priority points.';
} elseif ($sumToTen < 0) {
$errors[] = 'You have allocated too many sum-to-ten priority points.';
}
}
return $errors;
}

/**
* @return array<int, string>
*/
protected function validateNativeLanguage(): array
{
$nativeLanguages = 0;
/** @var KnowledgeSkill $knowledge */
foreach ($this->getKnowledgeSkills() as $knowledge) {
if ('language' !== $knowledge->category) {
continue;
}
if ('N' !== $knowledge->level) {
continue;
}
++$nativeLanguages;
}
$bilingual = false;
foreach ($this->getQualities() as $quality) {
if ('bilingual' === $quality->id) {
$bilingual = true;
break;
}
}

if ($bilingual && 2 !== $nativeLanguages) {
return ['You haven\'t chosen two native languages for your bilingual quality'];
}

if (0 === $nativeLanguages) {
return ['You must choose a native language'];
}

if (!$bilingual && 1 !== $nativeLanguages) {
return ['You can only have one native language'];
}
return [];
}

/**
* @return array<int, string>
*/
protected function validateAttributes(): array
{
$errors = [];
$attributePoints = 0;
if (self::PRIORITY_STANDARD === ($this->priority_method ?? 'unknown')) {
switch (array_search('attributes', $this->priorities ?? [], true)) {
case 'a':
$attributePoints = 24;
break;
case 'b':
$attributePoints = 20;
break;
case 'c':
$attributePoints = 16;
break;
case 'd':
$attributePoints = 14;
break;
case 'e':
$attributePoints = 12;
break;
}
} else {
if (!isset($this->priorities['attributePriority'])) {
return [];
}
switch ($this->priorities['attributePriority']) {
case 'A':
$attributePoints = 24;
break;
case 'B':
$attributePoints = 20;
break;
case 'C':
$attributePoints = 16;
break;
case 'D':
$attributePoints = 14;
break;
case 'E':
$attributePoints = 12;
break;
}
}

$attributePoints = $attributePoints - $this->body - $this->agility
- $this->reaction - $this->strength - $this->willpower
- $this->logic - $this->intuition - $this->charisma;
if (0 < $attributePoints) {
$errors[] = sprintf(
'You have %d unspent attribute points',
$attributePoints,
);
}
return $errors;
}
}
1 change: 1 addition & 0 deletions Modules/Shadowrun5e/data/armor.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
'availability' => '',
'capacity' => ,
'chummer-id' => '',
'container-type' => ['armor', 'audio', 'vision'],
'cost' => ,
'description' => '',
'effects' => [],
Expand Down
7 changes: 7 additions & 0 deletions Modules/Shadowrun5e/data/qualities.php
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,13 @@
'ruleset' => 'chrome-flesh',
'severity' => 'Cyberware',
],
'bilingual' => [
'id' => 'bilingual',
'description' => 'Bilingual description.',
'incompatible-with' => ['bilingual'],
'karma' => -5,
'name' => 'Bilingual',
],
'exceptional-attribute-body' => [
'id' => 'exceptional-attribute-body',
'attribute' => 'Body',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1484,7 +1484,6 @@ public function testLoadKnowledgeSkillsMagical(): void
public function testLoadKnowledgeSkillsResonance(): void
{
$user = User::factory()->create();

$character = PartialCharacter::factory()->create([
'priorities' => [
'magic' => 'technomancer',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public function testAddWrongTypeDoesntAdd(): void
{
try {
// @phpstan-ignore argument.type
$this->powers->offsetSet(power: new stdClass());
$this->powers->offsetSet(value: new stdClass());
} catch (TypeError) {
// Ignored
}
Expand Down
Loading
Loading