diff --git a/app/Http/Controllers/Admin/Characters/CharacterController.php b/app/Http/Controllers/Admin/Characters/CharacterController.php
index d3141340b0..02b33beccf 100644
--- a/app/Http/Controllers/Admin/Characters/CharacterController.php
+++ b/app/Http/Controllers/Admin/Characters/CharacterController.php
@@ -55,7 +55,7 @@ public function getCreateCharacter() {
'subtypes' => ['0' => 'Pick a Species First'],
'features' => Feature::getDropdownItems(1),
'isMyo' => false,
- 'titles' => ['0' => 'Select Title', 'custom' => 'Custom Title'] + CharacterTitle::orderBy('sort', 'DESC')->pluck('title', 'id')->toArray(),
+ 'titles' => ['custom' => 'Custom Title'] + CharacterTitle::orderBy('sort', 'DESC')->pluck('title', 'id')->toArray(),
]);
}
@@ -106,7 +106,7 @@ public function postCreateCharacter(Request $request, CharacterManager $service)
'designer_id', 'designer_url',
'artist_id', 'artist_url',
'species_id', 'subtype_id', 'rarity_id', 'feature_id', 'feature_data',
- 'image', 'thumbnail', 'image_description', 'title_id', 'title_data',
+ 'image', 'thumbnail', 'image_description', 'title_ids', 'title_data',
]);
if ($character = $service->createCharacter($data, Auth::user())) {
flash('Character created successfully.')->success();
diff --git a/app/Http/Controllers/Admin/Characters/CharacterImageController.php b/app/Http/Controllers/Admin/Characters/CharacterImageController.php
index 2cf58c6475..05e9cee655 100644
--- a/app/Http/Controllers/Admin/Characters/CharacterImageController.php
+++ b/app/Http/Controllers/Admin/Characters/CharacterImageController.php
@@ -121,7 +121,7 @@ public function getEditImageFeatures($id) {
* @return \Illuminate\Http\RedirectResponse
*/
public function postEditImageFeatures(Request $request, CharacterManager $service, $id) {
- $data = $request->only(['species_id', 'subtype_id', 'rarity_id', 'feature_id', 'feature_data', 'title_id', 'title_data']);
+ $data = $request->only(['species_id', 'subtype_id', 'rarity_id', 'feature_id', 'feature_data', 'title_ids', 'title_data']);
$image = CharacterImage::find($id);
if (!$image) {
abort(404);
diff --git a/app/Http/Controllers/Admin/Data/CharacterTitleController.php b/app/Http/Controllers/Admin/Data/CharacterTitleController.php
index 680d339617..d4d4331e1e 100644
--- a/app/Http/Controllers/Admin/Data/CharacterTitleController.php
+++ b/app/Http/Controllers/Admin/Data/CharacterTitleController.php
@@ -72,7 +72,7 @@ public function getEditTitle($id) {
public function postCreateEditTitle(Request $request, CharacterTitleService $service, $id = null) {
$id ? $request->validate(CharacterTitle::$updateRules) : $request->validate(CharacterTitle::$createRules);
$data = $request->only([
- 'title', 'short_title', 'rarity_id', 'description', 'image', 'remove_image',
+ 'title', 'short_title', 'rarity_id', 'description', 'image', 'remove_image', 'colour',
]);
if ($id && $service->updateTitle(CharacterTitle::find($id), $data, Auth::user())) {
flash('Title updated successfully.')->success();
diff --git a/app/Http/Controllers/BrowseController.php b/app/Http/Controllers/BrowseController.php
index 170fbdfcfd..b8bf22f179 100644
--- a/app/Http/Controllers/BrowseController.php
+++ b/app/Http/Controllers/BrowseController.php
@@ -231,13 +231,13 @@ public function getCharacters(Request $request) {
if ($request->get('title_id')) {
if ($request->get('title_id') == 'custom') {
- $imageQuery->whereNull('title_id')->whereNotNull('title_data');
+ $imageQuery->whereRelation('titles', 'title_id', null)->whereNotNull('title_data');
} else {
- $imageQuery->where('title_id', $request->get('title_id'));
+ $imageQuery->whereRelation('titles', 'title_id', $request->get('title_id'));
}
}
if ($request->get('title_id') == 'custom' && $request->get('title_data')) {
- $imageQuery->where('title_data', 'LIKE', '%'.$request->get('title_data').'%');
+ $imageQuery->whereRelation('titles', 'title_data', 'LIKE', '%'.$request->get('title_data').'%');
}
if ($request->get('artist')) {
diff --git a/app/Models/Character/CharacterDesignUpdate.php b/app/Models/Character/CharacterDesignUpdate.php
index 00f8d0ee1b..93bd15bbe5 100644
--- a/app/Models/Character/CharacterDesignUpdate.php
+++ b/app/Models/Character/CharacterDesignUpdate.php
@@ -25,7 +25,7 @@ class CharacterDesignUpdate extends Model {
'hash', 'species_id', 'subtype_id', 'rarity_id',
'has_comments', 'has_image', 'has_addons', 'has_features',
'submitted_at', 'update_type', 'fullsize_hash',
- 'approval_votes', 'rejection_votes', 'title_id', 'title_data',
+ 'approval_votes', 'rejection_votes',
];
/**
@@ -111,13 +111,6 @@ public function rarity() {
return $this->belongsTo(Rarity::class, 'rarity_id');
}
- /**
- * Get the title of the design update.
- */
- public function title() {
- return $this->belongsTo('App\Models\Character\CharacterTitle', 'title_id');
- }
-
/**
* Get the features (traits) attached to the design update, ordered by display order.
*/
@@ -348,15 +341,6 @@ public function getVoteDataAttribute() {
return collect(json_decode($this->attributes['vote_data'], true));
}
- /**
- * Get the title data attribute as an associative array.
- *
- * @return array
- */
- public function getTitleDataAttribute() {
- return json_decode($this->attributes['title_data'], true);
- }
-
/**********************************************************************************************
OTHER FUNCTIONS
diff --git a/app/Models/Character/CharacterImage.php b/app/Models/Character/CharacterImage.php
index 89d90c4b0e..de2016f58e 100644
--- a/app/Models/Character/CharacterImage.php
+++ b/app/Models/Character/CharacterImage.php
@@ -22,7 +22,7 @@ class CharacterImage extends Model {
'extension', 'use_cropper', 'hash', 'fullsize_hash', 'fullsize_extension', 'sort',
'x0', 'x1', 'y0', 'y1',
'description', 'parsed_description',
- 'is_valid', 'title_id', 'title_data',
+ 'is_valid',
];
/**
@@ -110,8 +110,8 @@ public function rarity() {
/**
* Get the title of the character image.
*/
- public function title() {
- return $this->belongsTo('App\Models\Character\CharacterTitle', 'title_id');
+ public function titles() {
+ return $this->hasMany(CharacterImageTitle::class, 'character_image_id');
}
/**
@@ -275,24 +275,31 @@ public function getThumbnailUrlAttribute() {
}
/**
- * Checks if the image has title info associated with it.
- *
+ * Displays all of the images titles.
+ *
* @return string
*/
- public function getHasTitleAttribute() {
- if (isset($this->title_id) || isset($this->title_data)) {
- return true;
- } else {
- return false;
+ public function getDisplayTitlesAttribute() {
+ $titles = [];
+ foreach ($this->titles as $title) {
+ $titles[] = $title->displayTitle;
}
+
+ return implode(' ', $titles);
}
/**
- * Get the title data attribute as an associative array.
+ * Gets the id array of titles for select forms.
*
- * @return array
+ * @return string
*/
- public function getTitleDataAttribute() {
- return json_decode($this->attributes['title_data'], true);
+ public function getTitleIdsAttribute() {
+ $ids = [];
+ // we have to do foreach because null id means 'custom' title
+ foreach ($this->titles as $title) {
+ $ids[] = $title->title_id ?? 'custom';
+ }
+
+ return $ids;
}
}
diff --git a/app/Models/Character/CharacterImageTitle.php b/app/Models/Character/CharacterImageTitle.php
new file mode 100644
index 0000000000..e893b6d83f
--- /dev/null
+++ b/app/Models/Character/CharacterImageTitle.php
@@ -0,0 +1,71 @@
+ 'array',
+ ];
+
+ /**********************************************************************************************
+
+ RELATIONS
+
+ **********************************************************************************************/
+
+ /**
+ * Get the character image.
+ */
+ public function image() {
+ return $this->belongsTo(CharacterImage::class, 'character_image_id');
+ }
+
+ /**
+ * Get the title.
+ */
+ public function title() {
+ return $this->belongsTo(CharacterTitle::class, 'title_id');
+ }
+
+ /**********************************************************************************************
+
+ ATTRIBUTES
+
+ **********************************************************************************************/
+
+ /**
+ * Displays the title.
+ *
+ * @return string
+ */
+ public function getDisplayTitleAttribute() {
+ if ($this->title_id) {
+ return $this->title->displayTitle($this->data);
+ }
+
+ return '
'.isset($this->data['short']) ?? $this->data['full'].'
';
+ }
+}
diff --git a/app/Models/Character/CharacterTitle.php b/app/Models/Character/CharacterTitle.php
index 53b46fed22..eb72fc353e 100644
--- a/app/Models/Character/CharacterTitle.php
+++ b/app/Models/Character/CharacterTitle.php
@@ -3,6 +3,7 @@
namespace App\Models\Character;
use App\Models\Model;
+use App\Models\Rarity;
class CharacterTitle extends Model {
/**
@@ -11,7 +12,7 @@ class CharacterTitle extends Model {
* @var array
*/
protected $fillable = [
- 'title', 'short_title', 'sort', 'has_image', 'description', 'parsed_description', 'rarity_id',
+ 'title', 'short_title', 'sort', 'has_image', 'description', 'parsed_description', 'rarity_id', 'colour',
];
/**
@@ -55,12 +56,12 @@ class CharacterTitle extends Model {
* Get the rarity of the character image.
*/
public function rarity() {
- return $this->belongsTo('App\Models\Rarity', 'rarity_id');
+ return $this->belongsTo(Rarity::class, 'rarity_id');
}
/**********************************************************************************************
- ACCESSORS
+ ATTRIBUTES
**********************************************************************************************/
@@ -88,7 +89,7 @@ public function getDisplayNamePartialAttribute() {
* @return string
*/
public function getDisplayNameFullAttribute() {
- return ''.$this->title.''.($this->short_title ? ' ('.$this->short_title.')' : '').($this->rarity ? ' ('.$this->rarity->displayName.')' : '');
+ return ''.$this->title.''.($this->short_title ? ' ('.$this->short_title.')' : '').($this->rarity ? ' ('.$this->rarity->displayName.')' : '');
}
/**
@@ -149,6 +150,15 @@ public function getUrlAttribute() {
return url('world/character-titles?title='.$this->title);
}
+ /**
+ * Gets the URL of the model's encyclopedia page.
+ *
+ * @return string
+ */
+ public function getIdUrlAttribute() {
+ return url('world/character-titles/'.$this->id);
+ }
+
/**
* Gets the URL for a masterlist search of characters of this rarity.
*
@@ -157,4 +167,28 @@ public function getUrlAttribute() {
public function getSearchCharactersUrlAttribute() {
return url('masterlist?title_id='.$this->id);
}
+
+ /**
+ * Gets the currency's asset type for asset management.
+ *
+ * @return string
+ */
+ public function getAssetTypeAttribute() {
+ return 'character_title';
+ }
+
+ /**********************************************************************************************
+
+ OTHER FUNCTIONS
+
+ **********************************************************************************************/
+
+ /**
+ * Displays the title like a typing.
+ */
+ public function displayTitle($data) {
+ return ''.$data['full'] : '>'.$this->title)
+ .'';
+ }
}
diff --git a/app/Services/CharacterManager.php b/app/Services/CharacterManager.php
index 170ef6205d..1d1f3dfb95 100644
--- a/app/Services/CharacterManager.php
+++ b/app/Services/CharacterManager.php
@@ -11,6 +11,7 @@
use App\Models\Character\CharacterDesignUpdate;
use App\Models\Character\CharacterFeature;
use App\Models\Character\CharacterImage;
+use App\Models\Character\CharacterImageTitle;
use App\Models\Character\CharacterTransfer;
use App\Models\Sales\SalesCharacter;
use App\Models\Species\Subtype;
@@ -635,15 +636,31 @@ public function updateImageFeatures($data, $image, $user) {
$old['species'] = $image->species_id ? $image->species->displayName : null;
$old['subtype'] = $image->subtype_id ? $image->subtype->displayName : null;
$old['rarity'] = $image->rarity_id ? $image->rarity->displayName : null;
- $old['title'] = $image->title_id ? $image->title->displayName : ($image->title_data ? $image->title_data : null);
+ $old['titles'] = $image->titles->count() ? json_encode($image->titles) : null;
// Clear old features
$image->features()->delete();
+ // Clear old titles
+ $image->titles()->delete();
// Attach features
foreach ($data['feature_id'] as $key => $featureId) {
if ($featureId) {
- $feature = CharacterFeature::create(['character_image_id' => $image->id, 'feature_id' => $featureId, 'data' => $data['feature_data'][$key]]);
+ $feature = CharacterFeature::create([
+ 'character_image_id' => $image->id,
+ 'feature_id' => $featureId,
+ 'data' => $data['feature_data'][$key]]);
+ }
+ }
+
+ // Attach titles
+ if (isset($data['title_ids'])) {
+ foreach ($data['title_ids'] as $key=>$titleId) {
+ CharacterImageTitle::create([
+ 'character_image_id' => $image->id,
+ 'title_id' => $titleId == 'custom' ? null : $titleId,
+ 'data' => isset($data['title_data'][$titleId]) ? $data['title_data'][$titleId] : null,
+ ]);
}
}
@@ -651,8 +668,6 @@ public function updateImageFeatures($data, $image, $user) {
$image->species_id = $data['species_id'];
$image->subtype_id = $data['subtype_id'] ?: null;
$image->rarity_id = $data['rarity_id'];
- $image->title_id = isset($data['title_id']) && $data['title_id'] ? ($data['title_id'] != 'custom' ? $data['title_id'] : null) : null;
- $image->title_data = $data['title_id'] && isset($data['title_data']) && isset($data['title_data']['full']) ? json_encode($data['title_data']) : null;
$image->save();
$new = [];
@@ -660,7 +675,7 @@ public function updateImageFeatures($data, $image, $user) {
$new['species'] = $image->species_id ? $image->species->displayName : null;
$new['subtype'] = $image->subtype_id ? $image->subtype->displayName : null;
$new['rarity'] = $image->rarity_id ? $image->rarity->displayName : null;
- $new['title'] = $image->title_id ? $image->title->displayName : ($image->title_data ? $image->title_data : null);
+ $new['title'] = $image->titles->count() ? json_encode($image->titles) : null;
// Character also keeps track of these features
$image->character->rarity_id = $image->rarity_id;
@@ -1918,7 +1933,7 @@ private function handleCharacterImage($data, $character, $isMyo = false) {
}
$imageData = Arr::only($data, [
'species_id', 'subtype_id', 'rarity_id', 'use_cropper',
- 'x0', 'x1', 'y0', 'y1', 'title_id', 'title_data',
+ 'x0', 'x1', 'y0', 'y1',
]);
$imageData['use_cropper'] = isset($data['use_cropper']);
$imageData['description'] = $data['image_description'] ?? null;
@@ -1931,11 +1946,20 @@ private function handleCharacterImage($data, $character, $isMyo = false) {
$imageData['extension'] = (config('lorekeeper.settings.masterlist_image_format') ?? ($data['extension'] ?? $data['image']->getClientOriginalExtension()));
$imageData['fullsize_extension'] = (config('lorekeeper.settings.masterlist_fullsizes_format') ?? ($data['fullsize_extension'] ?? $data['image']->getClientOriginalExtension()));
$imageData['character_id'] = $character->id;
- $imageData['title_id'] = isset($data['title_id']) && $data['title_id'] ? ($data['title_id'] != 'custom' ? $data['title_id'] : null) : null;
- $imageData['title_data'] = isset($data['title_data']) && $data['title_data'] && isset($data['title_data']['full']) ? json_encode($data['title_data']) : null;
$image = CharacterImage::create($imageData);
+ // Titles
+ if (isset($data['title_ids'])) {
+ foreach ($data['title_ids'] as $key=>$titleId) {
+ CharacterImageTitle::create([
+ 'character_image_id' => $image->id,
+ 'title_id' => $titleId == 'custom' ? null : $titleId,
+ 'data' => isset($data['title_data'][$titleId]) ? $data['title_data'][$titleId] : null,
+ ]);
+ }
+ }
+
// Check if entered url(s) have aliases associated with any on-site users
$designers = array_filter($data['designer_url']); // filter null values
foreach ($designers as $key=> $url) {
diff --git a/app/Services/DesignUpdateManager.php b/app/Services/DesignUpdateManager.php
index 2dd52cfd7f..7bf42fb3de 100644
--- a/app/Services/DesignUpdateManager.php
+++ b/app/Services/DesignUpdateManager.php
@@ -568,10 +568,17 @@ public function approveRequest($data, $request, $user) {
'subtype_id' => ($request->character->is_myo_slot && isset($request->character->image->subtype_id)) ? $request->character->image->subtype_id : $request->subtype_id,
'rarity_id' => $request->rarity_id,
'sort' => 0,
- 'title_id' => isset($request->title_id) && $request->title_id ? $request->title_id : null,
- 'title_data' => isset($request->title_data) ? json_encode($request->title_data) : null,
]);
+ // Add old image titles to the new image
+ $image->titles()->createMany($request->character->image->titles->map(function ($title) use ($image) {
+ return [
+ 'title_id' => $title->title_id,
+ 'character_image_id' => $image->id,
+ 'data' => $title->data,
+ ];
+ })->toArray());
+
// Shift the image credits over to the new image
$request->designers()->update(['character_type' => 'Character', 'character_image_id' => $image->id]);
$request->artists()->update(['character_type' => 'Character', 'character_image_id' => $image->id]);
diff --git a/app/Services/Item/TitleService.php b/app/Services/Item/TitleService.php
new file mode 100644
index 0000000000..54a6259462
--- /dev/null
+++ b/app/Services/Item/TitleService.php
@@ -0,0 +1,145 @@
+ CharacterTitle::orderBy('title')->pluck('title', 'id')->toArray(),
+ ];
+ }
+
+ /**
+ * Processes the data attribute of the tag and returns it in the preferred format.
+ *
+ * @param object $tag
+ *
+ * @return mixed
+ */
+ public function getTagData($tag) {
+ return [
+ 'type' => $tag->data['type'] ?? null,
+ 'title_ids' => $tag->data['title_ids'] ?? [],
+ ];
+ }
+
+ /**
+ * Processes the data attribute of the tag and returns it in the preferred format.
+ *
+ * @param object $tag
+ * @param array $data
+ *
+ * @return bool
+ */
+ public function updateData($tag, $data) {
+ DB::beginTransaction();
+
+ try {
+
+ $tag->update(['data' => Arr::only($data, ['type', 'title_ids'])]);
+
+ return $this->commitReturn(true);
+ } catch (\Exception $e) {
+ $this->setError('error', $e->getMessage());
+ }
+
+ return $this->rollbackReturn(false);
+ }
+
+ /**
+ * Acts upon the item when used from the inventory.
+ *
+ * @param \App\Models\User\UserItem $stacks
+ * @param \App\Models\User\User $user
+ * @param array $data
+ *
+ * @return bool
+ */
+ public function act($stacks, $user, $data) {
+ DB::beginTransaction();
+
+ try {
+ $character = Character::find($data['character_id']);
+ if (!$character) {
+ throw new \Exception('Character not found.');
+ }
+ foreach ($stacks as $key=> $stack) {
+ // We don't want to let anyone who isn't the owner of the title open it,
+ // so do some validation...
+ if ($stack->user_id != $user->id) {
+ throw new \Exception('This item does not belong to you.');
+ }
+
+ // Next, try to delete the title item. If successful, we can start distributing rewards.
+ if ((new InventoryManager)->debitStack($stack->user, 'Title Used', [
+ 'data' => 'Used on '.$character->displayName,
+ ], $stack, $data['quantities'][$key])) {
+
+ $tag = $stack->item->tag('title');
+ if ($tag->getData()['type'] == 'choice') {
+ $title = CharacterTitle::find($data['title_id']);
+ if (!$title) {
+ throw new \Exception('Title not found.');
+ }
+ CharacterImageTitle::create([
+ 'character_image_id' => $character->image->id,
+ 'title_id' => $data['title_id'],
+ 'data' => [],
+ ]);
+ } else {
+ foreach ($stack->item->data['title_ids'] as $key=> $title_id) {
+ $title = CharacterTitle::find($title_id);
+ if (!$title) {
+ throw new \Exception('Title not found.');
+ }
+ CharacterImageTitle::create([
+ 'character_image_id' => $character->image->id,
+ 'title_id' => $title->id,
+ 'data' => [],
+ ]);
+ }
+ }
+ }
+ }
+
+ return $this->commitReturn(true);
+ } catch (\Exception $e) {
+ $this->setError('error', $e->getMessage());
+ }
+
+ return $this->rollbackReturn(false);
+ }
+
+ /**
+ * Acts upon the item when used from the inventory.
+ *
+ * @param array $rewards
+ *
+ * @return string
+ */
+ private function getTitleRewardsString($rewards) {
+ return 'You have received: '.createRewardsString($rewards);
+ }
+}
diff --git a/config/lorekeeper/item_tags.php b/config/lorekeeper/item_tags.php
index 3435be8b6a..9c5138fc0d 100644
--- a/config/lorekeeper/item_tags.php
+++ b/config/lorekeeper/item_tags.php
@@ -24,4 +24,10 @@
'text_color' => '#ffffff',
'background_color' => '#1fd1a7',
],
+
+ 'title' => [
+ 'name' => 'Character Title',
+ 'text_color' => '#ffffff',
+ 'background_color' => '#f44336',
+ ],
];
diff --git a/database/migrations/2024_10_25_115614_update_character_title_tables.php b/database/migrations/2024_10_25_115614_update_character_title_tables.php
new file mode 100644
index 0000000000..240d85b2f0
--- /dev/null
+++ b/database/migrations/2024_10_25_115614_update_character_title_tables.php
@@ -0,0 +1,54 @@
+dropColumn('title_id');
+ $table->dropColumn('title_data');
+ });
+
+ Schema::table('design_updates', function (Blueprint $table) {
+ $table->dropColumn('title_id');
+ $table->dropColumn('title_data');
+ });
+
+ Schema::create('character_image_titles', function (Blueprint $table) {
+ $table->id();
+ $table->integer('character_image_id');
+ $table->integer('title_id')->nullable()->default(null); // nullable for custom titles
+ $table->json('data')->nullable()->default(null);
+ });
+
+ Schema::table('character_titles', function (Blueprint $table) {
+ $table->string('colour')->nullable()->default(null);
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void {
+ //
+ Schema::dropIfExists('character_image_titles');
+
+ Schema::table('design_updates', function (Blueprint $table) {
+ $table->integer('title_id')->nullable()->default(null)->index();
+ $table->string('title_data')->nullable()->default(null);
+ });
+
+ Schema::table('character_images', function (Blueprint $table) {
+ $table->integer('title_id')->nullable()->default(null)->index();
+ $table->string('title_data')->nullable()->default(null);
+ });
+ }
+};
diff --git a/resources/views/admin/character_titles/create_edit_title.blade.php b/resources/views/admin/character_titles/create_edit_title.blade.php
index 92a150f3d1..344384642e 100644
--- a/resources/views/admin/character_titles/create_edit_title.blade.php
+++ b/resources/views/admin/character_titles/create_edit_title.blade.php
@@ -22,23 +22,24 @@
Basic Information
-
-
- {!! Form::label('Title') !!}
- {!! Form::text('title', $title->title, ['class' => 'form-control']) !!}
-
+
+ {!! Form::label('Title') !!}
+ {!! Form::text('title', $title->title, ['class' => 'form-control']) !!}
-
-
- {!! Form::label('Short Title (Optional)') !!} {!! add_help('Will be used in place of the full title for display alongside character name, etc. if set.') !!}
- {!! Form::text('short_title', $title->short_title, ['class' => 'form-control']) !!}
-
+
+ {!! Form::label('Short Title (Optional)') !!} {!! add_help('Will be used in place of the full title for display alongside character name, etc. if set.') !!}
+ {!! Form::text('short_title', $title->short_title, ['class' => 'form-control']) !!}
-
-