From 74788cd2a57e78fbd836e6885f046226e62b739f Mon Sep 17 00:00:00 2001
From: Jake
Date: Fri, 11 Jul 2025 00:49:42 +0200
Subject: [PATCH 01/42] support multiple subtypes for traits
---
app/Console/Commands/ConvertTraitSubtype.php | 68 +++++++++++++++++++
.../Admin/Data/FeatureController.php | 22 ++++--
app/Http/Controllers/WorldController.php | 14 ++--
app/Models/Feature/Feature.php | 34 +++++++---
app/Models/Feature/FeatureSubtype.php | 53 +++++++++++++++
app/Services/FeatureService.php | 49 +++++++++----
...25_07_10_193208_convert_trait_subtypes.php | 28 ++++++++
.../_create_edit_feature_subtype.blade.php | 4 +-
.../features/create_edit_feature.blade.php | 11 +--
.../views/admin/features/features.blade.php | 2 +-
.../views/world/_feature_entry.blade.php | 4 +-
11 files changed, 243 insertions(+), 46 deletions(-)
create mode 100644 app/Console/Commands/ConvertTraitSubtype.php
create mode 100644 app/Models/Feature/FeatureSubtype.php
create mode 100644 database/migrations/2025_07_10_193208_convert_trait_subtypes.php
diff --git a/app/Console/Commands/ConvertTraitSubtype.php b/app/Console/Commands/ConvertTraitSubtype.php
new file mode 100644
index 0000000000..c37358b8ef
--- /dev/null
+++ b/app/Console/Commands/ConvertTraitSubtype.php
@@ -0,0 +1,68 @@
+whereNotNull('subtype_id');
+ })->get();
+
+ $this->info('Converting '.count($features).' features\' subtypes...');
+ $imageBar = $this->output->createProgressBar(count($features));
+ foreach ($features as $feature) {
+ if ($feature->subtype_id) {
+ FeatureSubtype::create([
+ 'feature_id' => $feature->id,
+ 'subtype_id' => $feature->subtype_id,
+ ]);
+ }
+ $imageBar->advance();
+ }
+
+ $imageBar->finish();
+ $this->info("\n".'Dropping subtype ID column from the feature table...');
+
+ Schema::table('features', function (Blueprint $table) {
+ $table->dropColumn('subtype_id');
+ });
+
+ $this->info('Done!');
+ } else {
+ $this->info('This command will not execute, as it has already been run.');
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/Http/Controllers/Admin/Data/FeatureController.php b/app/Http/Controllers/Admin/Data/FeatureController.php
index 8c720ff6e6..9da52d4aeb 100644
--- a/app/Http/Controllers/Admin/Data/FeatureController.php
+++ b/app/Http/Controllers/Admin/Data/FeatureController.php
@@ -175,7 +175,9 @@ public function getFeatureIndex(Request $request) {
$query->where('species_id', $data['species_id']);
}
if (isset($data['subtype_id']) && $data['subtype_id'] != 'none') {
- $query->where('subtype_id', $data['subtype_id']);
+ $query->whereHas('subtypes', function ($where) use ($data) {
+ $where->where('subtype_id', $data['subtype_id']);
+ });
}
if (isset($data['name'])) {
$query->where('name', 'LIKE', '%'.$data['name'].'%');
@@ -200,7 +202,7 @@ public function getCreateFeature() {
'feature' => new Feature,
'rarities' => ['none' => 'Select a Rarity'] + Rarity::orderBy('sort', 'DESC')->pluck('name', 'id')->toArray(),
'specieses' => ['none' => 'No restriction'] + Species::orderBy('sort', 'DESC')->pluck('name', 'id')->toArray(),
- 'subtypes' => ['none' => 'No subtype'] + Subtype::orderBy('sort', 'DESC')->pluck('name', 'id')->toArray(),
+ 'subtypes' => Subtype::orderBy('sort', 'DESC')->pluck('name', 'id')->toArray(),
'categories' => ['none' => 'No category'] + FeatureCategory::orderBy('sort', 'DESC')->pluck('name', 'id')->toArray(),
]);
}
@@ -222,7 +224,7 @@ public function getEditFeature($id) {
'feature' => $feature,
'rarities' => ['none' => 'Select a Rarity'] + Rarity::orderBy('sort', 'DESC')->pluck('name', 'id')->toArray(),
'specieses' => ['none' => 'No restriction'] + Species::orderBy('sort', 'DESC')->pluck('name', 'id')->toArray(),
- 'subtypes' => ['none' => 'No subtype'] + Subtype::orderBy('sort', 'DESC')->pluck('name', 'id')->toArray(),
+ 'subtypes' => Subtype::orderBy('sort', 'DESC')->pluck('name', 'id')->toArray(),
'categories' => ['none' => 'No category'] + FeatureCategory::orderBy('sort', 'DESC')->pluck('name', 'id')->toArray(),
]);
}
@@ -238,7 +240,7 @@ public function getEditFeature($id) {
public function postCreateEditFeature(Request $request, FeatureService $service, $id = null) {
$id ? $request->validate(Feature::$updateRules) : $request->validate(Feature::$createRules);
$data = $request->only([
- 'name', 'species_id', 'subtype_id', 'rarity_id', 'feature_category_id', 'description', 'image', 'remove_image', 'is_visible',
+ 'name', 'species_id', 'subtype_ids', 'rarity_id', 'feature_category_id', 'description', 'image', 'remove_image', 'is_visible',
]);
if ($id && $service->updateFeature(Feature::find($id), $data, Auth::user())) {
flash('Trait updated successfully.')->success();
@@ -297,11 +299,17 @@ public function postDeleteFeature(Request $request, FeatureService $service, $id
*/
public function getCreateEditFeatureSubtype(Request $request) {
$species = $request->input('species');
- $subtype_id = $request->input('subtype_id');
+ $subtype_ids = $request->input('subtype_ids');
+
+ if ($subtype_ids != 'null') {
+ $subtype_ids = explode(',', $subtype_ids);
+ } else {
+ $subtype_ids = [];
+ }
return view('admin.features._create_edit_feature_subtype', [
- 'subtypes' => ['0' => 'Select Subtype'] + Subtype::where('species_id', '=', $species)->orderBy('sort', 'DESC')->pluck('name', 'id')->toArray(),
- 'subtype_id' => $subtype_id,
+ 'subtypes' => Subtype::where('species_id', '=', $species)->orderBy('sort', 'DESC')->pluck('name', 'id')->toArray(),
+ 'subtype_ids' => $subtype_ids,
]);
}
}
diff --git a/app/Http/Controllers/WorldController.php b/app/Http/Controllers/WorldController.php
index 52b47eeee4..5fa8f2c1d7 100644
--- a/app/Http/Controllers/WorldController.php
+++ b/app/Http/Controllers/WorldController.php
@@ -151,7 +151,7 @@ public function getFeatureCategories(Request $request) {
* @return \Illuminate\Contracts\Support\Renderable
*/
public function getFeatures(Request $request) {
- $query = Feature::visible(Auth::check() ? Auth::user() : null)->with('category', 'rarity', 'species', 'subtype');
+ $query = Feature::visible(Auth::check() ? Auth::user() : null)->with('category', 'rarity', 'species', 'subtypes');
$data = $request->only(['rarity_id', 'feature_category_id', 'species_id', 'subtype_id', 'name', 'sort']);
@@ -174,9 +174,13 @@ public function getFeatures(Request $request) {
}
if (isset($data['subtype_id']) && $data['subtype_id'] != 'none') {
if ($data['subtype_id'] == 'withoutOption') {
- $query->whereNull('subtype_id');
+ $query->doesntHave('subtypes');
} else {
- $query->where('subtype_id', $data['subtype_id']);
+ if (isset($data['subtype_id']) && $data['subtype_id'] != 'none') {
+ $query->whereHas('subtypes', function ($where) use ($data) {
+ $where->where('subtype_id', $data['subtype_id']);
+ });
+ }
}
}
if (isset($data['name'])) {
@@ -248,7 +252,7 @@ public function getSpeciesFeatures($id) {
$features = count($categories) ?
$species->features()
->visible(Auth::check() ? Auth::user() : null)
- ->with('rarity', 'subtype')
+ ->with('rarity', 'subtypes')
->orderByRaw('FIELD(feature_category_id,'.implode(',', $categories->pluck('id')->toArray()).')')
->orderByRaw('FIELD(rarity_id,'.implode(',', $rarities->pluck('id')->toArray()).')')
->orderBy('has_image', 'DESC')
@@ -264,7 +268,7 @@ public function getSpeciesFeatures($id) {
->groupBy(['feature_category_id', 'id']) :
$species->features()
->visible(Auth::check() ? Auth::user() : null)
- ->with('rarity', 'subtype')
+ ->with('rarity', 'subtypes')
->orderByRaw('FIELD(rarity_id,'.implode(',', $rarities->pluck('id')->toArray()).')')
->orderBy('has_image', 'DESC')
->orderBy('name')
diff --git a/app/Models/Feature/Feature.php b/app/Models/Feature/Feature.php
index f943491026..ade273f9b4 100644
--- a/app/Models/Feature/Feature.php
+++ b/app/Models/Feature/Feature.php
@@ -2,6 +2,7 @@
namespace App\Models\Feature;
+use App\Models\Feature\FeatureSubtype;
use App\Models\Model;
use App\Models\Rarity;
use App\Models\Species\Species;
@@ -15,7 +16,7 @@ class Feature extends Model {
* @var array
*/
protected $fillable = [
- 'feature_category_id', 'species_id', 'subtype_id', 'rarity_id', 'name', 'has_image', 'description', 'parsed_description', 'is_visible', 'hash',
+ 'feature_category_id', 'species_id', 'rarity_id', 'name', 'has_image', 'description', 'parsed_description', 'is_visible', 'hash',
];
/**
@@ -32,7 +33,6 @@ class Feature extends Model {
public static $createRules = [
'feature_category_id' => 'nullable',
'species_id' => 'nullable',
- 'subtype_id' => 'nullable',
'rarity_id' => 'required|exists:rarities,id',
'name' => 'required|unique:features|between:3,100',
'description' => 'nullable',
@@ -47,7 +47,6 @@ class Feature extends Model {
public static $updateRules = [
'feature_category_id' => 'nullable',
'species_id' => 'nullable',
- 'subtype_id' => 'nullable',
'rarity_id' => 'required|exists:rarities,id',
'name' => 'required|between:3,100',
'description' => 'nullable',
@@ -75,17 +74,17 @@ public function species() {
}
/**
- * Get the subtype the feature belongs to.
+ * Get the category the feature belongs to.
*/
- public function subtype() {
- return $this->belongsTo(Subtype::class);
+ public function category() {
+ return $this->belongsTo(FeatureCategory::class, 'feature_category_id');
}
/**
- * Get the category the feature belongs to.
+ * Get the subtypes of this feature.
*/
- public function category() {
- return $this->belongsTo(FeatureCategory::class, 'feature_category_id');
+ public function subtypes() {
+ return $this->hasMany(FeatureSubtype::class, 'feature_id');
}
/**********************************************************************************************
@@ -144,7 +143,7 @@ public function scopeSortSpecies($query) {
public function scopeSortSubtype($query) {
$ids = Subtype::orderBy('sort', 'DESC')->pluck('id')->toArray();
- return count($ids) ? $query->orderBy(DB::raw('FIELD(subtype_id, '.implode(',', $ids).')')) : $query;
+ return count($ids) ? $query->with('feature_subtypes')->orderBy(DB::raw('FIELD(feature_subtypes.subtype_id, '.implode(',', $ids).')')) : $query;
}
/**
@@ -296,6 +295,21 @@ public function getAdminPowerAttribute() {
**********************************************************************************************/
+ /**
+ * Displays the trait's subtypes as an imploded string.
+ */
+ public function displaySubtypes() {
+ if ($this->subtypes->isEmpty()) {
+ return 'None';
+ }
+ $subtypes = [];
+ foreach ($this->subtypes as $subtype) {
+ $subtypes[] = $subtype->subtype->displayName;
+ }
+
+ return implode(', ', $subtypes);
+ }
+
public static function getDropdownItems($withHidden = 0) {
$visibleOnly = 1;
if ($withHidden) {
diff --git a/app/Models/Feature/FeatureSubtype.php b/app/Models/Feature/FeatureSubtype.php
new file mode 100644
index 0000000000..108856a595
--- /dev/null
+++ b/app/Models/Feature/FeatureSubtype.php
@@ -0,0 +1,53 @@
+belongsTo(Feature::class, 'feature_id');
+ }
+
+ /**
+ * Get the subtype associated with this record.
+ */
+ public function subtype() {
+ return $this->belongsTo(Subtype::class, 'subtype_id');
+ }
+}
diff --git a/app/Services/FeatureService.php b/app/Services/FeatureService.php
index 76b7cbc3f8..9e165f02f6 100644
--- a/app/Services/FeatureService.php
+++ b/app/Services/FeatureService.php
@@ -4,6 +4,7 @@
use App\Models\Feature\Feature;
use App\Models\Feature\FeatureCategory;
+use App\Models\Feature\FeatureSubtype;
use App\Models\Species\Species;
use App\Models\Species\Subtype;
use Illuminate\Support\Facades\DB;
@@ -200,24 +201,26 @@ public function createFeature($data, $user) {
if (isset($data['species_id']) && $data['species_id'] == 'none') {
$data['species_id'] = null;
}
- if (isset($data['subtype_id']) && $data['subtype_id'] == 'none') {
- $data['subtype_id'] = null;
- }
-
if ((isset($data['feature_category_id']) && $data['feature_category_id']) && !FeatureCategory::where('id', $data['feature_category_id'])->exists()) {
throw new \Exception('The selected trait category is invalid.');
}
if ((isset($data['species_id']) && $data['species_id']) && !Species::where('id', $data['species_id'])->exists()) {
throw new \Exception('The selected species is invalid.');
}
- if (isset($data['subtype_id']) && $data['subtype_id']) {
- $subtype = Subtype::find($data['subtype_id']);
+ if (isset($data['subtype_ids']) && $data['subtype_ids']) {
+ $subtype = Subtype::find($data['subtype_ids']);
if (!(isset($data['species_id']) && $data['species_id'])) {
throw new \Exception('Species must be selected to select a subtype.');
}
- if (!$subtype || $subtype->species_id != $data['species_id']) {
- throw new \Exception('Selected subtype invalid or does not match species.');
+
+ foreach ($data['subtype_ids'] as $subtypeId) {
+ $subtype = Subtype::find($subtypeId);
+ if (!$subtype || $subtype->species_id != $data['species_id']) {
+ throw new \Exception('Selected subtype invalid or does not match species.');
+ }
}
+ } else {
+ $data['subtype_ids'] = null;
}
$data = $this->populateData($data);
@@ -269,9 +272,6 @@ public function updateFeature($feature, $data, $user) {
if (isset($data['species_id']) && $data['species_id'] == 'none') {
$data['species_id'] = null;
}
- if (isset($data['subtype_id']) && $data['subtype_id'] == 'none') {
- $data['subtype_id'] = null;
- }
// More specific validation
if (Feature::where('name', $data['name'])->where('id', '!=', $feature->id)->exists()) {
@@ -283,18 +283,35 @@ public function updateFeature($feature, $data, $user) {
if ((isset($data['species_id']) && $data['species_id']) && !Species::where('id', $data['species_id'])->exists()) {
throw new \Exception('The selected species is invalid.');
}
- if (isset($data['subtype_id']) && $data['subtype_id']) {
- $subtype = Subtype::find($data['subtype_id']);
+ if (isset($data['subtype_ids']) && $data['subtype_ids']) {
+ $subtype = Subtype::find($data['subtype_ids']);
if (!(isset($data['species_id']) && $data['species_id'])) {
throw new \Exception('Species must be selected to select a subtype.');
}
- if (!$subtype || $subtype->species_id != $data['species_id']) {
- throw new \Exception('Selected subtype invalid or does not match species.');
+
+ foreach ($data['subtype_ids'] as $subtypeId) {
+ $subtype = Subtype::find($subtypeId);
+ if (!$subtype || $subtype->species_id != $data['species_id']) {
+ throw new \Exception('Selected subtype invalid or does not match species.');
+ }
}
+ } else {
+ $data['subtype_ids'] = null;
}
$data = $this->populateData($data);
+ // remove old subtypes
+ $feature->subtypes()->delete();
+ if (isset($data['subtype_ids']) && $data['subtype_ids']) {
+ foreach ($data['subtype_ids'] as $subtypeId) {
+ FeatureSubtype::create([
+ 'feature_id' => $feature->id,
+ 'subtype_id' => $subtypeId,
+ ]);
+ }
+ }
+
$image = null;
if (isset($data['image']) && $data['image']) {
$data['has_image'] = 1;
@@ -342,6 +359,8 @@ public function deleteFeature($feature, $user) {
throw new \Exception('Failed to log admin action.');
}
+ $feature->subtypes->delete;
+
if ($feature->has_image) {
$this->deleteImage($feature->imagePath, $feature->imageFileName);
}
diff --git a/database/migrations/2025_07_10_193208_convert_trait_subtypes.php b/database/migrations/2025_07_10_193208_convert_trait_subtypes.php
new file mode 100644
index 0000000000..ef1df3b810
--- /dev/null
+++ b/database/migrations/2025_07_10_193208_convert_trait_subtypes.php
@@ -0,0 +1,28 @@
+integer('feature_id')->unsigned();
+ $table->integer('subtype_id')->unsigned();
+ });
+ }
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void {
+ //
+ Schema::dropIfExists('feature_subtypes');
+ }
+};
\ No newline at end of file
diff --git a/resources/views/admin/features/_create_edit_feature_subtype.blade.php b/resources/views/admin/features/_create_edit_feature_subtype.blade.php
index 2f571e001f..7edd1da881 100644
--- a/resources/views/admin/features/_create_edit_feature_subtype.blade.php
+++ b/resources/views/admin/features/_create_edit_feature_subtype.blade.php
@@ -1,2 +1,2 @@
-{!! Form::label('Subtype (Optional)') !!} {!! add_help('This is cosmetic and does not limit choice of traits in selections.') !!}
-{!! Form::select('subtype_id', $subtypes, $subtype_id, ['class' => 'form-control', 'id' => 'subtype']) !!}
+{!! Form::label('Subtypes (Optional)') !!} {!! add_help('This is cosmetic and does not limit choice of traits in selections.') !!}
+{!! Form::select('subtype_ids[]', $subtypes, $subtype_ids, ['class' => 'form-control', 'id' => 'subtype', 'multiple', 'placeholder' => 'Pick a species first.']) !!}
\ No newline at end of file
diff --git a/resources/views/admin/features/create_edit_feature.blade.php b/resources/views/admin/features/create_edit_feature.blade.php
index 847a803df3..6ca9b544a2 100644
--- a/resources/views/admin/features/create_edit_feature.blade.php
+++ b/resources/views/admin/features/create_edit_feature.blade.php
@@ -62,8 +62,8 @@
- {!! Form::label('Subtype (Optional)') !!} {!! add_help('This is cosmetic and does not limit choice of traits in selections.') !!}
- {!! Form::select('subtype_id', $subtypes, $feature->subtype_id, ['class' => 'form-control', 'id' => 'subtype']) !!}
+ {!! Form::label('Subtypes (Optional)') !!} {!! add_help('This is cosmetic and does not limit choice of traits in selections.') !!}
+ {!! Form::select('subtype_ids[]', $subtypes, $feature->subtypes, ['class' => 'form-control', 'id' => 'subtype', 'multiple', 'placeholder' => 'Pick a species first.']) !!}
@@ -110,16 +110,19 @@
function refreshSubtype() {
var species = $('#species').val();
- var subtype_id = {{ $feature->subtype_id ?: 'null' }};
+ var subtype_ids = "{{ !$feature->subtypes->isEmpty() ? implode(',', $feature->subtypes->pluck('subtype_id')->toArray()) : 'null' }}";
$.ajax({
type: "GET",
- url: "{{ url('admin/data/traits/check-subtype') }}?species=" + species + "&subtype_id=" + subtype_id,
+ url: "{{ url('admin/data/traits/check-subtype') }}?species=" + species + "&subtype_ids=" + subtype_ids,
dataType: "text"
}).done(function(res) {
$("#subtypes").html(res);
+ $("#subtype").selectize();
}).fail(function(jqXHR, textStatus, errorThrown) {
alert("AJAX call failed: " + textStatus + ", " + errorThrown);
});
};
+
+ $('#subtype').selectize();
@endsection
diff --git a/resources/views/admin/features/features.blade.php b/resources/views/admin/features/features.blade.php
index a3db2168bf..58ad97fc30 100644
--- a/resources/views/admin/features/features.blade.php
+++ b/resources/views/admin/features/features.blade.php
@@ -85,7 +85,7 @@
{{ $feature->species ? $feature->species->name : '---' }}
-
{{ $feature->subtype ? $feature->subtype->name : '---' }}
+
{!! $feature->displaySubtypes() !!}
diff --git a/resources/views/world/_feature_entry.blade.php b/resources/views/world/_feature_entry.blade.php
index 71eee4909a..069f442d01 100644
--- a/resources/views/world/_feature_entry.blade.php
+++ b/resources/views/world/_feature_entry.blade.php
@@ -25,8 +25,8 @@
@if ($feature->species_id)
Species: {!! $feature->species->displayName !!}
- @if ($feature->subtype_id)
- ({!! $feature->subtype->displayName !!} subtype)
+ @if (!$feature->subtypes->isEmpty())
+ ({!! $feature->displaySubtypes() !!})
@endif
@endif
From 29c6b29776b242a85446690d517919fb0b01c875 Mon Sep 17 00:00:00 2001
From: Jake
Date: Fri, 11 Jul 2025 07:41:15 +0200
Subject: [PATCH 02/42] rename whereHas $query variable
---
app/Http/Controllers/Admin/Data/FeatureController.php | 4 ++--
app/Http/Controllers/WorldController.php | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/app/Http/Controllers/Admin/Data/FeatureController.php b/app/Http/Controllers/Admin/Data/FeatureController.php
index 9da52d4aeb..d9c99ed418 100644
--- a/app/Http/Controllers/Admin/Data/FeatureController.php
+++ b/app/Http/Controllers/Admin/Data/FeatureController.php
@@ -175,8 +175,8 @@ public function getFeatureIndex(Request $request) {
$query->where('species_id', $data['species_id']);
}
if (isset($data['subtype_id']) && $data['subtype_id'] != 'none') {
- $query->whereHas('subtypes', function ($where) use ($data) {
- $where->where('subtype_id', $data['subtype_id']);
+ $query->whereHas('subtypes', function ($query) use ($data) {
+ $query->where('subtype_id', $data['subtype_id']);
});
}
if (isset($data['name'])) {
diff --git a/app/Http/Controllers/WorldController.php b/app/Http/Controllers/WorldController.php
index 5fa8f2c1d7..fa8a5733f9 100644
--- a/app/Http/Controllers/WorldController.php
+++ b/app/Http/Controllers/WorldController.php
@@ -177,8 +177,8 @@ public function getFeatures(Request $request) {
$query->doesntHave('subtypes');
} else {
if (isset($data['subtype_id']) && $data['subtype_id'] != 'none') {
- $query->whereHas('subtypes', function ($where) use ($data) {
- $where->where('subtype_id', $data['subtype_id']);
+ $query->whereHas('subtypes', function ($query) use ($data) {
+ $query->where('subtype_id', $data['subtype_id']);
});
}
}
From 55fddafc84fe59291c85450d8d9498d01b135f9f Mon Sep 17 00:00:00 2001
From: Jake
Date: Fri, 11 Jul 2025 08:06:03 +0200
Subject: [PATCH 03/42] refactor getSubtypeFeatures to support multiple
subtypes
---
app/Http/Controllers/WorldController.php | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/app/Http/Controllers/WorldController.php b/app/Http/Controllers/WorldController.php
index b442da3e57..316a0805ae 100644
--- a/app/Http/Controllers/WorldController.php
+++ b/app/Http/Controllers/WorldController.php
@@ -419,7 +419,9 @@ public function getSubtypeFeatures($id, Request $request) {
} else {
$features = $features
->filter(function ($feature) use ($subtype) {
- return !($feature->subtype && $feature->subtype->id != $subtype->id);
+ return !($feature->subtypes && !$feature::whereHas('subtypes', function ($query) use ($subtype) {
+ $query->where('subtype_id', $subtype->id);
+ }));
})
->groupBy(['feature_category_id', 'id']);
}
From f8ac8e13bda7ac565407fae0c48da62a456b42d5 Mon Sep 17 00:00:00 2001
From: Draconizations
Date: Fri, 11 Jul 2025 06:08:00 +0000
Subject: [PATCH 04/42] refactor: fix blade formatting
---
.../views/admin/features/_create_edit_feature_subtype.blade.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/resources/views/admin/features/_create_edit_feature_subtype.blade.php b/resources/views/admin/features/_create_edit_feature_subtype.blade.php
index 7edd1da881..c69f35f7d6 100644
--- a/resources/views/admin/features/_create_edit_feature_subtype.blade.php
+++ b/resources/views/admin/features/_create_edit_feature_subtype.blade.php
@@ -1,2 +1,2 @@
{!! Form::label('Subtypes (Optional)') !!} {!! add_help('This is cosmetic and does not limit choice of traits in selections.') !!}
-{!! Form::select('subtype_ids[]', $subtypes, $subtype_ids, ['class' => 'form-control', 'id' => 'subtype', 'multiple', 'placeholder' => 'Pick a species first.']) !!}
\ No newline at end of file
+{!! Form::select('subtype_ids[]', $subtypes, $subtype_ids, ['class' => 'form-control', 'id' => 'subtype', 'multiple', 'placeholder' => 'Pick a species first.']) !!}
From d2905fa9cbcea8dce3252312651121fcdbae8140 Mon Sep 17 00:00:00 2001
From: Draconizations
Date: Fri, 11 Jul 2025 06:10:40 +0000
Subject: [PATCH 05/42] refactor: fix PHP styling
---
app/Console/Commands/ConvertTraitSubtype.php | 2 +-
app/Http/Controllers/Admin/Data/FeatureController.php | 2 +-
app/Models/Feature/Feature.php | 5 ++---
app/Models/Feature/FeatureSubtype.php | 6 ++----
app/Services/FeatureService.php | 6 +++---
.../migrations/2025_07_10_193208_convert_trait_subtypes.php | 2 +-
6 files changed, 10 insertions(+), 13 deletions(-)
diff --git a/app/Console/Commands/ConvertTraitSubtype.php b/app/Console/Commands/ConvertTraitSubtype.php
index c37358b8ef..91ae254e84 100644
--- a/app/Console/Commands/ConvertTraitSubtype.php
+++ b/app/Console/Commands/ConvertTraitSubtype.php
@@ -65,4 +65,4 @@ public function handle() {
$this->info('This command will not execute, as it has already been run.');
}
}
-}
\ No newline at end of file
+}
diff --git a/app/Http/Controllers/Admin/Data/FeatureController.php b/app/Http/Controllers/Admin/Data/FeatureController.php
index ff340ab772..be796ed7ea 100644
--- a/app/Http/Controllers/Admin/Data/FeatureController.php
+++ b/app/Http/Controllers/Admin/Data/FeatureController.php
@@ -361,7 +361,7 @@ public function getCreateEditFeatureSubtype(Request $request) {
}
return view('admin.features._create_edit_feature_subtype', [
- 'subtypes' => Subtype::where('species_id', '=', $species)->orderBy('sort', 'DESC')->pluck('name', 'id')->toArray(),
+ 'subtypes' => Subtype::where('species_id', '=', $species)->orderBy('sort', 'DESC')->pluck('name', 'id')->toArray(),
'subtype_ids' => $subtype_ids,
]);
}
diff --git a/app/Models/Feature/Feature.php b/app/Models/Feature/Feature.php
index 5694d96896..fbdfba1025 100644
--- a/app/Models/Feature/Feature.php
+++ b/app/Models/Feature/Feature.php
@@ -2,7 +2,6 @@
namespace App\Models\Feature;
-use App\Models\Feature\FeatureSubtype;
use App\Models\Model;
use App\Models\Rarity;
use App\Models\Species\Species;
@@ -285,7 +284,7 @@ public function getAdminPowerAttribute() {
**********************************************************************************************/
- /**
+ /**
* Displays the trait's subtypes as an imploded string.
*/
public function displaySubtypes() {
@@ -299,7 +298,7 @@ public function displaySubtypes() {
return implode(', ', $subtypes);
}
-
+
public static function getDropdownItems($withHidden = 0) {
$visibleOnly = 1;
if ($withHidden) {
diff --git a/app/Models/Feature/FeatureSubtype.php b/app/Models/Feature/FeatureSubtype.php
index 108856a595..2642f88e9c 100644
--- a/app/Models/Feature/FeatureSubtype.php
+++ b/app/Models/Feature/FeatureSubtype.php
@@ -2,19 +2,17 @@
namespace App\Models\Feature;
-use App\Models\Feature\Feature;
use App\Models\Model;
use App\Models\Species\Subtype;
-class FeatureSubtype extends Model
-{
+class FeatureSubtype extends Model {
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
- 'feature_id', 'subtype_id'
+ 'feature_id', 'subtype_id',
];
/**
diff --git a/app/Services/FeatureService.php b/app/Services/FeatureService.php
index 633ed4a5c2..873c282d91 100644
--- a/app/Services/FeatureService.php
+++ b/app/Services/FeatureService.php
@@ -212,7 +212,7 @@ public function createFeature($data, $user) {
if (!(isset($data['species_id']) && $data['species_id'])) {
throw new \Exception('Species must be selected to select a subtype.');
}
-
+
foreach ($data['subtype_ids'] as $subtypeId) {
$subtype = Subtype::find($subtypeId);
if (!$subtype || $subtype->species_id != $data['species_id']) {
@@ -288,7 +288,7 @@ public function updateFeature($feature, $data, $user) {
if (!(isset($data['species_id']) && $data['species_id'])) {
throw new \Exception('Species must be selected to select a subtype.');
}
-
+
foreach ($data['subtype_ids'] as $subtypeId) {
$subtype = Subtype::find($subtypeId);
if (!$subtype || $subtype->species_id != $data['species_id']) {
@@ -306,7 +306,7 @@ public function updateFeature($feature, $data, $user) {
if (isset($data['subtype_ids']) && $data['subtype_ids']) {
foreach ($data['subtype_ids'] as $subtypeId) {
FeatureSubtype::create([
- 'feature_id' => $feature->id,
+ 'feature_id' => $feature->id,
'subtype_id' => $subtypeId,
]);
}
diff --git a/database/migrations/2025_07_10_193208_convert_trait_subtypes.php b/database/migrations/2025_07_10_193208_convert_trait_subtypes.php
index ef1df3b810..5c3188dc47 100644
--- a/database/migrations/2025_07_10_193208_convert_trait_subtypes.php
+++ b/database/migrations/2025_07_10_193208_convert_trait_subtypes.php
@@ -25,4 +25,4 @@ public function down(): void {
//
Schema::dropIfExists('feature_subtypes');
}
-};
\ No newline at end of file
+};
From adae854b3d84b42989c6302d1e38890ceec0830a Mon Sep 17 00:00:00 2001
From: Jake
Date: Fri, 11 Jul 2025 11:48:03 +0200
Subject: [PATCH 06/42] change model relation to belongsToMany
---
app/Models/Feature/Feature.php | 5 ++---
app/Models/Species/Subtype.php | 8 ++++++++
app/Services/FeatureService.php | 9 +++------
.../views/admin/features/create_edit_feature.blade.php | 2 +-
4 files changed, 14 insertions(+), 10 deletions(-)
diff --git a/app/Models/Feature/Feature.php b/app/Models/Feature/Feature.php
index ade273f9b4..d0286f64f7 100644
--- a/app/Models/Feature/Feature.php
+++ b/app/Models/Feature/Feature.php
@@ -2,7 +2,6 @@
namespace App\Models\Feature;
-use App\Models\Feature\FeatureSubtype;
use App\Models\Model;
use App\Models\Rarity;
use App\Models\Species\Species;
@@ -84,7 +83,7 @@ public function category() {
* Get the subtypes of this feature.
*/
public function subtypes() {
- return $this->hasMany(FeatureSubtype::class, 'feature_id');
+ return $this->belongsToMany(Subtype::class, 'feature_subtypes');
}
/**********************************************************************************************
@@ -304,7 +303,7 @@ public function displaySubtypes() {
}
$subtypes = [];
foreach ($this->subtypes as $subtype) {
- $subtypes[] = $subtype->subtype->displayName;
+ $subtypes[] = $subtype->displayName;
}
return implode(', ', $subtypes);
diff --git a/app/Models/Species/Subtype.php b/app/Models/Species/Subtype.php
index a207153f31..4305e530f5 100644
--- a/app/Models/Species/Subtype.php
+++ b/app/Models/Species/Subtype.php
@@ -3,6 +3,7 @@
namespace App\Models\Species;
use App\Models\Model;
+use App\Models\Feature\Feature;
class Subtype extends Model {
/**
@@ -67,6 +68,13 @@ public function species() {
return $this->belongsTo(Species::class, 'species_id');
}
+ /**
+ * Get the features associated with this subtype.
+ */
+ public function features() {
+ return $this->belongsToMany(Feature::class, 'feature_subtypes');
+ }
+
/**********************************************************************************************
SCOPES
diff --git a/app/Services/FeatureService.php b/app/Services/FeatureService.php
index 9e165f02f6..2a349392ce 100644
--- a/app/Services/FeatureService.php
+++ b/app/Services/FeatureService.php
@@ -302,13 +302,10 @@ public function updateFeature($feature, $data, $user) {
$data = $this->populateData($data);
// remove old subtypes
- $feature->subtypes()->delete();
+ $feature->subtypes()->detach();
if (isset($data['subtype_ids']) && $data['subtype_ids']) {
foreach ($data['subtype_ids'] as $subtypeId) {
- FeatureSubtype::create([
- 'feature_id' => $feature->id,
- 'subtype_id' => $subtypeId,
- ]);
+ $feature->subtypes()->attach($subtypeId);
}
}
@@ -359,7 +356,7 @@ public function deleteFeature($feature, $user) {
throw new \Exception('Failed to log admin action.');
}
- $feature->subtypes->delete;
+ $feature->subtypes->detach();
if ($feature->has_image) {
$this->deleteImage($feature->imagePath, $feature->imageFileName);
diff --git a/resources/views/admin/features/create_edit_feature.blade.php b/resources/views/admin/features/create_edit_feature.blade.php
index 6ca9b544a2..16a59d09e2 100644
--- a/resources/views/admin/features/create_edit_feature.blade.php
+++ b/resources/views/admin/features/create_edit_feature.blade.php
@@ -110,7 +110,7 @@
function refreshSubtype() {
var species = $('#species').val();
- var subtype_ids = "{{ !$feature->subtypes->isEmpty() ? implode(',', $feature->subtypes->pluck('subtype_id')->toArray()) : 'null' }}";
+ var subtype_ids = "{{ !$feature->subtypes->isEmpty() ? implode(',', $feature->subtypes->pluck('id')->toArray()) : 'null' }}";
$.ajax({
type: "GET",
url: "{{ url('admin/data/traits/check-subtype') }}?species=" + species + "&subtype_ids=" + subtype_ids,
From 04bf27d67d6a25527b9cb291368cd4164f5a82be Mon Sep 17 00:00:00 2001
From: Jake
Date: Fri, 11 Jul 2025 11:49:04 +0200
Subject: [PATCH 07/42] add extension tracker file
---
.../lorekeeper/ext-tracker/multiple_trait_subtypes.php | 9 +++++++++
1 file changed, 9 insertions(+)
create mode 100644 config/lorekeeper/ext-tracker/multiple_trait_subtypes.php
diff --git a/config/lorekeeper/ext-tracker/multiple_trait_subtypes.php b/config/lorekeeper/ext-tracker/multiple_trait_subtypes.php
new file mode 100644
index 0000000000..9d0d278e6d
--- /dev/null
+++ b/config/lorekeeper/ext-tracker/multiple_trait_subtypes.php
@@ -0,0 +1,9 @@
+ 'Multiple_Trait_Subtypes',
+ 'creators' => json_encode([
+ 'Uri' => 'https://github.com/Draconizations/',
+ ]),
+ 'version' => '1.0.0',
+];
From 9b7f3ede8f28744fc7d0d998183e24679b2f83bb Mon Sep 17 00:00:00 2001
From: Draconizations
Date: Fri, 11 Jul 2025 09:49:46 +0000
Subject: [PATCH 08/42] refactor: fix blade formatting
---
.../views/admin/features/_create_edit_feature_subtype.blade.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/resources/views/admin/features/_create_edit_feature_subtype.blade.php b/resources/views/admin/features/_create_edit_feature_subtype.blade.php
index 7edd1da881..c69f35f7d6 100644
--- a/resources/views/admin/features/_create_edit_feature_subtype.blade.php
+++ b/resources/views/admin/features/_create_edit_feature_subtype.blade.php
@@ -1,2 +1,2 @@
{!! Form::label('Subtypes (Optional)') !!} {!! add_help('This is cosmetic and does not limit choice of traits in selections.') !!}
-{!! Form::select('subtype_ids[]', $subtypes, $subtype_ids, ['class' => 'form-control', 'id' => 'subtype', 'multiple', 'placeholder' => 'Pick a species first.']) !!}
\ No newline at end of file
+{!! Form::select('subtype_ids[]', $subtypes, $subtype_ids, ['class' => 'form-control', 'id' => 'subtype', 'multiple', 'placeholder' => 'Pick a species first.']) !!}
From 1a25d7a025ed085ce7bad3065fa8566a70c6981c Mon Sep 17 00:00:00 2001
From: Jake
Date: Fri, 11 Jul 2025 11:51:20 +0200
Subject: [PATCH 09/42] small fixes
---
app/Models/Species/Subtype.php | 1 -
app/Services/FeatureService.php | 1 -
2 files changed, 2 deletions(-)
diff --git a/app/Models/Species/Subtype.php b/app/Models/Species/Subtype.php
index aa5ea631a6..e79dfbdd6b 100644
--- a/app/Models/Species/Subtype.php
+++ b/app/Models/Species/Subtype.php
@@ -4,7 +4,6 @@
use App\Models\Feature\Feature;
use App\Models\Model;
-use App\Models\Feature\Feature;
class Subtype extends Model {
/**
diff --git a/app/Services/FeatureService.php b/app/Services/FeatureService.php
index 615aeba1ec..ff068eaf3b 100644
--- a/app/Services/FeatureService.php
+++ b/app/Services/FeatureService.php
@@ -4,7 +4,6 @@
use App\Models\Feature\Feature;
use App\Models\Feature\FeatureCategory;
-use App\Models\Feature\FeatureSubtype;
use App\Models\Species\Species;
use App\Models\Species\Subtype;
use Illuminate\Support\Facades\DB;
From 7fba964560826bc69bea977b85ccdfb17b2560a4 Mon Sep 17 00:00:00 2001
From: Draconizations
Date: Fri, 11 Jul 2025 09:52:11 +0000
Subject: [PATCH 10/42] refactor: fix PHP styling
---
app/Console/Commands/ConvertTraitSubtype.php | 2 +-
app/Http/Controllers/Admin/Data/FeatureController.php | 2 +-
app/Models/Feature/Feature.php | 4 ++--
app/Models/Feature/FeatureSubtype.php | 6 ++----
app/Models/Species/Subtype.php | 2 +-
app/Services/FeatureService.php | 5 ++---
.../migrations/2025_07_10_193208_convert_trait_subtypes.php | 2 +-
7 files changed, 10 insertions(+), 13 deletions(-)
diff --git a/app/Console/Commands/ConvertTraitSubtype.php b/app/Console/Commands/ConvertTraitSubtype.php
index c37358b8ef..91ae254e84 100644
--- a/app/Console/Commands/ConvertTraitSubtype.php
+++ b/app/Console/Commands/ConvertTraitSubtype.php
@@ -65,4 +65,4 @@ public function handle() {
$this->info('This command will not execute, as it has already been run.');
}
}
-}
\ No newline at end of file
+}
diff --git a/app/Http/Controllers/Admin/Data/FeatureController.php b/app/Http/Controllers/Admin/Data/FeatureController.php
index d9c99ed418..75c67d898d 100644
--- a/app/Http/Controllers/Admin/Data/FeatureController.php
+++ b/app/Http/Controllers/Admin/Data/FeatureController.php
@@ -308,7 +308,7 @@ public function getCreateEditFeatureSubtype(Request $request) {
}
return view('admin.features._create_edit_feature_subtype', [
- 'subtypes' => Subtype::where('species_id', '=', $species)->orderBy('sort', 'DESC')->pluck('name', 'id')->toArray(),
+ 'subtypes' => Subtype::where('species_id', '=', $species)->orderBy('sort', 'DESC')->pluck('name', 'id')->toArray(),
'subtype_ids' => $subtype_ids,
]);
}
diff --git a/app/Models/Feature/Feature.php b/app/Models/Feature/Feature.php
index d0286f64f7..b4ee418f42 100644
--- a/app/Models/Feature/Feature.php
+++ b/app/Models/Feature/Feature.php
@@ -294,7 +294,7 @@ public function getAdminPowerAttribute() {
**********************************************************************************************/
- /**
+ /**
* Displays the trait's subtypes as an imploded string.
*/
public function displaySubtypes() {
@@ -308,7 +308,7 @@ public function displaySubtypes() {
return implode(', ', $subtypes);
}
-
+
public static function getDropdownItems($withHidden = 0) {
$visibleOnly = 1;
if ($withHidden) {
diff --git a/app/Models/Feature/FeatureSubtype.php b/app/Models/Feature/FeatureSubtype.php
index 108856a595..2642f88e9c 100644
--- a/app/Models/Feature/FeatureSubtype.php
+++ b/app/Models/Feature/FeatureSubtype.php
@@ -2,19 +2,17 @@
namespace App\Models\Feature;
-use App\Models\Feature\Feature;
use App\Models\Model;
use App\Models\Species\Subtype;
-class FeatureSubtype extends Model
-{
+class FeatureSubtype extends Model {
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
- 'feature_id', 'subtype_id'
+ 'feature_id', 'subtype_id',
];
/**
diff --git a/app/Models/Species/Subtype.php b/app/Models/Species/Subtype.php
index 4305e530f5..0e8eda806e 100644
--- a/app/Models/Species/Subtype.php
+++ b/app/Models/Species/Subtype.php
@@ -2,8 +2,8 @@
namespace App\Models\Species;
-use App\Models\Model;
use App\Models\Feature\Feature;
+use App\Models\Model;
class Subtype extends Model {
/**
diff --git a/app/Services/FeatureService.php b/app/Services/FeatureService.php
index 2a349392ce..1f6cc67024 100644
--- a/app/Services/FeatureService.php
+++ b/app/Services/FeatureService.php
@@ -4,7 +4,6 @@
use App\Models\Feature\Feature;
use App\Models\Feature\FeatureCategory;
-use App\Models\Feature\FeatureSubtype;
use App\Models\Species\Species;
use App\Models\Species\Subtype;
use Illuminate\Support\Facades\DB;
@@ -212,7 +211,7 @@ public function createFeature($data, $user) {
if (!(isset($data['species_id']) && $data['species_id'])) {
throw new \Exception('Species must be selected to select a subtype.');
}
-
+
foreach ($data['subtype_ids'] as $subtypeId) {
$subtype = Subtype::find($subtypeId);
if (!$subtype || $subtype->species_id != $data['species_id']) {
@@ -288,7 +287,7 @@ public function updateFeature($feature, $data, $user) {
if (!(isset($data['species_id']) && $data['species_id'])) {
throw new \Exception('Species must be selected to select a subtype.');
}
-
+
foreach ($data['subtype_ids'] as $subtypeId) {
$subtype = Subtype::find($subtypeId);
if (!$subtype || $subtype->species_id != $data['species_id']) {
diff --git a/database/migrations/2025_07_10_193208_convert_trait_subtypes.php b/database/migrations/2025_07_10_193208_convert_trait_subtypes.php
index ef1df3b810..5c3188dc47 100644
--- a/database/migrations/2025_07_10_193208_convert_trait_subtypes.php
+++ b/database/migrations/2025_07_10_193208_convert_trait_subtypes.php
@@ -25,4 +25,4 @@ public function down(): void {
//
Schema::dropIfExists('feature_subtypes');
}
-};
\ No newline at end of file
+};
From 3196e2d1b961d3c878af4bca36731d4902280ee0 Mon Sep 17 00:00:00 2001
From: Jake
Date: Fri, 11 Jul 2025 12:19:08 +0200
Subject: [PATCH 11/42] fix: show subtypes again
---
resources/views/world/species_features.blade.php | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/resources/views/world/species_features.blade.php b/resources/views/world/species_features.blade.php
index 3d05723f02..94c3c3421d 100644
--- a/resources/views/world/species_features.blade.php
+++ b/resources/views/world/species_features.blade.php
@@ -34,8 +34,8 @@
@endif
{!! $feature->first()->displayName !!}
- @if ($feature->first()->subtype)
-
({!! $feature->first()->subtype->displayName !!} Subtype)
+ @if (count($feature->first()->subtypes))
+
(Subtype{{ count($feature->first()->subtypes) > 1 ? 's' : "" }}: {!! $feature->first()->displaySubtypes() !!})
@endif
From 8be4fbd63deb47476b42e289ccf8d0b9d6c6b38f Mon Sep 17 00:00:00 2001
From: Draconizations
Date: Fri, 11 Jul 2025 10:19:51 +0000
Subject: [PATCH 12/42] refactor: fix blade formatting
---
resources/views/world/species_features.blade.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/resources/views/world/species_features.blade.php b/resources/views/world/species_features.blade.php
index 94c3c3421d..16dad6bc96 100644
--- a/resources/views/world/species_features.blade.php
+++ b/resources/views/world/species_features.blade.php
@@ -35,7 +35,7 @@
@endif
{!! $feature->first()->displayName !!}
@if (count($feature->first()->subtypes))
-
(Subtype{{ count($feature->first()->subtypes) > 1 ? 's' : "" }}: {!! $feature->first()->displaySubtypes() !!})
+
(Subtype{{ count($feature->first()->subtypes) > 1 ? 's' : '' }}: {!! $feature->first()->displaySubtypes() !!})
@endif
From 19942b0df12902c8c6756ee9aebda29497da1088 Mon Sep 17 00:00:00 2001
From: Jake
Date: Fri, 11 Jul 2025 12:24:32 +0200
Subject: [PATCH 13/42] oops
---
resources/views/world/_features_index.blade.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/resources/views/world/_features_index.blade.php b/resources/views/world/_features_index.blade.php
index 948908dc56..165bf112dc 100644
--- a/resources/views/world/_features_index.blade.php
+++ b/resources/views/world/_features_index.blade.php
@@ -21,7 +21,7 @@
@if (!$feature->first()->is_visible)
@endif
- {!! $feature->first()-> !!}
+ {!! $feature->first()->displayName !!}
@if ($showSubtype && count($feature->first()->subtypes))
(Subtype{{ count($feature->first()->subtypes) > 1 ? 's' : '' }}: {!! $feature->first()->displaySubtypes() !!})
@endif
From 59fd71c82884b9529083564abbd98a65fb1e00bd Mon Sep 17 00:00:00 2001
From: Jake
Date: Fri, 11 Jul 2025 12:37:37 +0200
Subject: [PATCH 14/42] fix: properly attach subtypes on feature creation
---
app/Services/FeatureService.php | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/app/Services/FeatureService.php b/app/Services/FeatureService.php
index 1f6cc67024..51a036e0d0 100644
--- a/app/Services/FeatureService.php
+++ b/app/Services/FeatureService.php
@@ -236,6 +236,10 @@ public function createFeature($data, $user) {
$feature = Feature::create($data);
+ foreach ($data['subtype_ids'] as $subtypeId) {
+ $feature->subtypes()->attach($subtypeId);
+ }
+
if (!$this->logAdminAction($user, 'Created Feature', 'Created '.$feature->displayName)) {
throw new \Exception('Failed to log admin action.');
}
From 8c04b2e3e2226ef59adf0e300a427ee114a265a4 Mon Sep 17 00:00:00 2001
From: Jake
Date: Fri, 11 Jul 2025 12:46:04 +0200
Subject: [PATCH 15/42] fix: properly allow setting 0 subtypes
---
app/Services/FeatureService.php | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/app/Services/FeatureService.php b/app/Services/FeatureService.php
index 51a036e0d0..cd8beea1e7 100644
--- a/app/Services/FeatureService.php
+++ b/app/Services/FeatureService.php
@@ -219,7 +219,7 @@ public function createFeature($data, $user) {
}
}
} else {
- $data['subtype_ids'] = null;
+ $data['subtype_ids'] = [];
}
$data = $this->populateData($data);
@@ -299,7 +299,7 @@ public function updateFeature($feature, $data, $user) {
}
}
} else {
- $data['subtype_ids'] = null;
+ $data['subtype_ids'] = [];
}
$data = $this->populateData($data);
From 9db392cea1a4896ec4bfe7c165a8565c96bb4b8f Mon Sep 17 00:00:00 2001
From: Jake
Date: Fri, 11 Jul 2025 16:15:00 +0200
Subject: [PATCH 16/42] fix: only include traits that include that subtype when
viewing basics
---
app/Http/Controllers/WorldController.php | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/app/Http/Controllers/WorldController.php b/app/Http/Controllers/WorldController.php
index 316a0805ae..c5d291b005 100644
--- a/app/Http/Controllers/WorldController.php
+++ b/app/Http/Controllers/WorldController.php
@@ -419,9 +419,7 @@ public function getSubtypeFeatures($id, Request $request) {
} else {
$features = $features
->filter(function ($feature) use ($subtype) {
- return !($feature->subtypes && !$feature::whereHas('subtypes', function ($query) use ($subtype) {
- $query->where('subtype_id', $subtype->id);
- }));
+ return ($feature->subtypes->isEmpty() || !$feature->subtypes->where('id', $subtype->id)->isEmpty());
})
->groupBy(['feature_category_id', 'id']);
}
From ce32444f10b09b25e025a5b43b37ae5c16fb3764 Mon Sep 17 00:00:00 2001
From: Draconizations
Date: Fri, 11 Jul 2025 14:18:13 +0000
Subject: [PATCH 17/42] refactor: fix PHP styling
---
app/Http/Controllers/WorldController.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/Http/Controllers/WorldController.php b/app/Http/Controllers/WorldController.php
index c5d291b005..fb75afc616 100644
--- a/app/Http/Controllers/WorldController.php
+++ b/app/Http/Controllers/WorldController.php
@@ -419,7 +419,7 @@ public function getSubtypeFeatures($id, Request $request) {
} else {
$features = $features
->filter(function ($feature) use ($subtype) {
- return ($feature->subtypes->isEmpty() || !$feature->subtypes->where('id', $subtype->id)->isEmpty());
+ return $feature->subtypes->isEmpty() || !$feature->subtypes->where('id', $subtype->id)->isEmpty();
})
->groupBy(['feature_category_id', 'id']);
}
From 3a04560d9cb8bb98ff1ae9d983b2cc2ee59b7682 Mon Sep 17 00:00:00 2001
From: Jake
Date: Fri, 11 Jul 2025 16:28:43 +0200
Subject: [PATCH 18/42] fix: more subtype -> subtypes fixes
---
app/Http/Controllers/WorldController.php | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/app/Http/Controllers/WorldController.php b/app/Http/Controllers/WorldController.php
index fa8a5733f9..cf3a913c45 100644
--- a/app/Http/Controllers/WorldController.php
+++ b/app/Http/Controllers/WorldController.php
@@ -259,8 +259,8 @@ public function getSpeciesFeatures($id) {
->orderBy('name')
->get()
->filter(function ($feature) {
- if ($feature->subtype) {
- return $feature->subtype->is_visible;
+ if (!$feature->subtypes->isEmpty()) {
+ return !$feature->subtypes->where('is_visible', true)->isEmpty();
}
return true;
@@ -274,8 +274,8 @@ public function getSpeciesFeatures($id) {
->orderBy('name')
->get()
->filter(function ($feature) {
- if ($feature->subtype) {
- return $feature->subtype->is_visible;
+ if (!$feature->subtypes->isEmpty()) {
+ return !$feature->subtypes->where('is_visible', true)->isEmpty();
}
return true;
@@ -299,7 +299,7 @@ public function getSpeciesFeatures($id) {
* @return \Illuminate\Contracts\Support\Renderable
*/
public function getSpeciesFeatureDetail($speciesId, $id) {
- $feature = Feature::where('id', $id)->with('species', 'subtype', 'rarity')->first();
+ $feature = Feature::where('id', $id)->with('species', 'subtypes', 'rarity')->first();
if (!$feature) {
abort(404);
From 4dada008a72994e1e949791956370946a970f715 Mon Sep 17 00:00:00 2001
From: Jake
Date: Fri, 11 Jul 2025 16:38:08 +0200
Subject: [PATCH 19/42] only show visible subtypes to user
---
app/Models/Feature/Feature.php | 6 +++---
resources/views/admin/features/features.blade.php | 2 +-
resources/views/world/_feature_entry.blade.php | 2 +-
resources/views/world/species_features.blade.php | 2 +-
4 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/app/Models/Feature/Feature.php b/app/Models/Feature/Feature.php
index b4ee418f42..cf1dd84b5c 100644
--- a/app/Models/Feature/Feature.php
+++ b/app/Models/Feature/Feature.php
@@ -297,12 +297,12 @@ public function getAdminPowerAttribute() {
/**
* Displays the trait's subtypes as an imploded string.
*/
- public function displaySubtypes() {
- if ($this->subtypes->isEmpty()) {
+ public function displaySubtypes($user = null) {
+ if (!count($this->subtypes()->visible($user)->get())) {
return 'None';
}
$subtypes = [];
- foreach ($this->subtypes as $subtype) {
+ foreach ($this->subtypes()->visible($user)->get() as $subtype) {
$subtypes[] = $subtype->displayName;
}
diff --git a/resources/views/admin/features/features.blade.php b/resources/views/admin/features/features.blade.php
index 58ad97fc30..8b1debf9e0 100644
--- a/resources/views/admin/features/features.blade.php
+++ b/resources/views/admin/features/features.blade.php
@@ -85,7 +85,7 @@
{{ $feature->species ? $feature->species->name : '---' }}
-
{!! $feature->displaySubtypes() !!}
+
{!! $feature->displaySubtypes(Auth::User() ?? null) !!}
diff --git a/resources/views/world/_feature_entry.blade.php b/resources/views/world/_feature_entry.blade.php
index 069f442d01..4823462587 100644
--- a/resources/views/world/_feature_entry.blade.php
+++ b/resources/views/world/_feature_entry.blade.php
@@ -26,7 +26,7 @@
Species: {!! $feature->species->displayName !!}
@if (!$feature->subtypes->isEmpty())
- ({!! $feature->displaySubtypes() !!})
+ ({!! $feature->displaySubtypes(Auth::User() ?? null) !!})
@endif
@endif
diff --git a/resources/views/world/species_features.blade.php b/resources/views/world/species_features.blade.php
index 16dad6bc96..cd8f29fffb 100644
--- a/resources/views/world/species_features.blade.php
+++ b/resources/views/world/species_features.blade.php
@@ -35,7 +35,7 @@
@endif
{!! $feature->first()->displayName !!}
@if (count($feature->first()->subtypes))
-
(Subtype{{ count($feature->first()->subtypes) > 1 ? 's' : '' }}: {!! $feature->first()->displaySubtypes() !!})
+
(Subtype{{ count($feature->first()->subtypes) > 1 ? 's' : '' }}: {!! $feature->first()->displaySubtypes(Auth::User() ?? null) !!})
@endif
From ba0e932aff5ab9ebb825b3b72175c42293aebfa0 Mon Sep 17 00:00:00 2001
From: Draconizations
Date: Fri, 11 Jul 2025 14:40:37 +0000
Subject: [PATCH 20/42] refactor: fix PHP styling
---
app/Models/Feature/Feature.php | 2 ++
1 file changed, 2 insertions(+)
diff --git a/app/Models/Feature/Feature.php b/app/Models/Feature/Feature.php
index cf1dd84b5c..5daf59e90e 100644
--- a/app/Models/Feature/Feature.php
+++ b/app/Models/Feature/Feature.php
@@ -296,6 +296,8 @@ public function getAdminPowerAttribute() {
/**
* Displays the trait's subtypes as an imploded string.
+ *
+ * @param mixed|null $user
*/
public function displaySubtypes($user = null) {
if (!count($this->subtypes()->visible($user)->get())) {
From d337d92888ad3b01601b9a6c6e5fd1365392069f Mon Sep 17 00:00:00 2001
From: Draconizations
Date: Fri, 11 Jul 2025 14:49:01 +0000
Subject: [PATCH 21/42] refactor: fix PHP styling
---
app/Models/Feature/Feature.php | 2 ++
1 file changed, 2 insertions(+)
diff --git a/app/Models/Feature/Feature.php b/app/Models/Feature/Feature.php
index 70e1243e9d..9c336c10c7 100644
--- a/app/Models/Feature/Feature.php
+++ b/app/Models/Feature/Feature.php
@@ -286,6 +286,8 @@ public function getAdminPowerAttribute() {
/**
* Displays the trait's subtypes as an imploded string.
+ *
+ * @param mixed|null $user
*/
public function displaySubtypes($user = null) {
if (!count($this->subtypes()->visible($user)->get())) {
From 6908e89b68e7142a8c280d458cbdae719ee6cd06 Mon Sep 17 00:00:00 2001
From: Jake
Date: Fri, 11 Jul 2025 16:58:19 +0200
Subject: [PATCH 22/42] fix: copy and paste error (I promise I made this)
---
config/lorekeeper/ext-tracker/multiple_trait_subtypes.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/config/lorekeeper/ext-tracker/multiple_trait_subtypes.php b/config/lorekeeper/ext-tracker/multiple_trait_subtypes.php
index 9d0d278e6d..b3280963e2 100644
--- a/config/lorekeeper/ext-tracker/multiple_trait_subtypes.php
+++ b/config/lorekeeper/ext-tracker/multiple_trait_subtypes.php
@@ -3,7 +3,7 @@
return [
'wiki_key' => 'Multiple_Trait_Subtypes',
'creators' => json_encode([
- 'Uri' => 'https://github.com/Draconizations/',
+ 'Fulmn' => 'https://github.com/Draconizations/',
]),
'version' => '1.0.0',
];
From aa83fef4492553606a7c968d11c95380bdf39ec0 Mon Sep 17 00:00:00 2001
From: Jake
Date: Fri, 11 Jul 2025 17:07:36 +0200
Subject: [PATCH 23/42] fix: properly detach traits on deletion
---
app/Services/FeatureService.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/Services/FeatureService.php b/app/Services/FeatureService.php
index cd8beea1e7..6db629d26f 100644
--- a/app/Services/FeatureService.php
+++ b/app/Services/FeatureService.php
@@ -359,7 +359,7 @@ public function deleteFeature($feature, $user) {
throw new \Exception('Failed to log admin action.');
}
- $feature->subtypes->detach();
+ $feature->subtypes()->detach();
if ($feature->has_image) {
$this->deleteImage($feature->imagePath, $feature->imageFileName);
From 29f97804d2aaabb09843e0c81b0921be28a9707e Mon Sep 17 00:00:00 2001
From: Jake Fulmine
Date: Fri, 19 Sep 2025 15:57:17 +0200
Subject: [PATCH 24/42] fix: show species traits without subtype
---
app/Http/Controllers/WorldController.php | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/app/Http/Controllers/WorldController.php b/app/Http/Controllers/WorldController.php
index 92074284cc..1cfdd370ea 100644
--- a/app/Http/Controllers/WorldController.php
+++ b/app/Http/Controllers/WorldController.php
@@ -369,8 +369,12 @@ public function getSpeciesFeatures($id) {
$features = $features->orderByRaw('FIELD(rarity_id,'.implode(',', $rarities->pluck('id')->toArray()).')')
->orderBy('has_image', 'DESC')
->orderBy('name')
- ->get()->filter(function ($feature) {
- return !$feature->subtypes->where('is_visible', true)->isEmpty();
+ ->get()
+ ->filter(function ($feature) {
+ if (!$feature->subtypes->isEmpty()) {
+ return !$feature->subtypes->where('is_visible', true)->isEmpty();
+ }
+ return true;
})
->groupBy(['feature_category_id', 'id']);
From 84f9498300f7279bfc4c785791bb90b317e80227 Mon Sep 17 00:00:00 2001
From: Draconizations
Date: Fri, 19 Sep 2025 14:03:13 +0000
Subject: [PATCH 25/42] refactor: fix PHP styling
---
app/Http/Controllers/WorldController.php | 1 +
1 file changed, 1 insertion(+)
diff --git a/app/Http/Controllers/WorldController.php b/app/Http/Controllers/WorldController.php
index 1cfdd370ea..f54b4d7fec 100644
--- a/app/Http/Controllers/WorldController.php
+++ b/app/Http/Controllers/WorldController.php
@@ -374,6 +374,7 @@ public function getSpeciesFeatures($id) {
if (!$feature->subtypes->isEmpty()) {
return !$feature->subtypes->where('is_visible', true)->isEmpty();
}
+
return true;
})
->groupBy(['feature_category_id', 'id']);
From 44fd1d707bafc3bfc95a4c212a72c15c598e1597 Mon Sep 17 00:00:00 2001
From: ScuffedNewt
Date: Mon, 20 Oct 2025 22:05:58 +0100
Subject: [PATCH 26/42] feat: extra dropdown options
---
app/Models/Feature/Feature.php | 40 +++++++++++++++++--
config/lorekeeper/extensions.php | 12 +++++-
.../admin/_edit_features_modal.blade.php | 37 ++++++++++++++---
.../character/admin/upload_image.blade.php | 9 ++++-
resources/views/pages/credits.blade.php | 4 +-
.../views/widgets/_image_upload_js.blade.php | 39 ++++++++++++++----
6 files changed, 120 insertions(+), 21 deletions(-)
diff --git a/app/Models/Feature/Feature.php b/app/Models/Feature/Feature.php
index a09783407b..08b02e394d 100644
--- a/app/Models/Feature/Feature.php
+++ b/app/Models/Feature/Feature.php
@@ -292,10 +292,13 @@ public static function getDropdownItems($withHidden = 0) {
$visibleOnly = 0;
}
- if (config('lorekeeper.extensions.organised_traits_dropdown')) {
+ if (config('lorekeeper.extensions.organised_traits_dropdown.enable')) {
$sorted_feature_categories = collect(FeatureCategory::all()->where('is_visible', '>=', $visibleOnly)->sortBy('sort')->pluck('name')->toArray());
- $grouped = self::where('is_visible', '>=', $visibleOnly)->select('name', 'id', 'feature_category_id')->with('category')->orderBy('name')->get()->keyBy('id')->groupBy('category.name', $preserveKeys = true)->toArray();
+ $grouped = self::where('is_visible', '>=', $visibleOnly)
+ ->select('name', 'id', 'feature_category_id', 'rarity_id', 'species_id', 'subtype_id')->with(['category', 'rarity', 'species', 'subtype'])
+ ->orderBy('name')->get()->keyBy('id')->groupBy('category.name', $preserveKeys = true)
+ ->toArray();
if (isset($grouped[''])) {
if (!$sorted_feature_categories->contains('Miscellaneous')) {
$sorted_feature_categories->push('Miscellaneous');
@@ -307,9 +310,40 @@ public static function getDropdownItems($withHidden = 0) {
return in_array($value, array_keys($grouped), true);
});
+ // Sort by rarity if enabled
+ if (config('lorekeeper.extensions.organised_traits_dropdown.rarity.enable') && config('lorekeeper.extensions.organised_traits_dropdown.rarity.sort_by_rarity')) {
+ foreach ($grouped as $category => &$features) { // &$features to modify the array in place
+ uasort($features, function ($a, $b) {
+ $sortA = $a['rarity']['sort'] ?? -1;
+ $sortB = $b['rarity']['sort'] ?? -1;
+
+ if ($sortA == $sortB) {
+ return strnatcasecmp($a['name'], $b['name']);
+ }
+ return $sortB <=> $sortA;
+ });
+ }
+ unset($features); // break the reference
+ }
+
foreach ($grouped as $category => $features) {
foreach ($features as $id => $feature) {
- $grouped[$category][$id] = $feature['name'];
+ $grouped[$category][$id] = $feature['name'] .
+ (
+ config('lorekeeper.extensions.organised_traits_dropdown.display_species') && $feature['species_id'] ?
+ ' '.$feature['species']['name'].''
+ : ''
+ ) .
+ (
+ config('lorekeeper.extensions.organised_traits_dropdown.display_subtype') && $feature['subtype_id'] ?
+ ' ('.$feature['subtype']['name'].')'
+ : ''
+ ) .
+ ( // rarity
+ config('lorekeeper.extensions.organised_traits_dropdown.rarity.enable') && $feature['rarity'] ?
+ ' ('.Rarity::find($feature['rarity']['id'])->name.')'
+ : ''
+ );
}
}
$features_by_category = $sorted_feature_categories->map(function ($category) use ($grouped) {
diff --git a/config/lorekeeper/extensions.php b/config/lorekeeper/extensions.php
index c09b4f1eba..0d53b18304 100644
--- a/config/lorekeeper/extensions.php
+++ b/config/lorekeeper/extensions.php
@@ -83,8 +83,16 @@
'currency_id' => 1,
],
- // Organised Traits Dropdown - Draginraptor
- 'organised_traits_dropdown' => 0,
+ // Organised Traits Dropdown - Draginraptor, ScuffedNewt
+ 'organised_traits_dropdown' => [
+ 'enable' => 1,
+ 'display_species' => 0, // displays species
+ 'display_subtype' => 0, // displays subtype, SPECIES DISPLAY MUST ALSO BE ENABLED FOR THIS TO WORK.
+ 'rarity' => [
+ 'enable' => 1, // If enabled, displays trait rarity in the dropdown.
+ 'sort_by_rarity' => 1, // If enabled, sorts traits by rarity in the dropdown.
+ ]
+ ],
// Previous & Next buttons on Character pages - Speedy
// Adds buttons linking to the previous character as well as the next character on all character pages.
diff --git a/resources/views/character/admin/_edit_features_modal.blade.php b/resources/views/character/admin/_edit_features_modal.blade.php
index ea2fc214a7..ac29de65e5 100644
--- a/resources/views/character/admin/_edit_features_modal.blade.php
+++ b/resources/views/character/admin/_edit_features_modal.blade.php
@@ -40,11 +40,20 @@