Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
74788cd
support multiple subtypes for traits
Draconizations Jul 10, 2025
29c6b29
rename whereHas $query variable
Draconizations Jul 11, 2025
72b927a
Merge branch 'develop' of https://github.com/corowne/lorekeeper into …
Draconizations Jul 11, 2025
55fddaf
refactor getSubtypeFeatures to support multiple subtypes
Draconizations Jul 11, 2025
f8ac8e1
refactor: fix blade formatting
Draconizations Jul 11, 2025
d2905fa
refactor: fix PHP styling
Draconizations Jul 11, 2025
adae854
change model relation to belongsToMany
Draconizations Jul 11, 2025
04bf27d
add extension tracker file
Draconizations Jul 11, 2025
9b7f3ed
refactor: fix blade formatting
Draconizations Jul 11, 2025
dc8d84a
Merge remote-tracking branch 'origin/extension/multiple-trait-subtype…
Draconizations Jul 11, 2025
1a25d7a
small fixes
Draconizations Jul 11, 2025
7fba964
refactor: fix PHP styling
Draconizations Jul 11, 2025
3196e2d
fix: show subtypes again
Draconizations Jul 11, 2025
8be4fbd
refactor: fix blade formatting
Draconizations Jul 11, 2025
68137bc
Merge remote-tracking branch 'origin/extension/multiple-trait-subtype…
Draconizations Jul 11, 2025
19942b0
oops
Draconizations Jul 11, 2025
59fd71c
fix: properly attach subtypes on feature creation
Draconizations Jul 11, 2025
b635081
Merge branch 'extension/multiple-trait-subtypes' of github.com:Dracon…
Draconizations Jul 11, 2025
4cd03b5
Merge remote-tracking branch 'origin/extension/multiple-trait-subtype…
Draconizations Jul 11, 2025
8c04b2e
fix: properly allow setting 0 subtypes
Draconizations Jul 11, 2025
903f0d3
Merge remote-tracking branch 'origin/extension/multiple-trait-subtype…
Draconizations Jul 11, 2025
9db392c
fix: only include traits that include that subtype when viewing basics
Draconizations Jul 11, 2025
ce32444
refactor: fix PHP styling
Draconizations Jul 11, 2025
3a04560
fix: more subtype -> subtypes fixes
Draconizations Jul 11, 2025
4dada00
only show visible subtypes to user
Draconizations Jul 11, 2025
ba0e932
refactor: fix PHP styling
Draconizations Jul 11, 2025
806db57
Merge remote-tracking branch 'origin/extension/multiple-trait-subtype…
Draconizations Jul 11, 2025
d7821df
Merge branch 'extension/multiple-trait-subtypes-v3.1' of github.com:D…
Draconizations Jul 11, 2025
d337d92
refactor: fix PHP styling
Draconizations Jul 11, 2025
6908e89
fix: copy and paste error (I promise I made this)
Draconizations Jul 11, 2025
4a89c50
Merge branch 'extension/multiple-trait-subtypes' of github.com:Dracon…
Draconizations Jul 11, 2025
fdd3927
Merge remote-tracking branch 'origin/extension/multiple-trait-subtype…
Draconizations Jul 11, 2025
8d4aae2
Merge branch 'extension/multiple-trait-subtypes-v3.1' of github.com:D…
Draconizations Jul 11, 2025
aa83fef
fix: properly detach traits on deletion
Draconizations Jul 11, 2025
0e4db4f
fix: properly detach subtypes on trait deletion
Draconizations Jul 11, 2025
29f9780
fix: show species traits without subtype
Draconizations Sep 19, 2025
84f9498
refactor: fix PHP styling
Draconizations Sep 19, 2025
44fd1d7
feat: extra dropdown options
ScuffedNewt Oct 20, 2025
b2ab079
refactor: fix PHP styling
ScuffedNewt Oct 20, 2025
c3df2d0
refactor: fix blade formatting
ScuffedNewt Oct 20, 2025
7d8191d
chore: remove console.logs
ScuffedNewt Oct 28, 2025
4c59e9b
Merge branch 'feature/trait-dropdown-enhanced' of https://github.com/…
ScuffedNewt Oct 28, 2025
a70677e
Update config/lorekeeper/extensions.php
ScuffedNewt Nov 12, 2025
046e8e9
Merge branch 'enhanced-trait-dropdown' into 'multiple-trait-subtypes-…
Draconizations Nov 12, 2025
e286b22
update multiple trait subtypes in dropdown
Draconizations Nov 12, 2025
b34911f
move extension to credits page
Draconizations Nov 25, 2025
5f2965e
fix logic in console command
Draconizations Nov 25, 2025
52afc54
refactor: fix PHP styling
Draconizations Nov 25, 2025
602982a
merge new trait dropdown fixes
Draconizations Nov 25, 2025
af40910
merge develop@upstream into feat/multiple-trait-subtypes
Draconizations Nov 25, 2025
97fafbd
merge php styling
Draconizations Nov 25, 2025
5a575ae
refactor: fix PHP styling
Draconizations Nov 25, 2025
a8b586f
fix: put those comments back where they belong
Draconizations Nov 25, 2025
e05b2aa
begone pesky period
Draconizations Nov 25, 2025
68d2bbf
fix another merge mistake (reverse sorting)
Draconizations Nov 25, 2025
110f425
...another merge mistake OTL
Draconizations Nov 25, 2025
4cee8d5
fix: only show visible subtypes to user
Draconizations Nov 26, 2025
88e5936
Merge remote-tracking branch 'upstream/develop' into feat/multiple-tr…
Draconizations Nov 27, 2025
2749709
refactor: remove unneccesary implode/explode
Draconizations Nov 27, 2025
30e19b6
refactor: fix blade formatting
Draconizations Nov 27, 2025
f78ffae
Merge remote-tracking branch 'upstream/develop' into feat/multiple-tr…
Draconizations Dec 2, 2025
dc9af8e
Merge remote-tracking branch 'fulmine/feat/multiple-trait-subtypes' i…
Draconizations Dec 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions app/Console/Commands/ConvertTraitSubtype.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php

