diff --git a/proto/cofd_api.proto b/proto/cofd_api.proto index 83fc8cd..3c00711 100644 --- a/proto/cofd_api.proto +++ b/proto/cofd_api.proto @@ -64,6 +64,13 @@ message UpdateMeritsRequest { repeated CofdSheet.Merit merits = 2; } +//Update all items on the character sheet by overwriting them. +//Primarily for the web UI. +message UpdateItemsRequest { + CharacterIdentifier character = 1; + repeated CofdSheet.Item items = 2; +} + //Add a Condition to a Chronicles of Darkness character sheet. message AddConditionRequest { string character_username = 1; diff --git a/src/frontend/_proto/cofd_api_pb.d.ts b/src/frontend/_proto/cofd_api_pb.d.ts index 26b5662..7ba4dd1 100644 --- a/src/frontend/_proto/cofd_api_pb.d.ts +++ b/src/frontend/_proto/cofd_api_pb.d.ts @@ -236,6 +236,34 @@ export namespace UpdateMeritsRequest { } } +export class UpdateItemsRequest extends jspb.Message { + hasCharacter(): boolean; + clearCharacter(): void; + getCharacter(): CharacterIdentifier | undefined; + setCharacter(value?: CharacterIdentifier): void; + + clearItemsList(): void; + getItemsList(): Array; + setItemsList(value: Array): void; + addItems(value?: cofd_pb.CofdSheet.Item, index?: number): cofd_pb.CofdSheet.Item; + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): UpdateItemsRequest.AsObject; + static toObject(includeInstance: boolean, msg: UpdateItemsRequest): UpdateItemsRequest.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: UpdateItemsRequest, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): UpdateItemsRequest; + static deserializeBinaryFromReader(message: UpdateItemsRequest, reader: jspb.BinaryReader): UpdateItemsRequest; +} + +export namespace UpdateItemsRequest { + export type AsObject = { + character?: CharacterIdentifier.AsObject, + itemsList: Array, + } +} + export class AddConditionRequest extends jspb.Message { getCharacterUsername(): string; setCharacterUsername(value: string): void; diff --git a/src/frontend/_proto/cofd_api_pb.js b/src/frontend/_proto/cofd_api_pb.js index 0011a8f..1ca7780 100644 --- a/src/frontend/_proto/cofd_api_pb.js +++ b/src/frontend/_proto/cofd_api_pb.js @@ -22,6 +22,7 @@ goog.exportSymbol('proto.models.proto.cofd.api.CharacterIdentifier', null, globa goog.exportSymbol('proto.models.proto.cofd.api.RemoveConditionRequest', null, global); goog.exportSymbol('proto.models.proto.cofd.api.UpdateAttributeRequest', null, global); goog.exportSymbol('proto.models.proto.cofd.api.UpdateBasicInfoRequest', null, global); +goog.exportSymbol('proto.models.proto.cofd.api.UpdateItemsRequest', null, global); goog.exportSymbol('proto.models.proto.cofd.api.UpdateMeritsRequest', null, global); goog.exportSymbol('proto.models.proto.cofd.api.UpdateSkillRequest', null, global); goog.exportSymbol('proto.models.proto.cofd.api.UpdateSkillSpecializationsRequest', null, global); @@ -194,6 +195,27 @@ if (goog.DEBUG && !COMPILED) { */ proto.models.proto.cofd.api.UpdateMeritsRequest.displayName = 'proto.models.proto.cofd.api.UpdateMeritsRequest'; } +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.models.proto.cofd.api.UpdateItemsRequest = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, proto.models.proto.cofd.api.UpdateItemsRequest.repeatedFields_, null); +}; +goog.inherits(proto.models.proto.cofd.api.UpdateItemsRequest, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.models.proto.cofd.api.UpdateItemsRequest.displayName = 'proto.models.proto.cofd.api.UpdateItemsRequest'; +} /** * Generated by JsPbCodeGenerator. * @param {Array=} opt_data Optional initial data array, typically from a @@ -1879,6 +1901,217 @@ proto.models.proto.cofd.api.UpdateMeritsRequest.prototype.clearMeritsList = func +/** + * List of repeated fields within this message type. + * @private {!Array} + * @const + */ +proto.models.proto.cofd.api.UpdateItemsRequest.repeatedFields_ = [2]; + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.models.proto.cofd.api.UpdateItemsRequest.prototype.toObject = function(opt_includeInstance) { + return proto.models.proto.cofd.api.UpdateItemsRequest.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.models.proto.cofd.api.UpdateItemsRequest} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.models.proto.cofd.api.UpdateItemsRequest.toObject = function(includeInstance, msg) { + var f, obj = { + character: (f = msg.getCharacter()) && proto.models.proto.cofd.api.CharacterIdentifier.toObject(includeInstance, f), + itemsList: jspb.Message.toObjectList(msg.getItemsList(), + cofd_pb.CofdSheet.Item.toObject, includeInstance) + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.models.proto.cofd.api.UpdateItemsRequest} + */ +proto.models.proto.cofd.api.UpdateItemsRequest.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.models.proto.cofd.api.UpdateItemsRequest; + return proto.models.proto.cofd.api.UpdateItemsRequest.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.models.proto.cofd.api.UpdateItemsRequest} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.models.proto.cofd.api.UpdateItemsRequest} + */ +proto.models.proto.cofd.api.UpdateItemsRequest.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = new proto.models.proto.cofd.api.CharacterIdentifier; + reader.readMessage(value,proto.models.proto.cofd.api.CharacterIdentifier.deserializeBinaryFromReader); + msg.setCharacter(value); + break; + case 2: + var value = new cofd_pb.CofdSheet.Item; + reader.readMessage(value,cofd_pb.CofdSheet.Item.deserializeBinaryFromReader); + msg.addItems(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.models.proto.cofd.api.UpdateItemsRequest.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.models.proto.cofd.api.UpdateItemsRequest.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.models.proto.cofd.api.UpdateItemsRequest} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.models.proto.cofd.api.UpdateItemsRequest.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getCharacter(); + if (f != null) { + writer.writeMessage( + 1, + f, + proto.models.proto.cofd.api.CharacterIdentifier.serializeBinaryToWriter + ); + } + f = message.getItemsList(); + if (f.length > 0) { + writer.writeRepeatedMessage( + 2, + f, + cofd_pb.CofdSheet.Item.serializeBinaryToWriter + ); + } +}; + + +/** + * optional CharacterIdentifier character = 1; + * @return {?proto.models.proto.cofd.api.CharacterIdentifier} + */ +proto.models.proto.cofd.api.UpdateItemsRequest.prototype.getCharacter = function() { + return /** @type{?proto.models.proto.cofd.api.CharacterIdentifier} */ ( + jspb.Message.getWrapperField(this, proto.models.proto.cofd.api.CharacterIdentifier, 1)); +}; + + +/** + * @param {?proto.models.proto.cofd.api.CharacterIdentifier|undefined} value + * @return {!proto.models.proto.cofd.api.UpdateItemsRequest} returns this +*/ +proto.models.proto.cofd.api.UpdateItemsRequest.prototype.setCharacter = function(value) { + return jspb.Message.setWrapperField(this, 1, value); +}; + + +/** + * Clears the message field making it undefined. + * @return {!proto.models.proto.cofd.api.UpdateItemsRequest} returns this + */ +proto.models.proto.cofd.api.UpdateItemsRequest.prototype.clearCharacter = function() { + return this.setCharacter(undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.models.proto.cofd.api.UpdateItemsRequest.prototype.hasCharacter = function() { + return jspb.Message.getField(this, 1) != null; +}; + + +/** + * repeated models.proto.cofd.CofdSheet.Item items = 2; + * @return {!Array} + */ +proto.models.proto.cofd.api.UpdateItemsRequest.prototype.getItemsList = function() { + return /** @type{!Array} */ ( + jspb.Message.getRepeatedWrapperField(this, cofd_pb.CofdSheet.Item, 2)); +}; + + +/** + * @param {!Array} value + * @return {!proto.models.proto.cofd.api.UpdateItemsRequest} returns this +*/ +proto.models.proto.cofd.api.UpdateItemsRequest.prototype.setItemsList = function(value) { + return jspb.Message.setRepeatedWrapperField(this, 2, value); +}; + + +/** + * @param {!proto.models.proto.cofd.CofdSheet.Item=} opt_value + * @param {number=} opt_index + * @return {!proto.models.proto.cofd.CofdSheet.Item} + */ +proto.models.proto.cofd.api.UpdateItemsRequest.prototype.addItems = function(opt_value, opt_index) { + return jspb.Message.addToRepeatedWrapperField(this, 2, opt_value, proto.models.proto.cofd.CofdSheet.Item, opt_index); +}; + + +/** + * Clears the list making it empty but non-null. + * @return {!proto.models.proto.cofd.api.UpdateItemsRequest} returns this + */ +proto.models.proto.cofd.api.UpdateItemsRequest.prototype.clearItemsList = function() { + return this.setItemsList([]); +}; + + + if (jspb.Message.GENERATE_TO_OBJECT) { diff --git a/src/frontend/scripts/api.ts b/src/frontend/scripts/api.ts index 2b09154..f1480df 100644 --- a/src/frontend/scripts/api.ts +++ b/src/frontend/scripts/api.ts @@ -1,5 +1,5 @@ import * as jspb from "google-protobuf"; -import { ApiResult, UpdateBasicInfoRequest, UpdateAttributeRequest, UpdateSkillValueRequest, UpdateMeritsRequest } from "../_proto/cofd_api_pb"; +import { ApiResult, UpdateBasicInfoRequest, UpdateAttributeRequest, UpdateSkillValueRequest, UpdateMeritsRequest, UpdateItemsRequest } from "../_proto/cofd_api_pb"; const PROTOBUF_CONTENT_TYPE = { 'Content-Type': 'application/x-protobuf' }; @@ -33,3 +33,8 @@ export async function updateMerits(params: UpdateMeritsRequest): Promise { + let data = await makeRequest('/api/rpc/cofd/update_items', params); + return ApiResult.deserializeBinary(data); +} diff --git a/src/frontend/scripts/characters/edit-ui.ts b/src/frontend/scripts/characters/edit-ui.ts new file mode 100644 index 0000000..87e031d --- /dev/null +++ b/src/frontend/scripts/characters/edit-ui.ts @@ -0,0 +1,49 @@ +export function addMeritLine(parent: Element) { + const meritName = document.createElement('input'); + meritName.type = 'text'; + meritName.value = ''; + meritName.classList.add('merit-name'); + + const meritDots = document.createElement('input'); + meritDots.type = 'number'; + meritDots.min = '0'; + meritDots.value = '0'; + meritDots.classList.add('merit-dots'); + + const removeButton = document.createElement('button'); + removeButton.innerText = "X"; + removeButton.setAttribute('data-event-type', 'remove-merit'); + removeButton.classList.add('remove-merit'); + + const dots = document.createElement('span'); + dots.innerText = 'Dots'; + + const merit = document.createElement('div'); + merit.classList.add('merit'); + merit.appendChild(meritName); + merit.appendChild(meritDots); + merit.appendChild(dots); + merit.appendChild(removeButton); + + parent.appendChild(merit); +} + + +export function addItemLine(parent: Element) { + const itemName = document.createElement('input'); + itemName.type = 'text'; + itemName.value = ''; + itemName.classList.add('item-name'); + + const removeButton = document.createElement('button'); + removeButton.innerText = "X"; + removeButton.setAttribute('data-event-type', 'remove-item'); + removeButton.classList.add('remove-item'); + + const item = document.createElement('div'); + item.classList.add('item'); + item.appendChild(itemName); + item.appendChild(removeButton); + + parent.appendChild(item); +} diff --git a/src/frontend/scripts/characters/edit.ts b/src/frontend/scripts/characters/edit.ts index 8724694..0465edb 100644 --- a/src/frontend/scripts/characters/edit.ts +++ b/src/frontend/scripts/characters/edit.ts @@ -1,6 +1,7 @@ -import { CharacterIdentifier, UpdateBasicInfoRequest, UpdateSkillValueRequest, UpdateAttributeRequest, UpdateMeritsRequest } from "../../_proto/cofd_api_pb"; +import { CharacterIdentifier, UpdateBasicInfoRequest, UpdateSkillValueRequest, UpdateAttributeRequest, UpdateMeritsRequest, UpdateItemsRequest } from "../../_proto/cofd_api_pb"; import { CofdSheet } from "../../_proto/cofd_pb"; import * as api from "../api"; +import * as ui from "./edit-ui"; // This is the scripting for the edit character page, which submits // changes to the server as the user makes them. @@ -129,37 +130,7 @@ import * as api from "../api"; } } - function addMeritLine() { - const meritName = document.createElement('input'); - meritName.type = 'text'; - meritName.value = ''; - meritName.classList.add('merit-name'); - - const meritDots = document.createElement('input'); - meritDots.type = 'number'; - meritDots.min = '0'; - meritDots.value = '0'; - meritDots.classList.add('merit-dots'); - - const removeButton = document.createElement('button'); - removeButton.innerText = "X"; - removeButton.setAttribute('data-event-type', 'remove-merit'); - removeButton.classList.add('remove-merit'); - - const dots = document.createElement('span'); - dots.innerText = 'Dots'; - - const merit = document.createElement('div'); - merit.classList.add('merit'); - merit.appendChild(meritName); - merit.appendChild(meritDots); - merit.appendChild(dots); - merit.appendChild(removeButton); - - document.querySelector("#merit-list")?.appendChild(merit); - } - - async function processControlEvent(event: Event) { + async function processEvent(event: Event) { console.log('processing merit control event'); const element = event.target as Element; const eventType = element.getAttribute('data-event-type') ?? ''; @@ -167,23 +138,78 @@ import * as api from "../api"; if (eventType == 'remove-merit') { await removeMerit(event); } else if (eventType == 'add-merit') { - addMeritLine(); + const meritList = document.querySelector("#merit-list")!; + ui.addMeritLine(meritList); } } const meritControls = document.querySelector("#merit-controls"); - meritControls?.addEventListener('click', processControlEvent); + meritControls?.addEventListener('click', processEvent); const meritList = document.querySelector("#merit-list"); - meritList?.addEventListener('click', processControlEvent); + meritList?.addEventListener('click', processEvent); meritList?.addEventListener('blur', updateMerits); meritList?.addEventListener('change', updateMerits); } + function setupItems() { + async function updateItems() { + let meritElements = document.querySelectorAll('#items input[class="item-name"]'); + let items: CofdSheet.Item[] = + Array.from(meritElements).map(input => { + const item = new CofdSheet.Item(); + item.setName(input.value); + item.setDescription(""); //TODO add description + item.setRules(""); //TODO add rules + + return item; + }); + + console.log("items are", items); + const params = new UpdateItemsRequest(); + params.setCharacter(characterId()); + params.setItemsList(items); + + let resp = await api.updateItems(params); + console.log("got a response back", resp); + } + + async function removeItem(event: Event) { + const button = event.target as Option; + const itemLine = button?.parentElement; + if (itemLine) { + document.querySelector("#item-list")?.removeChild(itemLine); + await updateItems(); + } + } + + async function processEvent(event: Event) { + console.log('processing item control event'); + const element = event.target as Element; + const eventType = element.getAttribute('data-event-type') ?? ''; + + if (eventType == 'remove-item') { + await removeItem(event); + } else if (eventType == 'add-item') { + const itemList = document.querySelector("#item-list")!; + ui.addItemLine(itemList); + } + } + + const meritControls = document.querySelector("#item-controls"); + meritControls?.addEventListener('click', processEvent); + + const meritList = document.querySelector("#item-list"); + meritList?.addEventListener('click', processEvent); + meritList?.addEventListener('blur', updateItems); + meritList?.addEventListener('change', updateItems); + } + setupAttributes(); setupSkills(); setupBasicInfo(); setupMerits(); + setupItems(); })().catch(e => { alert(e); }); diff --git a/src/frontend/templates/characters/edit_character.html.tera b/src/frontend/templates/characters/edit_character.html.tera index ae24603..89228ea 100644 --- a/src/frontend/templates/characters/edit_character.html.tera +++ b/src/frontend/templates/characters/edit_character.html.tera @@ -108,16 +108,18 @@ margin: 0; } - #merits { + #merits, #items { padding: 4px; - } - - #merit-list { display: inline-flex; flex-direction: column; } - .merit { + #merit-list, #item-list { + display: inline-flex; + flex-direction: column; + } + + .merit, .item { margin: 0; padding: 0; display: inline-flex; @@ -125,7 +127,7 @@ margin-top: -1px; } - .merit .merit-name { + .merit .merit-name, .item .item-name { width: 10em; vertical-align: text-bottom; padding: 0; @@ -151,7 +153,7 @@ margin: 0; } - .merit .remove-merit { + .merit .remove-merit, .item .remove-item { max-width: 4em; padding: 0; padding: 3px; @@ -267,5 +269,17 @@ - + +
+

