Files
shadowrun-server/frontend/src/lib/shadorwun/character.svelte
2025-12-10 22:54:41 +01:00

473 lines
16 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts">
import { onMount } from 'svelte';
import { API_BASE } from '$lib/config';
import { Defaults } from './defaults.svelte';
import type { Skill, Connection} from './types.svelte';
import { autoGrow } from '$lib/common/autogrow';
export let currentCharacter: any;
export let currentCharacterData: any;
let characterData = currentCharacterData ?? {};
// Setup the character with default values if data is not existant
characterData.Info ??= Defaults.Info
characterData.Attributes ??= Defaults.Attributes
characterData.Skills ??= [];
characterData.Connections ??= [];
characterData.RangedWeapons ??= [];
characterData.MeleeWeapons ??= [];
characterData.Armor ??= [];
characterData.Cyberware ??= [];
characterData.Bioware ??= [];
characterData.PositiveQualities ??= [];
characterData.NegativeQualities ??= [];
characterData.PysicalCondition ??= Defaults.PysicalCondition
characterData.StunCondition ??= Defaults.StunCondition
characterData.Notes ??= ""
const characterInfoTypes = {
Metatype: "text",
Age: "number",
Sex: "text",
Nuyen: "number",
Lifestyle: "text",
"Total Karma": "number",
"C. Karma": "number",
"Street Cred": "number",
Notoriety: "number",
Fame: "number"
};
function add(key : keyof typeof Defaults) {
const copy = { ...Defaults[key] }; // OBS not shallow copy
characterData[key] = [...characterData[key], copy];
}
function remove(key : keyof typeof characterData, index: number) {
characterData[key] = characterData[key].filter((_: any, i: number) => i !== index);
}
function viewPage(pageNumber: number) {
if (pageNumber > 0) {
// Open in new tab
window.open(`${API_BASE}/assets/shadowrun/Shadowrun-4E-Corebook-p${pageNumber}.pdf`, "_blank");
}
}
async function saveCharacterData() {
const res = await fetch(`${API_BASE}/api/shadowrun/characters_data/${currentCharacter.id}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(characterData)
});
}
// Inventory
let inventory = currentCharacter?.inventory || [];
</script>
<h1>Name: {currentCharacter.name}</h1>
<h2>Character Info</h2>
<div class="info-container">
{#each Object.entries(characterData.Info) as [key, value], i}
<div class="input-row">
<label for={"field-" + i}>{key}</label>
<input
id={"field-" + i}
type={characterInfoTypes[key]}
bind:value={characterData.Info[key]}
min={characterInfoTypes[key] === "number" ? 0 : null}
max={characterInfoTypes[key] === "number" ? 100 : null} />
</div>
{/each}
</div>
<h2>Attributes</h2>
<div class="info-container">
<table>
<tbody>
<tr>
<td>
<label for={"field-Agility"}>Agility</label>
<input id={"field-Agility"} type="number" bind:value={characterData.Attributes.Agility} min=0 max=100/>
</td>
<td>
<label for={"field-Body"}>Body</label>
<input id={"field-Body"} type="number" bind:value={characterData.Attributes.Body} min=0 max=100/>
</td>
<td>
<label for={"field-Charisma"}>Charisma</label>
<input id={"field-Charisma"} type="number" bind:value={characterData.Attributes.Charisma} min=0 max=100/>
</td>
<td>
<label for={"field-Edge"}>Edge</label>
<input id={"field-Edge"} type="number" bind:value={characterData.Attributes.Edge} min=0 max=100/>
</td>
</tr>
<tr>
<td>
<label for={"field-Essence"}>Essence</label>
<input id={"field-Essence"} type="number" bind:value={characterData.Attributes.Essence} min=0 max=100/>
</td>
<td>
<label for={"field-Initiative"}>Initiative</label>
<input id={"field-Initiative"} type="number" bind:value={characterData.Attributes.Initiative} min=0 max=100/>
</td>
<td>
<label for={"field-Charisma"}>Charisma</label>
<input id={"field-Charisma"} type="number" bind:value={characterData.Attributes.Charisma} min=0 max=100/>
</td>
<td>
<label for={"field-Edge"}>Edge</label>
<input id={"field-Edge"} type="number" bind:value={characterData.Attributes.Edge} min=0 max=100/>
</td>
</tr>
</tbody>
</table>
</div>
<h2>Skills</h2>
<table>
<thead>
<tr>
<th>Name</th>
<th>Rating</th>
<th>Attribute</th>
<th>Dice</th>
<th>Page</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
{#each characterData.Skills as skill, i}
<tr>
<td><textarea use:autoGrow class="input-height" rows="1" bind:value={characterData.Skills[i].Name}></textarea></td>
<td><input type="number" min=0 max=100 bind:value={characterData.Skills[i].Rating} /></td>
<td><select bind:value={characterData.Skills[i].Attribute}>
{#each Object.keys(Defaults.Attributes) as attr}
<option value={attr}>{attr}</option>
{/each}
</select></td>
<td><span>{skill.Rating + characterData.Attributes[skill.Attribute]}</span></td>
<td><input type="number" min=1 max=354 bind:value={characterData.Skills[i].Page} /></td>
<td><button on:click={() => viewPage(characterData.Skills[i].Page)}>View</button></td>
<td><button class="red-button" on:click={() => remove("Skills", i)}>X</button></td>
</tr>
{/each}
</tbody>
</table>
<button on:click={() => add("Skills")}>+</button>
<h2>Contacts</h2>
<table>
<thead>
<tr>
<th>Name</th>
<th>Loyalty</th>
<th>Connection</th>
<th></th>
</tr>
</thead>
<tbody>
{#each characterData.Connections as connection, i}
<tr>
<td><textarea use:autoGrow class="input-height" rows="1" bind:value={characterData.Connections[i].Name}></textarea></td>
<td><input type="number" min=0 max=100 bind:value={characterData.Connections[i].Loyalty} /></td>
<td><input type="number" min=0 max=100 bind:value={characterData.Connections[i].Connection} /></td>
<td><button class="red-button" on:click={() => remove("Connections", i)}>X</button></td>
</tr>
{/each}
</tbody>
</table>
<button on:click={() => add("Connections")}>+</button>
<h2>Ranged Weapons</h2>
<table>
<thead>
<tr>
<th>Weapon</th>
<th>Damage</th>
<th>Type</th>
<th>AP</th>
<th>Mode</th>
<th>RC</th>
<th>Ammo</th>
<th>Availabiliy</th>
<th>Page</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
{#each characterData.RangedWeapons as weapon, i}
<tr>
<td><textarea use:autoGrow class="input-height" rows="1" bind:value={characterData.RangedWeapons[i].Weapon}></textarea></td>
<td><input type="number" min=0 max=100 bind:value={characterData.RangedWeapons[i].Damage} /></td>
<td><textarea use:autoGrow class="input-height" style="width: 5em;" rows="1" bind:value={characterData.RangedWeapons[i].Type}></textarea></td>
<td><input type="number" min=0 max=100 bind:value={characterData.RangedWeapons[i].AP} /></td>
<td><textarea use:autoGrow class="input-height" style="width: 3em;" rows="1" bind:value={characterData.RangedWeapons[i].Mode}></textarea></td>
<td><input type="number" min=0 max=100 bind:value={characterData.RangedWeapons[i].RC} /></td>
<td><input type="number" min=0 max=9999 bind:value={characterData.RangedWeapons[i].Ammo} /></td>
<td><textarea use:autoGrow class="input-height" style="width: 3em;" rows="1" bind:value={characterData.RangedWeapons[i].Availabiliy}></textarea></td>
<td><input type="number" min=1 max=354 bind:value={characterData.Connections[i].Page} /></td>
<td><button on:click={() => viewPage(characterData.Connections[i].Page)}>View</button></td>
<td><button class="red-button" on:click={() => remove("RangedWeapons", i)}>X</button></td>
</tr>
{/each}
</tbody>
</table>
<button on:click={() => add("RangedWeapons")}>+</button>
<h2>Melee Weapons</h2>
<table>
<thead>
<tr>
<th>Weapon</th>
<th>Reach</th>
<th>Damage</th>
<th>Type</th>
<th>Multiplier</th>
<th>Cal.Dmg</th>
<th>AP</th>
<th>Page</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
{#each characterData.MeleeWeapons as weapon, i}
<tr>
<td><input placeholder="Weapon" bind:value={characterData.MeleeWeapons[i].Weapon} /></td>
<td><input type="number" min=0 max=100 bind:value={characterData.MeleeWeapons[i].Reach} /></td>
<td><input type="number" min=0 max=100 bind:value={characterData.MeleeWeapons[i].Damage} /></td>
<td><input placeholder="Type" bind:value={characterData.MeleeWeapons[i].Type} /></td>
<td><input type="number" min=0 max=999 bind:value={characterData.MeleeWeapons[i]["Strength Multiplier"]} /></td>
<td><span> {(weapon["Strength Multiplier"] * characterData.Attributes.Strength) + weapon.Damage }</span></td>
<td><input type="number" min=0 max=100 bind:value={characterData.MeleeWeapons[i].AP} /></td>
<td><input type="number" min=1 max=354 bind:value={characterData.Connections[i].Page} /></td>
<td><button on:click={() => viewPage(characterData.Connections[i].Page)}>View</button></td>
<td><button class="red-button" on:click={() => remove("MeleeWeapons", i)}>X</button></td>
</tr>
{/each}
</tbody>
</table>
<button on:click={() => add("MeleeWeapons")}>+</button>
<h2>Armor</h2>
<table>
<thead>
<tr>
<th>Armor</th>
<th>Ballistic</th>
<th>Impact</th>
<th>Page</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
{#each characterData.Armor as armor, i}
<tr>
<td><input placeholder="Armor" bind:value={characterData.Armor[i].Armor} /></td>
<td><input type="number" min=0 max=100 bind:value={characterData.Armor[i].Ballistic} /></td>
<td><input type="number" min=0 max=100 bind:value={characterData.Armor[i].Impact} /></td>
<td><input type="number" min=1 max=354 bind:value={characterData.Armor[i].Page} /></td>
<td><button on:click={() => viewPage(characterData.Armor[i].Page)}>View</button></td>
<td><button class="red-button" on:click={() => remove("Armor", i)}>X</button></td>
</tr>
{/each}
</tbody>
</table>
<button on:click={() => add("Armor")}>+</button>
<h2>Cyberware</h2>
<table>
<thead>
<tr>
<th>Cyberware</th>
<th>Rating</th>
<th>Essence</th>
<th>Notes</th>
<th>Page</th>
<th></th>
<th></th> <!-- for remove button -->
</tr>
</thead>
<tbody>
{#each characterData.Cyberware as Cyberware, i}
<tr>
<td><input placeholder="Cyberware" bind:value={characterData.Cyberware[i].Implant} /></td>
<td><input type="number" min=0 max=100 bind:value={characterData.Cyberware[i].Rating} /></td>
<td><input type="number" min=0 max=100 bind:value={characterData.Cyberware[i].Essence} /></td>
<td><input placeholder="Notes" bind:value={characterData.Cyberware[i].Notes} /></td>
<td><input type="number" min=1 max=354 bind:value={characterData.Connections[i].Page} /></td>
<td><button on:click={() => viewPage(characterData.Connections[i].Page)}>View</button></td>
<td><button class="red-button" on:click={() => remove("Cyberware", i)}>X</button></td>
</tr>
{/each}
</tbody>
</table>
<button on:click={() => add("Cyberware")}>+</button>
<h2>Bioware</h2>
<table>
<thead>
<tr>
<th>Bioware</th>
<th>Rating</th>
<th>Essence</th>
<th>Notes</th>
<th>Page</th>
<th></th>
<th></th> <!-- for remove button -->
</tr>
</thead>
<tbody>
{#each characterData.Bioware as Bioware, i}
<tr>
<td><input placeholder="Bioware" bind:value={characterData.Bioware[i].Implant} /></td>
<td><input type="number" min="0" bind:value={characterData.Bioware[i].Rating} /></td>
<td><input type="number" min="0" bind:value={characterData.Bioware[i].Essence} /></td>
<td><input placeholder="Notes" bind:value={characterData.Bioware[i].Notes} /></td>
<td><input type="number" min=1 max=354 bind:value={characterData.Connections[i].Page} /></td>
<td><button on:click={() => viewPage(characterData.Connections[i].Page)}>View</button></td>
<td><button class="red-button" on:click={() => remove("Bioware", i)}>X</button></td>
</tr>
{/each}
</tbody>
</table>
<button on:click={() => add("Bioware")}>+</button>
<h2>Qualities</h2>
<h3>Positive</h3>
<table>
<thead>
<tr>
<th>Quality</th>
<th>Page</th>
<th></th>
<th></th> <!-- for remove button -->
</tr>
</thead>
<tbody>
{#each characterData.PositiveQualities as qualitiy, i}
<tr>
<td><input placeholder="Qualitiy" bind:value={characterData.PositiveQualities[i].Qualitiy} /></td>
<td><input type="number" min=1 max=354 bind:value={characterData.PositiveQualities[i].Page} /></td>
<td><button on:click={() => viewPage(characterData.PositiveQualities[i].Page)}>View</button></td>
<td><button class="red-button" on:click={() => remove("PositiveQualities", i)}>X</button></td>
</tr>
{/each}
</tbody>
</table>
<button on:click={() => add("PositiveQualities")}>+</button>
<h3>Negative</h3>
<table>
<thead>
<tr>
<th>Quality</th>
<th>Page</th>
<th></th>
<th></th> <!-- for remove button -->
</tr>
</thead>
<tbody>
{#each characterData.NegativeQualities as qualitiy, i}
<tr>
<td><input placeholder="Qualitiy" bind:value={characterData.NegativeQualities[i].Qualitiy} /></td>
<td><input type="number" min=1 max=354 bind:value={characterData.NegativeQualities[i].Page} /></td>
<td><button on:click={() => viewPage(characterData.NegativeQualities[i].Page)}>View</button></td>
<td><button class="red-button" on:click={() => remove("NegativeQualities", i)}>X</button></td>
</tr>
{/each}
</tbody>
</table>
<button on:click={() => add("NegativeQualities")}>+</button>
<h2>Pysical Condition</h2>
<table>
<tbody>
{#each characterData.PysicalCondition as row, rowIndex}
<tr>
{#each row as cell, colIndex}
<td>
<input
type="checkbox"
bind:checked={characterData.PysicalCondition[rowIndex][colIndex]}
/>
</td>
{/each}
</tr>
{/each}
</tbody>
</table>
<h2>Stun Condition</h2>
<table>
<tbody>
{#each characterData.StunCondition as row, rowIndex}
<tr>
{#each row as cell, colIndex}
<td>
<input
type="checkbox"
bind:checked={characterData.StunCondition[rowIndex][colIndex]}
/>
</td>
{/each}
</tr>
{/each}
</tbody>
</table>
<h2>Notes</h2>
<div>
<textarea
id="notes"
bind:value={characterData.Notes}
rows="10"
cols="100"
placeholder="Write your character notes here..."
></textarea>
</div>
<button on:click={saveCharacterData}>Save</button>
<style>
.info-container {
display: flex;
flex-wrap: wrap; /* allows items to go to next line if needed */
gap: 0.5em; /* space between each input-row */
}
.red-button {
border: none; /* optional: remove default border */
cursor: pointer; /* optional: pointer cursor */
}
.input-height {
border: none; /* optional: remove default border */
font: inherit; /* copies all font-related properties from the parent/input styling */
height: auto; /* allows autoGrow action to work */
min-height: 0;
/* resize: none; */
overflow: hidden; /* Allow autoGrow to work clean */
width: 10em; /* 15 × current font size */
}
.input-row {
display: flex;
align-items: center;
gap: 0.5em; /* space between label and input */
}
th {
text-align: left;
}
</style>