namespace App\Console\Commands;

use App\Models\Feature\Feature;
use App\Models\Feature\FeatureSubtype;
use Illuminate\Console\Command;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class ConvertTraitSubtype extends Command {
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'convert-trait-subtype';

/**
* The console command description.
*
* @var string
*/
protected $description = 'Converts the subtype_id column in the features table to a new row in the feature_subtypes table.';

/**
* Create a new command instance.
*/
public function __construct() {
parent::__construct();
}

/**
* Execute the console command.
*
* @return mixed
*/
public function handle() {
if (Schema::hasTable('feature_subtypes')) {
if (!Schema::hasColumn('features', 'subtype_id')) {
$this->info('This command will not execute, as it has already been run.');

return;
}

$features = Feature::where(function ($query) {
$query->whereNotNull('subtype_id');
})->get();

$this->info('Converting '.count($features).' features\' subtypes...');
$bar = $this->output->createProgressBar(count($features));
foreach ($features as $feature) {
if ($feature->subtype_id) {
FeatureSubtype::create([
'feature_id' => $feature->id,
'subtype_id' => $feature->subtype_id,
]);
}
$bar->advance();
}

$bar->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('Required table does not exist, please run migrations first.');
}
}
}
18 changes: 10 additions & 8 deletions app/Http/Controllers/Admin/Data/FeatureController.php
Original file line number Diff line number Diff line change
Expand Up @@ -184,9 +184,11 @@ public function getFeatureIndex(Request $request) {
}
if (isset($data['subtype_id']) && $data['subtype_id'] != 'none') {
if ($data['subtype_id'] == 'withoutOption') {
$query->whereNull('subtype_id');
$query->subtypes->isEmpty();
} else {
$query->where('subtype_id', $data['subtype_id']);
$query->whereHas('subtypes', function ($query) use ($data) {
$query->where('subtype_id', $data['subtype_id']);
});
}
}
if (isset($data['name'])) {
Expand Down Expand Up @@ -253,7 +255,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(),
]);
}
Expand All @@ -275,7 +277,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(),
]);
}
Expand All @@ -291,7 +293,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();
Expand Down Expand Up @@ -350,11 +352,11 @@ 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');

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,
]);
}
}
23 changes: 16 additions & 7 deletions app/Http/Controllers/WorldController.php
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ public function getFeatureCategories(Request $request) {
* @return \Illuminate\Contracts\Support\Renderable
*/
public function getFeatures(Request $request) {
$query = Feature::visible(Auth::user() ?? null)->with('category', 'rarity', 'species', 'subtype');
$query = Feature::visible(Auth::user() ?? null)->with('category', 'rarity', 'species', 'subtypes');

$data = $request->only(['rarity_id', 'feature_category_id', 'species_id', 'subtype_id', 'name', 'sort']);

Expand All @@ -287,9 +287,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 ($query) use ($data) {
$query->where('subtype_id', $data['subtype_id']);
});
}
}
}
if (isset($data['name'])) {
Expand Down Expand Up @@ -358,15 +362,20 @@ public function getSpeciesFeatures($id) {
abort(404);
}

$features = $species->features()->visible(Auth::user() ?? null)->with('rarity', 'subtype');
$features = $species->features()->visible(Auth::user() ?? null)->with('rarity', 'subtypes');
$features = count($categories) ?
$features->orderByRaw('FIELD(feature_category_id,'.implode(',', $categories->pluck('id')->toArray()).')') :
$features;
$features = $features->orderByRaw('FIELD(rarity_id,'.implode(',', $rarities->pluck('id')->toArray()).')')
->orderBy('has_image', 'DESC')
->orderBy('name')
->get()->filter(function ($feature) {
return $feature->subtype?->is_visible !== 0;
->get()
->filter(function ($feature) {
if (!$feature->subtypes->isEmpty()) {
return !$feature->subtypes->where('is_visible', true)->isEmpty();
}

return true;
})
->groupBy(['feature_category_id', 'id']);

Expand Down Expand Up @@ -413,7 +422,7 @@ 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->isEmpty() || !$feature->subtypes->where('id', $subtype->id)->isEmpty();
})
->groupBy(['feature_category_id', 'id']);
}
Expand Down
62 changes: 49 additions & 13 deletions app/Models/Feature/Feature.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,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',
];