Inventory

+ +
+ {% for item in sheet.items %} + {{ macros::item(name=item.name) }} + {% endfor %} +
+
+ +
+
{% endblock content %} diff --git a/src/frontend/templates/characters/edit_character_macros.html.tera b/src/frontend/templates/characters/edit_character_macros.html.tera index 6d141d4..be14363 100644 --- a/src/frontend/templates/characters/edit_character_macros.html.tera +++ b/src/frontend/templates/characters/edit_character_macros.html.tera @@ -19,3 +19,11 @@ {% endmacro merit %} + + +{% macro item(name) %} +
+ + +
+{% endmacro item %} diff --git a/src/routes/api.rs b/src/routes/api.rs index 81f40f9..704fda1 100644 --- a/src/routes/api.rs +++ b/src/routes/api.rs @@ -12,6 +12,7 @@ pub(crate) fn routes() -> Vec { cofd::update_skill, cofd::update_skill_value, cofd::update_merits, + cofd::update_items, cofd::add_condition, cofd::remove_condition ] diff --git a/src/routes/api/cofd.rs b/src/routes/api/cofd.rs index cd3eed5..ad25ccf 100644 --- a/src/routes/api/cofd.rs +++ b/src/routes/api/cofd.rs @@ -187,6 +187,30 @@ pub(super) async fn update_merits<'a>( Ok(Proto(ApiResult::success())) } +#[post("/rpc/cofd/update_items", data = "")] +pub(super) async fn update_items<'a>( + req: Proto, + conn: TenebrousDbConn<'_>, + logged_in_user: Option<&User>, +) -> Result, Error> { + let mut character = load_character( + &conn, + logged_in_user, + &req.character.owner(), + req.character.id(), + ) + .await?; + + let mut sheet: CofdSheet = character.try_deserialize()?; + sheet.items = req.items.clone(); + + println!("updated items"); + + character.update_data(&sheet)?; + conn.update_character_sheet(&character).await?; + Ok(Proto(ApiResult::success())) +} + #[put("/rpc/cofd/add_condition", data = "")] pub(super) fn add_condition<'a>(info: Proto) -> &'a str { "lol"