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 @@