Add reputation attribute to FalukantCharacter model and update related services and views

- Introduced a new 'reputation' attribute in the FalukantCharacter model with a default value and validation.
- Updated FalukantService to include 'reputation' in character attributes for API responses.
- Enhanced ReputationView component to display current reputation and load it from the API.
- Added translations for reputation in both German and English locales.
This commit is contained in:
Torsten Schulz (local)
2025-12-20 23:00:31 +01:00
parent f9ea4715d7
commit 6cf8fa8a9c
6 changed files with 125 additions and 5 deletions

View File

@@ -0,0 +1,79 @@
"use strict";
module.exports = {
async up(queryInterface, Sequelize) {
// falukant_data.character.reputation (integer, default random 20..80)
// Wichtig: Schema explizit angeben
// Vorgehen:
// - Spalte anlegen (falls noch nicht vorhanden)
// - bestehende Zeilen initialisieren (random 20..80)
// - DEFAULT setzen (random 20..80)
// - NOT NULL + CHECK 0..100 erzwingen
await queryInterface.sequelize.query(`
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_schema = 'falukant_data'
AND table_name = 'character'
AND column_name = 'reputation'
) THEN
ALTER TABLE falukant_data."character"
ADD COLUMN reputation integer;
END IF;
END$$;
`);
// Backfill: nur NULLs initialisieren (damit bestehende Werte nicht überschrieben werden)
await queryInterface.sequelize.query(`
UPDATE falukant_data."character"
SET reputation = (floor(random()*61)+20)::int
WHERE reputation IS NULL;
`);
// DEFAULT + NOT NULL (nach Backfill)
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data."character"
ALTER COLUMN reputation SET DEFAULT (floor(random()*61)+20)::int;
`);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data."character"
ALTER COLUMN reputation SET NOT NULL;
`);
// Enforce 0..100 at DB level (percent)
// (IF NOT EXISTS pattern, because deployments can be re-run)
await queryInterface.sequelize.query(`
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint c
JOIN pg_class t ON t.oid = c.conrelid
JOIN pg_namespace n ON n.oid = t.relnamespace
WHERE c.conname = 'character_reputation_0_100_chk'
AND n.nspname = 'falukant_data'
AND t.relname = 'character'
) THEN
ALTER TABLE falukant_data."character"
ADD CONSTRAINT character_reputation_0_100_chk
CHECK (reputation >= 0 AND reputation <= 100);
END IF;
END$$;
`);
},
async down(queryInterface, Sequelize) {
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data."character"
DROP CONSTRAINT IF EXISTS character_reputation_0_100_chk;
`);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data."character"
DROP COLUMN IF EXISTS reputation;
`);
},
};

View File

@@ -34,6 +34,18 @@ FalukantCharacter.init(
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 1}
,
reputation: {
type: DataTypes.INTEGER,
allowNull: false,
// Initialisierung: zufällig 20..80 (Prozent)
// DB-seitig per DEFAULT umgesetzt, damit es auch ohne App-Logic gilt.
defaultValue: sequelize.literal('(floor(random()*61)+20)'),
validate: {
min: 0,
max: 100
}
}
},
{
sequelize,

View File

@@ -372,7 +372,7 @@ class FalukantService extends BaseService {
{ model: TitleOfNobility, as: 'nobleTitle', attributes: ['labelTr', 'id'] },
{ model: CharacterTrait, as: 'traits', attributes: ['id', 'tr'] }
],
attributes: ['id', 'birthdate', 'gender', 'moodId', 'health']
attributes: ['id', 'birthdate', 'gender', 'moodId', 'health', 'reputation']
},
{
model: UserHouse,
@@ -496,7 +496,7 @@ class FalukantService extends BaseService {
{
model: FalukantCharacter,
as: 'character',
attributes: ['birthdate', 'health'],
attributes: ['birthdate', 'health', 'reputation'],
include: [
{
model: Relationship,

View File

@@ -742,7 +742,8 @@
"reputation": {
"title": "Reputation",
"overview": {
"title": "Übersicht"
"title": "Übersicht",
"current": "Deine aktuelle Reputation"
},
"party": {
"title": "Feste",

View File

@@ -198,6 +198,16 @@
"nobility": {
"cooldown": "You can only advance again on {date}."
},
"reputation": {
"title": "Reputation",
"overview": {
"title": "Overview",
"current": "Your current reputation"
},
"party": {
"title": "Parties"
}
},
"branchProduction": {
"storageAvailable": "Free storage"
},

View File

@@ -12,7 +12,10 @@
<div class="tab-content">
<div v-if="activeTab === 'overview'">
<p>Deine aktuelle Reputation: </p>
<p>
{{ $t('falukant.reputation.overview.current') }}:
<strong>{{ reputationDisplay }}</strong>
</p>
</div>
<div v-else-if="activeTab === 'party'">
@@ -169,7 +172,8 @@ export default {
selectedNobilityIds: [],
servantRatio: 50,
inProgressParties: [],
completedParties: []
completedParties: [],
reputation: null,
}
},
methods: {
@@ -198,6 +202,15 @@ export default {
return partyDate <= twentyFourHoursAgo;
});
},
async loadReputation() {
try {
const { data } = await apiClient.get('/api/falukant/info');
this.reputation = data?.character?.reputation ?? null;
} catch (e) {
console.error('Failed to load reputation', e);
this.reputation = null;
}
},
async loadNobilityTitles() {
this.nobilityTitles = await apiClient.get('/api/falukant/nobility/titels').then(r => r.data)
},
@@ -219,6 +232,10 @@ export default {
}
},
computed: {
reputationDisplay() {
if (this.reputation == null) return '—';
return String(this.reputation);
},
formattedCost() {
const type = this.partyTypes.find(t => t.id === this.newPartyTypeId) || {};
const music = this.musicTypes.find(m => m.id === this.musicId) || {};
@@ -245,6 +262,7 @@ export default {
await this.loadPartyTypes();
await this.loadNobilityTitles();
await this.loadParties();
await this.loadReputation();
}
}
</script>