/**
Expand All @@ -32,7 +32,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',
Expand All @@ -47,7 +46,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',
Expand Down Expand Up @@ -75,17 +73,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->belongsToMany(Subtype::class, 'feature_subtypes');
}

/**********************************************************************************************
Expand Down Expand Up @@ -144,7 +142,14 @@ 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;
if (count($ids)) {
$ordered_subtypes = $this->subtypes()->latest('id')->pluck('id')->toArray();
foreach ($ordered_subtypes as $subtype) {
return $query->with('feature_subtypes')->orderBy(DB::raw($subtype.', '.implode(',', $ids).')'));
}
}

return $query;
}

/**
Expand Down Expand Up @@ -286,6 +291,32 @@ public function getAdminPowerAttribute() {

**********************************************************************************************/

/**
* Gets the trait's subtypes that are visible to the current user.
*
* @param mixed|null $user
*/
public function getSubtypes($user = null) {
return $this->subtypes()->visible($user)->get();
}

/**
* 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())) {
return 'None';
}
$subtypes = [];
foreach ($this->subtypes()->visible($user)->get() as $subtype) {
$subtypes[] = $subtype->displayName;
}

return implode(', ', $subtypes);
}

public static function getDropdownItems($withHidden = 0) {
$visibleOnly = 1;
if ($withHidden) {
Expand All @@ -296,7 +327,7 @@ public static function getDropdownItems($withHidden = 0) {
$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', 'rarity_id', 'species_id', 'subtype_id')->with(['category', 'rarity', 'species', 'subtype'])
->select('name', 'id', 'feature_category_id', 'rarity_id', 'species_id')->with(['category', 'rarity', 'species', 'subtypes'])
->orderBy('name')->get()->keyBy('id')->groupBy('category.name', $preserveKeys = true)
->toArray();
if (isset($grouped[''])) {
Expand Down Expand Up @@ -336,8 +367,13 @@ public static function getDropdownItems($withHidden = 0) {
: ''
).
(
config('lorekeeper.extensions.organised_traits_dropdown.display_subtype') && $feature['subtype_id'] ?
' <span class="text-muted"><small>('.$feature['subtype']['name'].')</small></span>'
config('lorekeeper.extensions.organised_traits_dropdown.display_subtype') && count($feature['subtypes']) ?
' <span class="text-muted"><small>('.implode(', ', array_map(
function (array $subtype) {
return $subtype['name'];
},
$feature['subtypes']
)).')</small></span>'
: ''
).
( // rarity
Expand Down
51 changes: 51 additions & 0 deletions app/Models/Feature/FeatureSubtype.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

namespace App\Models\Feature;

use App\Models\Model;
use App\Models\Species\Subtype;

class FeatureSubtype extends Model {
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'feature_id', 'subtype_id',
];

/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'feature_subtypes';

/**
* Whether the model contains timestamps to be saved and updated.
*
* @var string
*/
public $timestamps = false;

/**********************************************************************************************

RELATIONS

**********************************************************************************************/

/**
* Get the feature associated with this record.
*/
public function feature() {
return $this->belongsTo(Feature::class, 'feature_id');
}

/**
* Get the subtype associated with this record.
*/
public function subtype() {
return $this->belongsTo(Subtype::class, 'subtype_id');
}
}
2 changes: 1 addition & 1 deletion app/Models/Species/Subtype.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public function species() {
* Get the features associated with this subtype.
*/
public function features() {
return $this->hasMany(Feature::class);
return $this->belongsToMany(Feature::class, 'feature_subtypes');
}

/**********************************************************************************************
Expand Down
Loading