473 lines
16 KiB
Svelte
473 lines
16 KiB
Svelte
<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> |