Speichere individuelle Haar- und Augenfarben je Charakter
All checks were successful
Deploy to production / deploy (push) Successful in 2m24s
All checks were successful
Deploy to production / deploy (push) Successful in 2m24s
This commit is contained in:
38
backend/docs/falukant-character-appearance.md
Normal file
38
backend/docs/falukant-character-appearance.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# Dauerhafte Charakterfarben
|
||||
|
||||
`falukant_data.character` enthält `hair_color` und `eye_color` als `VARCHAR(7)`.
|
||||
Die Sequelize- und API-Felder heißen `hairColor` und `eyeColor`.
|
||||
Erlaubt sind sechsstellige Hex-Farben einschließlich `#`, z. B. `#69432d`.
|
||||
|
||||
Migration: `20261104000001-add-character-appearance.cjs`.
|
||||
Im Backend-Verzeichnis mit `npm run db:migrate` ausführen, bevor der neue
|
||||
Backend-Code startet. Die Migration wird hier nur in einer isolierten
|
||||
Testdatenbank ausgeführt; die produktive Datenbank wird nicht verändert.
|
||||
|
||||
Die Migration vergibt für bestehende Charaktere einmalig Farben aus einer
|
||||
Palette mit sieben Haar- und sechs Augenfarben. Bereits vorhandene Werte
|
||||
bleiben erhalten, auch beim erneuten Ausführen der Migration. Ein separater
|
||||
DB-Trigger vergibt Farben für neue Charaktere, einschließlich roher INSERTs
|
||||
des Rust-Daemons für NPCs und Neugeborene. NULL-Werte bei Updates behalten die
|
||||
bisherige Farbe. Andere explizite, gültige Hex-Farben sind ebenfalls zulässig.
|
||||
Die Farben ändern sich beim Altern nicht automatisch. Vererbung und Ergrauen
|
||||
sind noch nicht implementiert; die aktuelle Palette ist eine Spielverteilung.
|
||||
|
||||
Familie (Partner, Kinder, Liebschaften), Taufliste, Direktoren sowie politische
|
||||
und kirchliche Amtsträger liefern die Farben an die vorhandene 3D-Anzeige.
|
||||
Die Spielerdaten und das Dashboard liefern sie ebenfalls. Die 3D-Komponenten
|
||||
verwenden die gespeicherten Farben nur in den markierten Haar-/Irisflächen.
|
||||
Unmarkierte Altersmodelle und die 2D-Porträts behalten ihre Originaltexturen.
|
||||
Der Modell-Editor bleibt eine Vorschau ohne Schreibzugriff auf Charakterdaten.
|
||||
|
||||
Prüfung mit einer ausdrücklich bereitgestellten, wegwerfbaren Datenbank:
|
||||
|
||||
```bash
|
||||
TEST_DATABASE_URL='postgresql://…/yourpart_development_test' \
|
||||
node --test tools/characterAppearance.test.mjs
|
||||
```
|
||||
|
||||
Der Integrationstest prüft Bestandsdaten, wiederholte und teilweise manuell
|
||||
vorbereitete Migrationen, beide DB-Trigger zusammen, rohe INSERTs,
|
||||
Sequelize-Erstellung/-Updates, Farbvalidierung und Rollback. Er verwirft
|
||||
das Schema `falukant_data` ausschließlich in `yourpart_development_test`.
|
||||
@@ -0,0 +1,75 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface) {
|
||||
await queryInterface.sequelize.transaction(async (transaction) => {
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE falukant_data.character
|
||||
ADD COLUMN IF NOT EXISTS hair_color VARCHAR(7),
|
||||
ADD COLUMN IF NOT EXISTS eye_color VARCHAR(7);
|
||||
|
||||
CREATE OR REPLACE FUNCTION falukant_data.initialize_character_appearance()
|
||||
RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
BEGIN
|
||||
IF TG_OP = 'UPDATE' THEN
|
||||
NEW.hair_color := COALESCE(NEW.hair_color, OLD.hair_color);
|
||||
NEW.eye_color := COALESCE(NEW.eye_color, OLD.eye_color);
|
||||
END IF;
|
||||
IF NEW.hair_color IS NULL THEN
|
||||
NEW.hair_color := (ARRAY[
|
||||
'#211b18', '#493024', '#69432d', '#95603c', '#c69c60', '#d8bf83', '#a34826'
|
||||
])[1 + floor(random() * 7)::int];
|
||||
END IF;
|
||||
IF NEW.eye_color IS NULL THEN
|
||||
NEW.eye_color := (ARRAY[
|
||||
'#493026', '#795532', '#8b7544', '#56704c', '#497c9c', '#75858b'
|
||||
])[1 + floor(random() * 6)::int];
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DROP TRIGGER IF EXISTS initialize_character_appearance ON falukant_data.character;
|
||||
CREATE TRIGGER initialize_character_appearance
|
||||
BEFORE INSERT OR UPDATE ON falukant_data.character
|
||||
FOR EACH ROW EXECUTE FUNCTION falukant_data.initialize_character_appearance();
|
||||
|
||||
UPDATE falukant_data.character SET hair_color = hair_color
|
||||
WHERE hair_color IS NULL OR eye_color IS NULL;
|
||||
|
||||
ALTER TABLE falukant_data.character
|
||||
ALTER COLUMN hair_color SET NOT NULL,
|
||||
ALTER COLUMN eye_color SET NOT NULL;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint
|
||||
WHERE conrelid = 'falukant_data.character'::regclass
|
||||
AND conname = 'character_appearance_colors_valid'
|
||||
) THEN
|
||||
ALTER TABLE falukant_data.character
|
||||
ADD CONSTRAINT character_appearance_colors_valid CHECK (
|
||||
hair_color ~ '^#[0-9a-fA-F]{6}$'
|
||||
AND eye_color ~ '^#[0-9a-fA-F]{6}$'
|
||||
);
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
`, { transaction });
|
||||
});
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
await queryInterface.sequelize.transaction(async (transaction) => {
|
||||
await queryInterface.sequelize.query(`
|
||||
DROP TRIGGER IF EXISTS initialize_character_appearance ON falukant_data.character;
|
||||
DROP FUNCTION IF EXISTS falukant_data.initialize_character_appearance();
|
||||
ALTER TABLE falukant_data.character
|
||||
DROP CONSTRAINT IF EXISTS character_appearance_colors_valid,
|
||||
DROP COLUMN IF EXISTS hair_color,
|
||||
DROP COLUMN IF EXISTS eye_color;
|
||||
`, { transaction });
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -23,6 +23,17 @@ FalukantCharacter.init(
|
||||
defaultValue: DataTypes.NOW},
|
||||
gender: {
|
||||
type: DataTypes.STRING},
|
||||
// The DB trigger initializes these for Sequelize and raw daemon inserts.
|
||||
hairColor: {
|
||||
type: DataTypes.STRING(7),
|
||||
allowNull: true,
|
||||
validate: { is: /^#[0-9a-fA-F]{6}$/ },
|
||||
},
|
||||
eyeColor: {
|
||||
type: DataTypes.STRING(7),
|
||||
allowNull: true,
|
||||
validate: { is: /^#[0-9a-fA-F]{6}$/ },
|
||||
},
|
||||
// Initialized once by the DB trigger, also for raw daemon INSERTs.
|
||||
// Nullable here so Sequelize lets omitted values reach that trigger;
|
||||
// the database enforces NOT NULL and the ordering of both ages.
|
||||
|
||||
@@ -980,7 +980,7 @@ class FalukantService extends BaseService {
|
||||
{ model: TitleOfNobility, as: 'nobleTitle', attributes: ['labelTr', 'id', 'level'] }
|
||||
],
|
||||
// reputation: für Adel-Voraussetzungen u.a. (checkReputationRequirement nutzt user.character ohne Nachladen)
|
||||
attributes: ['id', 'birthdate', 'gender', 'moodId', 'health', 'reputation', 'pubertyStartAge', 'fertileFromAge']
|
||||
attributes: ['id', 'birthdate', 'gender', 'moodId', 'health', 'reputation', 'pubertyStartAge', 'fertileFromAge', 'hairColor', 'eyeColor']
|
||||
},
|
||||
]
|
||||
});
|
||||
@@ -1028,7 +1028,7 @@ class FalukantService extends BaseService {
|
||||
{ model: FalukantPredefineLastname, as: 'definedLastName', attributes: ['name'] },
|
||||
{ model: TitleOfNobility, as: 'nobleTitle', attributes: ['labelTr', 'level'] }
|
||||
],
|
||||
attributes: ['id', 'birthdate', 'gender', 'reputation', 'titleOfNobility']
|
||||
attributes: ['id', 'birthdate', 'gender', 'reputation', 'titleOfNobility', 'hairColor', 'eyeColor']
|
||||
},
|
||||
{
|
||||
model: RegionData,
|
||||
@@ -1142,7 +1142,7 @@ class FalukantService extends BaseService {
|
||||
{
|
||||
model: FalukantCharacter,
|
||||
as: 'character',
|
||||
attributes: ['id', 'birthdate', 'health', 'reputation', 'titleOfNobility', 'pubertyStartAge', 'fertileFromAge'],
|
||||
attributes: ['id', 'birthdate', 'health', 'reputation', 'titleOfNobility', 'pubertyStartAge', 'fertileFromAge', 'hairColor', 'eyeColor'],
|
||||
include: [
|
||||
{ model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] },
|
||||
{ model: FalukantPredefineLastname, as: 'definedLastName', attributes: ['name'] },
|
||||
@@ -2944,7 +2944,7 @@ class FalukantService extends BaseService {
|
||||
{
|
||||
model: FalukantCharacter,
|
||||
as: 'character',
|
||||
attributes: ['firstName', 'lastName', 'birthdate', 'titleOfNobility', 'gender'],
|
||||
attributes: ['firstName', 'lastName', 'birthdate', 'titleOfNobility', 'gender', 'hairColor', 'eyeColor'],
|
||||
where: { regionId },
|
||||
include: [
|
||||
{ model: FalukantPredefineFirstname, as: 'definedFirstName' },
|
||||
@@ -3287,6 +3287,8 @@ class FalukantService extends BaseService {
|
||||
age,
|
||||
knowledge,
|
||||
gender: proposal.character.gender,
|
||||
hairColor: proposal.character.hairColor ?? null,
|
||||
eyeColor: proposal.character.eyeColor ?? null,
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -3370,7 +3372,7 @@ class FalukantService extends BaseService {
|
||||
as: 'character',
|
||||
// Wichtige Felder explizit auswählen, damit die Joins auf Titel,
|
||||
// Namen und Region im äußeren Select funktionieren.
|
||||
attributes: ['id', 'birthdate', 'gender', 'titleOfNobility', 'firstName', 'lastName', 'regionId'],
|
||||
attributes: ['id', 'birthdate', 'gender', 'titleOfNobility', 'firstName', 'lastName', 'regionId', 'hairColor', 'eyeColor'],
|
||||
where: {
|
||||
regionId: branch.regionId,
|
||||
},
|
||||
@@ -3434,6 +3436,8 @@ class FalukantService extends BaseService {
|
||||
title: director.character.nobleTitle.labelTr,
|
||||
age,
|
||||
gender: director.character.gender,
|
||||
hairColor: director.character.hairColor ?? null,
|
||||
eyeColor: director.character.eyeColor ?? null,
|
||||
nobleTitle: director.character.nobleTitle,
|
||||
definedFirstName: director.character.definedFirstName,
|
||||
definedLastName: director.character.definedLastName,
|
||||
@@ -3463,7 +3467,7 @@ class FalukantService extends BaseService {
|
||||
{
|
||||
model: FalukantCharacter,
|
||||
as: 'character',
|
||||
attributes: ['id', 'birthdate', 'gender'],
|
||||
attributes: ['id', 'birthdate', 'gender', 'hairColor', 'eyeColor'],
|
||||
include: [
|
||||
{
|
||||
model: TitleOfNobility,
|
||||
@@ -3632,7 +3636,7 @@ class FalukantService extends BaseService {
|
||||
RelationshipType.findAll({ where: { id: typeIds }, attributes: ['id', 'tr'] }),
|
||||
FalukantCharacter.findAll({
|
||||
where: { id: char2Ids },
|
||||
attributes: ['id', 'birthdate', 'gender', 'moodId', 'pubertyStartAge', 'fertileFromAge'],
|
||||
attributes: ['id', 'birthdate', 'gender', 'moodId', 'pubertyStartAge', 'fertileFromAge', 'hairColor', 'eyeColor'],
|
||||
include: [
|
||||
{ model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] },
|
||||
{ model: TitleOfNobility, as: 'nobleTitle', attributes: ['labelTr'] },
|
||||
@@ -3675,6 +3679,8 @@ class FalukantService extends BaseService {
|
||||
age: calcAge(c2.birthdate),
|
||||
development: getCharacterDevelopment(c2, calcAge(c2.birthdate)),
|
||||
gender: c2.gender,
|
||||
hairColor: c2.hairColor ?? null,
|
||||
eyeColor: c2.eyeColor ?? null,
|
||||
firstName: c2.definedFirstName?.name || 'Unknown',
|
||||
nobleTitle: c2.nobleTitle?.labelTr || '',
|
||||
mood: c2.mood,
|
||||
@@ -3720,7 +3726,7 @@ class FalukantService extends BaseService {
|
||||
const childChars = childCharIds.length
|
||||
? await FalukantCharacter.findAll({
|
||||
where: { id: childCharIds },
|
||||
attributes: ['id', 'birthdate', 'gender', 'regionId', 'titleOfNobility', 'pubertyStartAge', 'fertileFromAge'],
|
||||
attributes: ['id', 'birthdate', 'gender', 'regionId', 'titleOfNobility', 'pubertyStartAge', 'fertileFromAge', 'hairColor', 'eyeColor'],
|
||||
include: [{ model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] }]
|
||||
})
|
||||
: [];
|
||||
@@ -3781,6 +3787,8 @@ class FalukantService extends BaseService {
|
||||
childCharacterId: rel.childCharacterId,
|
||||
name: kid?.definedFirstName?.name || 'Unknown',
|
||||
gender: kid?.gender,
|
||||
hairColor: kid?.hairColor ?? null,
|
||||
eyeColor: kid?.eyeColor ?? null,
|
||||
age: kid?.birthdate ? calcAge(kid.birthdate) : null,
|
||||
development: getCharacterDevelopment(kid, kid?.birthdate ? calcAge(kid.birthdate) : null),
|
||||
hasName: rel.nameSet,
|
||||
@@ -3857,6 +3865,8 @@ class FalukantService extends BaseService {
|
||||
name: partner.firstName || 'Unknown',
|
||||
age: partner.age != null ? partner.age : null,
|
||||
gender: partner.gender || null,
|
||||
hairColor: partner.hairColor ?? null,
|
||||
eyeColor: partner.eyeColor ?? null,
|
||||
title: partner.nobleTitle || '',
|
||||
role: state.loverRole || 'lover',
|
||||
affection: state.affection,
|
||||
@@ -4451,7 +4461,7 @@ class FalukantService extends BaseService {
|
||||
const loadCandidates = async (where) => FalukantCharacter.findAll({
|
||||
where,
|
||||
include: includes,
|
||||
attributes: ['id', 'birthdate', 'gender'],
|
||||
attributes: ['id', 'birthdate', 'gender', 'hairColor', 'eyeColor'],
|
||||
order: sequelize.random(),
|
||||
limit: 10
|
||||
});
|
||||
@@ -4522,7 +4532,7 @@ class FalukantService extends BaseService {
|
||||
{
|
||||
model: FalukantCharacter,
|
||||
as: 'proposedCharacter',
|
||||
attributes: ['id', 'firstName', 'lastName', 'gender', 'regionId', 'birthdate'],
|
||||
attributes: ['id', 'firstName', 'lastName', 'gender', 'regionId', 'birthdate', 'hairColor', 'eyeColor'],
|
||||
include: [
|
||||
{ model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] },
|
||||
{ model: FalukantPredefineLastname, as: 'definedLastName', attributes: ['name'] },
|
||||
@@ -5774,6 +5784,8 @@ class FalukantService extends BaseService {
|
||||
return {
|
||||
id: child.child.id,
|
||||
gender: child.child.gender,
|
||||
hairColor: child.child.hairColor ?? null,
|
||||
eyeColor: child.child.eyeColor ?? null,
|
||||
age: calcAge(child.child.birthdate),
|
||||
proposedFirstName: child.child.definedFirstName.name,
|
||||
};
|
||||
@@ -6824,7 +6836,7 @@ class FalukantService extends BaseService {
|
||||
{
|
||||
model: FalukantCharacter,
|
||||
as: 'holder',
|
||||
attributes: ['id', 'gender', 'birthdate'],
|
||||
attributes: ['id', 'gender', 'birthdate', 'hairColor', 'eyeColor'],
|
||||
include: [
|
||||
{
|
||||
model: FalukantPredefineFirstname,
|
||||
@@ -6906,6 +6918,8 @@ class FalukantService extends BaseService {
|
||||
definedLastName: o.holder.definedLastName,
|
||||
nobleTitle: o.holder.nobleTitle,
|
||||
gender: o.holder.gender,
|
||||
hairColor: o.holder.hairColor ?? null,
|
||||
eyeColor: o.holder.eyeColor ?? null,
|
||||
age: o.holder.birthdate ? calcAge(o.holder.birthdate) : null
|
||||
}
|
||||
: null,
|
||||
@@ -6975,7 +6989,7 @@ class FalukantService extends BaseService {
|
||||
include: [{
|
||||
model: FalukantCharacter,
|
||||
as: 'character',
|
||||
attributes: ['birthdate', 'gender'],
|
||||
attributes: ['birthdate', 'gender', 'hairColor', 'eyeColor'],
|
||||
include: [
|
||||
{ model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] },
|
||||
{ model: FalukantPredefineLastname, as: 'definedLastName', attributes: ['name'] },
|
||||
@@ -7890,7 +7904,7 @@ ORDER BY r.id`,
|
||||
{
|
||||
model: FalukantCharacter,
|
||||
as: 'character',
|
||||
attributes: ['id', 'birthdate', 'gender'],
|
||||
attributes: ['id', 'birthdate', 'gender', 'hairColor', 'eyeColor'],
|
||||
include: [
|
||||
{ model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] },
|
||||
{ model: FalukantPredefineLastname, as: 'definedLastName', attributes: ['name'] },
|
||||
@@ -7929,6 +7943,8 @@ ORDER BY r.id`,
|
||||
titleLabelTr: titleLabelTr || null,
|
||||
nameWithoutTitle,
|
||||
gender: character.gender ?? null,
|
||||
hairColor: character.hairColor ?? null,
|
||||
eyeColor: character.eyeColor ?? null,
|
||||
age,
|
||||
money: Number(falukantUser.money ?? 0),
|
||||
unreadNotificationsCount,
|
||||
@@ -8404,7 +8420,7 @@ ORDER BY r.id`,
|
||||
{
|
||||
model: FalukantCharacter,
|
||||
as: 'holder',
|
||||
attributes: ['id', 'gender', 'birthdate'],
|
||||
attributes: ['id', 'gender', 'birthdate', 'hairColor', 'eyeColor'],
|
||||
required: false,
|
||||
include: [
|
||||
{
|
||||
@@ -8470,6 +8486,8 @@ ORDER BY r.id`,
|
||||
id: o.holder.id,
|
||||
name: `${o.holder.definedFirstName?.name || ''} ${o.holder.definedLastName?.name || ''}`.trim(),
|
||||
gender: o.holder.gender,
|
||||
hairColor: o.holder.hairColor ?? null,
|
||||
eyeColor: o.holder.eyeColor ?? null,
|
||||
title: o.holder.nobleTitle?.labelTr,
|
||||
age: o.holder.birthdate ? calcAge(o.holder.birthdate) : null
|
||||
}
|
||||
|
||||
81
backend/tools/characterAppearance.test.mjs
Normal file
81
backend/tools/characterAppearance.test.mjs
Normal file
@@ -0,0 +1,81 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { Sequelize, DataTypes } from 'sequelize';
|
||||
import migration from '../migrations-active/20261104000001-add-character-appearance.cjs';
|
||||
import developmentMigration from '../migrations-active/20261104000000-add-character-development-ages.cjs';
|
||||
|
||||
// Only the explicitly supplied disposable database may be changed.
|
||||
test('appearance migration persists colors for existing characters, Sequelize and raw daemon inserts', {
|
||||
skip: !process.env.TEST_DATABASE_URL,
|
||||
}, async () => {
|
||||
const db = new Sequelize(process.env.TEST_DATABASE_URL, { logging: false });
|
||||
try {
|
||||
const [[{ name }]] = await db.query('SELECT current_database() AS name');
|
||||
assert.equal(name, 'yourpart_development_test', 'Use the isolated development test database');
|
||||
await db.query(`
|
||||
DROP SCHEMA IF EXISTS falukant_data CASCADE;
|
||||
CREATE SCHEMA falukant_data;
|
||||
CREATE TABLE falukant_data.character (
|
||||
id SERIAL PRIMARY KEY, gender TEXT, health INT DEFAULT 100,
|
||||
hair_color VARCHAR(7), updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
INSERT INTO falukant_data.character (gender, hair_color)
|
||||
VALUES ('female', '#AbC123'), ('male', NULL);
|
||||
`);
|
||||
const qi = db.getQueryInterface();
|
||||
await developmentMigration.up(qi);
|
||||
const [developmentBefore] = await db.query('SELECT id, health, puberty_start_age, fertile_from_age, updated_at FROM falukant_data.character ORDER BY id');
|
||||
await migration.up(qi);
|
||||
const [developmentAfter] = await db.query('SELECT id, health, puberty_start_age, fertile_from_age, updated_at FROM falukant_data.character ORDER BY id');
|
||||
assert.deepEqual(developmentAfter, developmentBefore, 'Existing character development remains intact');
|
||||
const [before] = await db.query('SELECT * FROM falukant_data.character ORDER BY id');
|
||||
assert.equal(before[0].hair_color, '#AbC123', 'Keep manually prepared values');
|
||||
for (const row of before) {
|
||||
assert.match(row.hair_color, /^#[0-9a-fA-F]{6}$/);
|
||||
assert.match(row.eye_color, /^#[0-9a-fA-F]{6}$/);
|
||||
}
|
||||
await migration.up(qi);
|
||||
await db.query(`UPDATE falukant_data.character SET hair_color = NULL, eye_color = NULL`);
|
||||
const [after] = await db.query('SELECT * FROM falukant_data.character ORDER BY id');
|
||||
assert.deepEqual(after, before, 'Repeated migrations and omitted colors keep existing assignments');
|
||||
|
||||
// NPCs and newborns created with raw Rust SQL also get stable colors.
|
||||
const [generated] = await db.query(`INSERT INTO falukant_data.character (gender)
|
||||
SELECT CASE WHEN i % 2 = 0 THEN 'female' ELSE 'male' END
|
||||
FROM generate_series(1, 1000) i RETURNING hair_color, eye_color`);
|
||||
assert.equal(new Set(generated.map(c => c.hair_color)).size, 7);
|
||||
assert.equal(new Set(generated.map(c => c.eye_color)).size, 6);
|
||||
assert.ok(generated.every(c => /^#[0-9a-fA-F]{6}$/.test(c.hair_color) && /^#[0-9a-fA-F]{6}$/.test(c.eye_color)));
|
||||
|
||||
const Character = db.define('AppearanceTestCharacter', {
|
||||
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
|
||||
gender: DataTypes.STRING,
|
||||
hairColor: { type: DataTypes.STRING(7), field: 'hair_color' },
|
||||
eyeColor: { type: DataTypes.STRING(7), field: 'eye_color' },
|
||||
}, { schema: 'falukant_data', tableName: 'character', timestamps: false });
|
||||
const character = await Character.create({ gender: 'female', eyeColor: '#123456' });
|
||||
assert.match(character.hairColor, /^#[0-9a-fA-F]{6}$/);
|
||||
assert.equal(character.eyeColor, '#123456');
|
||||
const assigned = character.hairColor;
|
||||
await character.update({ gender: 'male' });
|
||||
await character.reload();
|
||||
assert.equal(character.hairColor, assigned);
|
||||
await character.update({ hairColor: '#654321' });
|
||||
await character.reload();
|
||||
assert.equal(character.hairColor, '#654321');
|
||||
assert.equal(character.eyeColor, '#123456');
|
||||
for (const color of ['red', '#fff', '#12345z', '']) {
|
||||
await assert.rejects(db.query('UPDATE falukant_data.character SET eye_color = :color WHERE id = 1', {
|
||||
replacements: { color },
|
||||
}));
|
||||
}
|
||||
|
||||
const [[{ count: oldCount }]] = await db.query('SELECT count(*)::int FROM falukant_data.character');
|
||||
await migration.down(qi);
|
||||
const [[{ count: newCount }]] = await db.query('SELECT count(*)::int FROM falukant_data.character');
|
||||
assert.equal(newCount, oldCount, 'Rollback retains characters');
|
||||
await migration.up(qi);
|
||||
} finally {
|
||||
await db.close();
|
||||
}
|
||||
});
|
||||
@@ -42,7 +42,12 @@ da solche Dateien vom Quellmodell-Build übersprungen werden.
|
||||
|
||||
Unmarkierte Modelle behalten ihr bisheriges Aussehen. Die vollständige
|
||||
Markierung aller Modellvarianten ist eine einmalige Aufgabe je Modell. Es gibt
|
||||
keine neue Datenbankmigration oder Speicherung pro Spielcharakter.
|
||||
keine separate Speicherung der Markierungen pro Spielcharakter.
|
||||
|
||||
Die Charakterfarben selbst werden inzwischen als `hairColor` und `eyeColor`
|
||||
in den Charakterdaten gespeichert und an die Spielansichten weitergegeben.
|
||||
Siehe `backend/docs/falukant-character-appearance.md` für Migration und
|
||||
Initialisierung. Der Editor ändert weiterhin nur Modelle und Vorschaufarben.
|
||||
|
||||
## Schnittstelle für die Anzeige
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
<div class="director-main">
|
||||
<div class="director-visual" v-if="director.character">
|
||||
<FalukantCharacterVisual
|
||||
:appearance="director.character"
|
||||
:gender="director.character.gender"
|
||||
:age="director.character.age"
|
||||
:visual-settings="visualSettings"
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
<td>
|
||||
<div class="church-person-cell">
|
||||
<FalukantCharacterVisual
|
||||
:appearance="person"
|
||||
:gender="person.gender"
|
||||
:age="person.age"
|
||||
:visual-settings="visualSettings"
|
||||
@@ -91,6 +92,7 @@
|
||||
<span v-if="pos.character">
|
||||
<div class="church-person-cell">
|
||||
<FalukantCharacterVisual
|
||||
:appearance="pos.character"
|
||||
:gender="pos.character.gender"
|
||||
:age="pos.character.age"
|
||||
:visual-settings="visualSettings"
|
||||
|
||||
@@ -156,6 +156,7 @@
|
||||
></div>
|
||||
<div v-if="showCharacterFigure3d" class="character-3d-frame character-3d-frame--spouse">
|
||||
<Character3D
|
||||
:appearance="relationships[0].character2"
|
||||
:gender="relationships[0].character2.gender"
|
||||
:age="relationships[0].character2.age"
|
||||
/>
|
||||
@@ -341,6 +342,7 @@
|
||||
:style="getChild3dBackgroundStyle(child)"
|
||||
>
|
||||
<Character3D
|
||||
:appearance="child"
|
||||
:gender="child.gender"
|
||||
:age="child.age"
|
||||
:lightweight="true"
|
||||
@@ -452,6 +454,7 @@
|
||||
></div>
|
||||
<div v-if="showCharacterFigure3d" class="character-3d-frame character-3d-frame--compact">
|
||||
<Character3D
|
||||
:appearance="lover"
|
||||
:gender="lover.gender"
|
||||
:age="lover.age"
|
||||
:lightweight="true"
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
<div class="politics-card__meta">
|
||||
<div v-if="pos.character" class="politics-card__visual">
|
||||
<FalukantCharacterVisual
|
||||
:appearance="pos.character"
|
||||
:gender="pos.character.gender"
|
||||
:age="pos.character.age"
|
||||
:visual-settings="visualSettings"
|
||||
|
||||
Reference in New Issue
Block a user