'datetime', 'ends_at' => 'datetime', 'submission_starts_at' => 'datetime', 'submission_ends_at' => 'datetime', 'published_at' => 'datetime', 'is_featured' => 'boolean', 'accepts_submissions' => 'boolean', 'allow_readd_after_removal' => 'boolean', 'submission_note_enabled' => 'boolean', 'community_section_enabled' => 'boolean', 'is_recurring' => 'boolean', 'edition_year' => 'integer', 'related_tags_json' => 'array', 'section_order_json' => 'array', 'section_visibility_json' => 'array', ]; public function createdBy(): BelongsTo { return $this->belongsTo(User::class, 'created_by_user_id'); } public function parentWorld(): BelongsTo { return $this->belongsTo(self::class, 'parent_world_id'); } public function archiveEditions(): HasMany { return $this->hasMany(self::class, 'parent_world_id')->orderByDesc('edition_year')->orderByDesc('starts_at'); } public function worldRelations(): HasMany { return $this->hasMany(WorldRelation::class)->orderBy('section_key')->orderBy('sort_order')->orderBy('id'); } public function worldSubmissions(): HasMany { return $this->hasMany(WorldSubmission::class)->orderByDesc('reviewed_at')->orderByDesc('created_at'); } public function scopePublished(Builder $query): Builder { return $query ->where('status', self::STATUS_PUBLISHED) ->where(function (Builder $builder): void { $builder->whereNull('published_at') ->orWhere('published_at', '<=', now()); }); } public function scopePubliclyVisible(Builder $query): Builder { return $query ->whereIn('status', [self::STATUS_PUBLISHED, self::STATUS_ARCHIVED]) ->where(function (Builder $builder): void { $builder->whereNull('published_at') ->orWhere('published_at', '<=', now()); }); } public function scopeCurrent(Builder $query): Builder { return $query ->published() ->where(function (Builder $builder): void { $builder->whereNull('starts_at') ->orWhere('starts_at', '<=', now()); }) ->where(function (Builder $builder): void { $builder->whereNull('ends_at') ->orWhere('ends_at', '>=', now()); }); } public function scopeUpcoming(Builder $query): Builder { return $query ->published() ->whereNotNull('starts_at') ->where('starts_at', '>', now()); } public function scopeArchive(Builder $query): Builder { return $query ->publiclyVisible() ->where(function (Builder $builder): void { $builder->where('status', self::STATUS_ARCHIVED) ->orWhere(function (Builder $expired): void { $expired->whereNotNull('ends_at') ->where('ends_at', '<', now()); }); }); } public function isPubliclyVisible(): bool { if (! in_array($this->status, [self::STATUS_PUBLISHED, self::STATUS_ARCHIVED], true)) { return false; } if ($this->published_at && $this->published_at->isFuture()) { return false; } return true; } public function isCurrent(): bool { if (! $this->isPubliclyVisible()) { return false; } if ($this->starts_at && $this->starts_at->isFuture()) { return false; } if ($this->ends_at && $this->ends_at->isPast()) { return false; } return true; } public function isAcceptingSubmissions(): bool { if (! $this->isPubliclyVisible() || ! $this->accepts_submissions || ! $this->allowsCreatorParticipation()) { return false; } if ($this->submission_starts_at && $this->submission_starts_at->isFuture()) { return false; } if ($this->submission_ends_at && $this->submission_ends_at->isPast()) { return false; } return true; } public function allowsCreatorParticipation(): bool { return in_array((string) $this->participation_mode, [ self::PARTICIPATION_MODE_MANUAL_APPROVAL, self::PARTICIPATION_MODE_AUTO_ADD, ], true); } public function submissionStartsAsLive(): bool { return (string) $this->participation_mode === self::PARTICIPATION_MODE_AUTO_ADD; } public function coverUrl(): ?string { $path = trim((string) $this->cover_path); if ($path === '') { return null; } if (str_starts_with($path, 'http://') || str_starts_with($path, 'https://')) { return $path; } return rtrim((string) config('cdn.files_url', 'https://files.skinbase.org'), '/') . '/' . ltrim($path, '/'); } public function ogImageUrl(): ?string { $path = trim((string) ($this->og_image_path ?: $this->cover_path ?: '')); if ($path === '') { return null; } if (str_starts_with($path, 'http://') || str_starts_with($path, 'https://')) { return $path; } return rtrim((string) config('cdn.files_url', 'https://files.skinbase.org'), '/') . '/' . ltrim($path, '/'); } public function publicUrl(): string { return route('worlds.show', ['world' => $this->slug]); } public function sectionOrder(): array { $defaults = array_values(array_filter(config('worlds.default_section_order', []), 'is_string')); $custom = array_values(array_filter($this->section_order_json ?? [], 'is_string')); return array_values(array_unique(array_merge($custom, $defaults))); } public function sectionVisibility(): array { $defaults = collect(array_keys((array) config('worlds.sections', []))) ->mapWithKeys(fn (string $key): array => [$key => true]) ->all(); $custom = collect((array) $this->section_visibility_json) ->mapWithKeys(fn ($value, $key): array => [(string) $key => (bool) $value]) ->all(); return array_merge($defaults, $custom); } }