From 014cc1f1bd39a0e922dbd30f693e324cb0a6f99a Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Tue, 10 Apr 2018 20:54:29 -0400 Subject: [PATCH] [#93] Editor: fix generator editor button when error occur and refresh page - add debug for javascript controler when receive data from http --- database/tl_lore.json | 310 -- database/tl_manual.json | 2779 ----------------- src/web/__main__.py | 5 +- src/web/base_handler.py | 2 - src/web/bower.json | 3 +- src/web/handlers.py | 96 +- src/web/partials/_base.html | 8 +- src/web/partials/admin/_base.html | 6 +- src/web/partials/admin/character.html | 506 ++- src/web/partials/admin/editor.html | 30 +- src/web/partials/admin/setting.html | 4 +- src/web/partials/character.html | 477 ++- src/web/partials/character_sheet_print.html | 12 +- src/web/partials/lore.html | 34 +- src/web/partials/manual.html | 37 +- src/web/partials/news.html | 138 +- src/web/py_class/db.py | 50 +- .../doc_generator/doc_connector_gspread.py | 893 ++++-- src/web/py_class/lore.py | 26 - src/web/py_class/manual.py | 56 +- src/web/resources/css/_base_dark.css | 4 + .../character_ctrl/character_ctrl.js | 1061 +++++-- .../character_ctrl/tl_character_schema.js | 822 ----- .../js/tl_module/editor_ctrl/editor_ctrl.js | 29 +- .../js/tl_module/lore_ctrl/lore_ctrl.js | 3 +- .../js/tl_module/manual_ctrl/manual_ctrl.js | 1 + .../js/tl_module/profile_ctrl/profile_ctrl.js | 5 +- src/web/resources/js/tl_module/tl_module.js | 15 +- src/web/web.py | 6 +- 29 files changed, 2820 insertions(+), 4598 deletions(-) delete mode 100644 database/tl_lore.json delete mode 100644 database/tl_manual.json delete mode 100644 src/web/py_class/lore.py delete mode 100644 src/web/resources/js/tl_module/character_ctrl/tl_character_schema.js diff --git a/database/tl_lore.json b/database/tl_lore.json deleted file mode 100644 index 5247d8e0..00000000 --- a/database/tl_lore.json +++ /dev/null @@ -1,310 +0,0 @@ -{ - "lore": [ - { - "title": "Univers de jeu", - "description": [ - "Les joueurs de Traitre-Lame sont la région nordique de la Sarsonne. Il s’agit d’une péninsule reculée du continent où aucune faction ne domine réellement. On y trouve de bons lots de brigands, de créatures et d’organisations douteuses. L’histoire de la région se compte au gré des guerres, des vagues d’exilés cherchant une nouvelle vie ou purgeant une sentence et d’innombrables phénomènes ésotériques ayant rendu la région célèbre. Au loin dans le monde civilisé, on parle de la Sarsonne comme une terre maudite et sauvage.", - "Le terrain de l’Atelier du Loisir représente le village de Vallam et les diverses installations coloniales autour de ce carrefour important de la Sarsonne. Vallam y représente un point central. C’est un lieu où les frontières des différentes factions de la Sarsonne se rencontrent. Il s'agit donc d’une terre de diplomatie, de traîtrise et de constants conflits.", - "Les joueurs incarnent des habitants de la Sarsonne provenant de divers passés. Afin de définir le leur, les joueurs doivent déterminer leur race, et leur origine. La race ne donne pas un avantage de jeu direct, mais fournit un élément pour définir le personnage. Il existe certaines opportunités disponibles seulement aux membres de certaines races. L’origine du personnage est la faction dans lequel le joueur commence son premier jeu. Il existe 5 factions jouables :", - [ - "Le royaume de Canavim", - "L'empire de Vanicant", - "La tribu de Sarsar", - "Le village de Vallam", - "Les Balmonts" - ], - "Les joueurs ont le choix une fois en jeu de choisir leur allégeance selon leur bon gré. Il existe aussi des factions à découvrir en jeu dans lesquels les joueurs peuvent s’investir. Le document est divisé en trois parties : les races, les factions de départ et autres factions." - ], - "section": [ - { - "title": "Cartes", - "description": "", - "section": [ - { - "title": "La Sarsonne", - "description": [ - "Carte du terrain.", - { - "type": "image", - "src": "static/resources/media/carte_de_demi_terrain.jpg" - } - ] - }, - { - "title": "Carte du monde", - "description": [ - "Map du monde.", - { - "type": "image", - "src": "static/resources/media/carte_du_monde.jpg" - } - ] - } - ] - }, - { - "title": "Races", - "section": [ - { - "title": "Humain", - "description": "Race la plus commune sur le continent. Dans la majorité des États, les humains tiennent toutes les charges de pouvoir. On les retrouve dans toutes les conditions, malgré tout." - }, - { - "title": "Wartul", - "description": "Originaires de la forêt Waldur à l’est du continent, ces humanoïdes à la peau verte ont une intelligence et des aptitudes comparables aux humains. Pendant la majorité de leur histoire, les Wartuls ont été isolés dans leur forêt. Leur premier contact avec l’humanité fut leur conquête par l’empire de Vanicant. Pendant plusieurs siècles, ils ont appris à évoluer dans l’empire, restant majoritairement confinés à leur petit territoire. Grâce aux différents tumultes politiques, la forêt Waldur est redevenue un royaume Wartul autonome. Depuis, nombre de ses citoyens ont ouvert leurs horizons et partent à travers le monde." - }, - { - "title": "Meldorsan", - "description": "Ces humanoïdes à la peau pâle striée de veines mauves et bleues saillantes se sont adaptés à un environnement nordique. Les souches de toutes les familles Meldorsanes remontent aux différentes péninsules nordiques qui ont tranquillement migré leur population croissante au sud. Tant dans l’empire de Vanciant que dans le royaume de Canavim, on retrouve des familles nobles Meldorsanes." - } - ] - }, - { - "title": "Factions de départ", - "section": [ - { - "title": "Le Royaume de Canavim", - "description": [ - "Il y a un peu plus d’un siècle et demi, le grand empire de Vanicant se scindait en deux sous le choc d’une rébellion des seigneurs de l’ouest. C’était la naissance du Royaume de Canavim, sous la gouvernance du roi Herbert Aquinas 1er et de ses six seigneurs de guerre, maintenant Archiducs. Sous la dynastie Aquinas, trois rois se sont succédé, tous nommé Herbert en souvenir du mythique premier roi de Canavim. Les règnes des rois Herbert 1er, 2e et 3e furent marqués par une abondance manifeste, de nombreux succès militaires et une grande harmonie dans le royaume. Cette période glorieuse prit fin avec la mort suspecte et soudaine du roi Herbert III, alors que son fils et héritier, Herbert IV, n’était âgé que de 8 ans. Une terrible guerre civile éclata, au terme de laquelle le pouvoir du roi fut secrètement divisé entre les 6 archiducs des grandes provinces de Canavim.", - "C’est dans ce contexte de division que l’Archiduc Casséris Ventour, une ancienne liche des territoires nordiques, choisit d’envoyer un corps expéditionnaire en Sarsonne pour fonder un Marquisat fidèle au royaume de Canavim. La conquête de la Sarsonne se buta à la résilience des locaux et à la compétition de Vanciant. Si le Marquisat fut en définitive établi, la Marquise disparut dans de mystérieuses circonstances la même année. Devant les nombreuses difficultés de la colonisation de la Sarsonne, les forces de Canavim se sont éventuellement tournées vers le choix des alliances et de la coopération locale. Le drapeau de Canavim est rapidement devenu un symbole d’espoir en Sarsonne, les forces expéditionnaires fondant l’ordre des chevaliers du peuple pour protéger les localités de Sarsonne des nombreux périls du nord.", - "Les joueurs peuvent incarner un soldat membre des forces coloniales de Canavim, un colon venu du royaume ou un sympathisant local. Un groupe de joueurs peut désigner un noble parmi ses rangs, à discuter avec l’organisation.", - [ - "L'étendard de Canavim est une tête de bouc de côté sur fond bleu et blanc;", - "La langue nobiliaire de Canavim est l’anglais, mais la langue populaire est le français;", - "Le site principal de la faction est la forteresse à l’entrée du terrain;", - "La religion officielle de Canavim est le Maratisme, une religion qui place les dieux comme des modèles que doivent imiter les êtres humains." - ], - { - "type": "image", - "src": "static/resources/media/Canavim.jpg" - } - ] - }, - { - "title": "L'empire de Vanicant", - "description": [ - "Maintenant situé principalement à l’est du continent, l’empire de Vanicant a déjà couvert presque toutes les terres continentales. C’est un empire presque millénaire, qui a su prospérer à travers les âges, traverser les crises et se renouveler pour assurer sa survie. Au coeur du fonctionnement de l’empire se trouve un rigide système de castes. Au bas de ce système se trouvent les esclaves. La majorité de la population se retrouve dans la caste des communs, qui occupent les emplois les plus humbles, mais souvent les plus nécessaires. Au-dessus, on retrouve les experts, qui occupent les professions libérales et les postes de basse administration. Finalement, on retrouve une noblesse dominée par 5 familles régnantes : les Borgata du nord-ouest, les Anturis au centre et au nord, Les Zazanov à l’Ouest, les Belchir au Sud et les Cirado à l’est. Ces familles ont longtemps dominé l’empire à travers le conseil des Patriarches, soutenu par le sénat impérial.", - "Quand Canavim envoya une force pour annexer la Sarsonne au nord, Vanciant mobilisa les troupes d’élite de la garde écarlate pour bloquer l’expansion de son voisin rival. Or la colonie de Vanicant au nord devait prendre une tout autre vocation avec le temps. De cette position reculée, un mouvement de révolution cherchant à endiguer la corruption des patriarches en restaurant le pouvoir de l’impératrice est né. Avec l’aide de nombreuses organisations, la force expéditionnaire de Vanicant en Sarsonne devint un repère sécuritaire pour les personnages clefs de la révolution. Alors que la guerre fait encore rage, les familles Zazanov, Belchir et Cirado se sont toutes soumises au nouveau pouvoir impérial. Les familles Borgata et Anturis ont formé leurs propres États, refusant d’abandonner leur pouvoir. La révolution a aussi séparé de Vanciant la forêt Waldur, libérant le peuple Wartul de plusieurs siècles de domination vanicante.", - "Les joueurs peuvent incarner des membres des forces coloniales de Vanicant, des colons de l’empire ou des sympathisants locaux. Un groupe de joueurs peut désigner un noble parmi ses membres, à discuter avec l’organisation.", - [ - "L’étendard de Vanciant est la couronne impériale en noir sur fond rouge;", - "Bien que la majorité de Vanicant parle le français, les terres désertiques du sud sont largement occupées par la population Al Aquine, parlant arabe;", - "Le site principal de la faction est la forteresse de croûte cachée en forêt;", - "La religion officielle de Vanicant est le Sravénisme, une religion qui place le Cosmos comme une grande force qui régit tous les aspects de la vie." - ], - { - "type": "image", - "src": "static/resources/media/Vanicant.jpg" - } - ] - }, - { - "title": "La tribu Sarsar", - "description": [ - "La Sarsonne a été pour la majorité de son histoire une péninsule boisée et froide, où très peu d’individus osaient s’installer. La tribu Sarsar est le seul peuple que l’on peut dire natif de la région. Ils ont toujours conservé une existence tribale et chamanique axée sur la vénération des ancêtres, la guerre et le respect de la nature. La grande capitale d’Etar’Tiak fut par contre le siège de nombreux gouverneurs coloniaux ayant capturé la Sarsonne à travers les âges. Le plus notable de ces envahisseurs, la tribu Strône de la péninsule voisine, fut évincé dans une violente révolution il y a plus d’un siècle. La tribu coexiste depuis avec les nombreuses forces de Sarsonne, se jurant de reprendre le contrôle total des terres un jour. La tribu est dirigée par un vaste conseil présidé par le Guide suprême de la tribu. Siègent sur ce conseil les vénérables patriarches, reconnus pour leur sagesse, ainsi que les guides et grands guides Sarsar qui organisent les clans de la tribu partout en Sarsonne.", - "La région de la Grande Forêt, celle qui borde le village de Vallam, est occupée par plusieurs clans Sarsars. Le plus célèbre est certainement le clan Ish, qui a combattu avec détermination les envahisseurs coloniaux. Ce clan a construit avec les années une proximité entre les druides et la tribu. Cela a permis d’utiliser les forces déchaînées de la nature dans leur lutte contre les empires coloniaux. On y retrouve aussi le clan Smith, composé de colons des empires ayant rejoint la tribu volontairement. Plus terre à terre, se concentrant sur la géopolitique et le commerce, ce clan compte vaincre les empires avec leurs propres armes. Nombre d’autres clans ont marché aux côtés de ces deux, et d’autres viendront certainement peupler les terres de la Grande Forêt.", - "Un joueur Sarsar peut incarner un membre d’un clan existant ou fonder son propre clan s’il vient avec un groupe. Un clan peut désigner son propre chef et déterminer ses propres traditions.", - [ - "Les Sarsars n’ont pas d’étendard unique. Chaque clan porte ses propres couleurs, et marque son appartenance à la tribu Sarsr par la construction de nombreux totems dévoués à la vie, à la guerre, à la nature, au Cosmos et aux ancêtres;", - "La tribu Sarsar parle le français;", - "Les sites principaux de la faction sont le village suspendu et la fermette;", - "La religion officielle de la tribu Sarsar est le Sravénisme totémique, où l’influence du Cosmos est fractionnée en 5 totems (Ancêtre, vie, nature, Cosmos et guerre) et où le Vide représente les forces obscures." - ] - ] - }, - { - "title": "Le village de Vallam", - "description": [ - "Pendant la majorité de son histoire, la Sarsonne a servi de terre d’exil pour les deux grands empires. Quand un citoyen était accusé de crimes graves, il pouvait être condamné à une vie recluse au coeur des dangers de la Sarsonne. Cette pratique a créé les nombreux petits villages nordiques de la région, échappant au contrôle des différentes factions. Le village de Vallam occupe une position centrale en Sarsonne. Géographiquement, c’est la croisée des chemins entre les factions du nord, ce qui en fait un lieu de rencontres diplomatiques, de conquêtes et de conflits. Le village est aussi un nid de trafiquants, de bandits et de cultistes uni dans leur détermination à conserver leur autonomie. Le village a historiquement été dirigé par un maire, bien que les mandats soient réputés très courts.", - "Depuis l'arrivée des forces coloniales des empires, le village de Vallam a tenté de tirer son épingle du jeu tout en conservant son autonomie. On retrouve des villageois dans presque toutes les manigances, s’alliant aux créatures les plus immondes pour assurer leur survie. Le village est aussi au centre de plusieurs cultes, notamment un culte de la nuit voué au dieu Cesserak (qu’ils prononcent Kesserak pour se distinguer des autres religions). C’est aussi le village qui a accueilli la créature Natueur et fondé la base du culte de shamans noirs, qui visent à étendre l’influence de Natueur sur le continent.", - "Un joueur au village peut incarner un natif, un exilé venu rejoindre la localité ou un déserteur d’une des autres factions cherchant la liberté. Un groupe de joueur peut désigner un de ses membres afin d’obtenir un statut particulier (noble déchu ou fils d’un célèbre voleur, par exemple). À discuter avec l’organisation.", - [ - "Le village de Vallam change d’étendard au gré de ses dirigeants;", - "Toutes les communautés et langues existent au village;", - "Le site principal du village de Vallam est le village à l’entrée du terrain;", - "Il y a pratiquement autant de religions que d’individus au village." - ] - ] - }, - { - "title": "Les Balmonts", - "description": [ - "Il y a de cela de nombreux siècles, la famille Balmont était une puissante famille noble de Vanicant, situé dans la région des Pics de Feu. Dans un grand tremblement de terre suivant de nombreuses éruptions volcaniques, les terres des Balmonts se volatilisèrent d’une journée à l’autre. Tous croyaient la famille disparue à jamais, mais leur malédiction était bien pire. Les Balmonts et leurs forces furent jetés dans les dunes de débris de l’Enfer où ils furent condamnés à combattre inlassablement ses habitants infernaux. Cette situation désespérée construit une faction militariste, paranoïaque et sévèrement endoctrinée dans sa recherche de la pureté. Cette dévotion et cette discipline leur ont permis de ne jamais abandonner la recherche pour une voie de sortie.", - "Il y a seulement deux ans, les Balmonts sont parvenus à ouvrir une brèche intermittente en Sarsonne pour permettre aux leurs de reprendre une influence dans le monde matériel. Ils ont fondé une ligue de défense pour unir les citoyens de Sarsonne à leur bannière. Depuis leur retour, les Balmonts ont tissé des liens avec les grands empires sans toutefois perdre leur indépendance. La majorité de leurs efforts tournent encore autour de la purification des nombreuses horreurs de Sarsonne et aux expéditions au-delà de la brèche infernale pour supporter leurs frères toujours en Enfer.", - "Les joueurs incarnent des convertis à l’idéologie Balmont. Un groupe de joueur peut nommer un capitaine qui répond pour l’unité. À discuter avec l’organisation.", - [ - "L’étendard Balmont est une croix prolongée pour former un B sur fond blanc;", - "Les Balmonts parlent français;", - "Le site principal des Balmonts est la petite forteresse au coeur de la forêt de bouleaux;", - "Les Balmonts pratiquement le Sravénisme et le Maratisme, mais subordonne la pratique religieuse à la recherche de la pureté." - ] - ] - } - ] - }, - { - "title": "Autres factions en Sarsonne", - "section": [ - { - "title": "La Guilde d'Amunsrat", - "description": [ - "Depuis près de 50 ans, cette organisation survie de la traite d’esclaves. Pendant longtemps, l’isolation au nord a permis à la guilde de croître avec un bon réseau stable. Or, à l'arrivée des empires coloniaux en Sarsonne, les choses ont changé. Lors de la première invasion Strône, ils furent largement décimés. Il leur a fallu 4 ans pour rebâtir, juste à temps pour la seconde invasion Strône. Or cette fois, ne voulant pas être de nouveau anéantis, ils ont choisi de supporter l’invasion en assurant la logistique de la guerre du Roi Strône.", - "Un nécromancien à l’humour morbide du nom de Slaven a élu domicile près de Vallam pour brasser les affaires de la guilde dans la région. On le retrouve généralement la nuit, loin dans la forêt, dans une forteresse décharnée au-delà du village Sarsar." - ] - }, - { - "title": "Le Royaume de Figaro", - "description": [ - "Il y a une génération, un puissant élémentaliste de Canavim se rendait en Sarsonne pour baigner de feu les forêts nordiques afin de débusquer un groupe de traitres. Dans le chaos de cette époque, un jeune page de Canavim du nom de Valentin Figaro a rassemblé brigands et clans épars de l’est de la Sarsonne pour fonder un royaume sans foi ni loi, sujet à ses caprices. Le groupe a vite développé une expertise en alchimie pour garantir leur survie. Au moment de l’arrivée des forces coloniales en Sarsonne, l’empire de Vanicant a reconnu l’auto proclamée roi en mariant une fille noble du clan Borgata à l’ignoble roi nordique. La lune de miel fut bien courte. Figaro devint un fier défenseur du gouvernement des patriarches. Ses poisons furent au coeur de nombreux assassinats politiques, incluant une tentative ratée sur l’impératrice elle-même. Ses terres sont aujourd’hui envahies par les forces de la Confrérie de Bronze, le roi Valentin Figaro maintenant en exil.", - "Afin de lever les fonds nécessaires pour continuer la guerre contre la Confrérie de Bronze, Figaro a envoyé un homme de main surnommé le Colosse. Ce grivois roublard rôde près de Vallam le jour, disparaissant la nuit pour diriger sa compagnie de brigands." - ] - }, - { - "title": "La Confrérie de Bronze", - "description": [ - "La chute du premier régime impérial de Vanicant a donné lieu à nombre de turbulences. Pour la vieille garde, le monde changeait durablement, mais beaucoup voulaient continuer à combattre pour l’ancien ordre. La confrérie de bronze a commencé comme une petite association de notables déchut dans le changement de régime. Le but ultime était de restaurer le pouvoir impérial. L’organisation avait vite muté en un nid de criminels, les idéaux passés jetés au rencart. Un jeune idéaliste du nom de Darko Zazanov a ranimé la flamme impériale du groupe avec l’aide des forces coloniales de l’empire en Sarsonne. À travers nombre d’assassinats politiques, de manipulation et de combat acharné, la Confrérie renouvelée a créé un vent de révolution qui a mené à la récente restauration du pouvoir impérial. En Sarsonne, la confrérie s’est embourbée dans une guerre d’occupation dans le royaume de Figaro.", - "Darko Zazanov est toujours en Sarsonne et se présente régulièrement à Vallam pour superviser les opérations de la confrérie près du village. Il passe généralement sous le couvert de la nuit." - ] - }, - { - "title": "Les pirates d'Angbar d'Allurstriase", - "description": [ - "Angbar d’Allurstriase est une légende vivante. C’est un aventurier, sortie de nulle part, qui dans son jeune âge a rallié par son grand charisme nombre de navires. Il s’est enrichi en pillant la marine marchande des empires sur les rives nordiques du continent. De là, il a recruté les plus puissants experts de magie de création pour créer l’unique nexus de création en existence. Au sommet de sa puissance, il a construit une armée de golems pour prendre le contrôle de la Sarsonne. Son armée fut détruite suite à la trahison d’un de ses agents et du meurtre de son maître enchanteur. Ses ambitions de conquête détruite, le pirate s’est rabattu sur le commerce d’objets magiques, et il vie comme un roi au nord de la Sarsonne. Il est présentement au coeur de l’invasion Strône, ses forces luttant pour repousser l’envahisseur.", - "Angbar a envoyé un de ses enchanteurs, Cyril de Valprenant, à Vallam pour obtenir le soutien des locaux contre l’invasion Strône. L’enchanteur semble impossible à trouver la nuit." - ] - }, - { - "title": "Les barbares Strônes", - "description": [ - "Natif d’une péninsule à l’ouest de la Sarsonne, il s’agit d’une tribu de pillards et de barbares avec une longue histoire de rivalité avec la Sarsonne. Ils ont jadis occupé le territoire et réduit à l’esclavage la tribu Sarsar. Depuis, les deux peuples sont ennemis. Depuis l'arrivée des empires en Sarsonne, les Strônes ont fait deux invasions massives. La première était dirigée par Muladath Strône, qu’on appelle aujourd’hui le dernier empereur Strône. Il a été défait pendant l’invasion de Vallam et grièvement blessé, annonçant le début d’une pénible retraite pour les barbares nordiques. Leur deuxième invasion est fort récente, et par trois fronts. D’un côté, un nouveau roi Strône mène une attaque par la petite enclave Strône en Sarsonne à travers les terres d’Angbar et de la tribu Sarsar. À partir de Port-Marais, le portail ouvert par les Strônes permet au gros de la force de déferler dans les terres Sarsar. Finalement, un clan particulièrement brutal que l’on appelle le Crâne Sanglant a conquis une lisière de territoire entre les terres Sarsar et de Figaro d’où ils mènent des pillages.", - "Le chef du clan du Crâne Sanglant, Morla l’arracheur de tête s’est installé près de Vallam afin de représenter les forces Strônes dans la région. S’il a ses propres objectifs également, il maraude autour du village la nuit, souvent avec quelques-uns de ses pillards." - ] - }, - { - "title": "Maledastarone", - "description": [ - "La Sarsonne est délimitée par un long fleuve qui coupe le continent. Au coeur de ce cours d’eau se trouve la grande cité État de Maledastarone, coeur économique de la Sarsonne. Cette ville neutre accueille tous les visiteurs et distribue tous les types de produits et services. La sécurité dans la ville est assurée par une garde sournoise faisant usage de tunnels infinis creusés sous les bâtiments de la ville. La ville est dirigée par un mystérieux personnage que l’on nomme le Grand Coordonnateur. Ses adjoints sont dénudés de leur identité et portent comme nom un numéro correspondant au nombre d’adjoints engagés avant le candidat. Ces agents gèrent les guildes marchandes et les associations d’artisans de la ville en plus d’en organiser la défense.", - "Le réputé collectionneur 73 fait maintenant des visites régulières à Vallam pour flairer les opportunités d’affaires. Une fois par mois, l’adjoint de haut niveau se présente au village pour parler affaires et offrir des contrats." - ] - }, - { - "title": "Les Druides", - "description": "Sur le continent, la mention des druides soulève la peur et la haine. Ces humains à l'apparence animale veulent éliminer la civilisation humaine pour retourner le monde à une existence plus simple. Les druides ont toujours été actifs dans le nord. Près de Vallam, les clans Sarsars ont généralement supporté leur cause. Si les druides ont longtemps terrorisé la région de Vallam, ils sont devenus beaucoup plus discrets depuis l’arrivée de Natueur. La créature a détruit la Racine Mère, une importante source de magie naturelle pour les druides. Depuis cette défaite, les druides ont concentré leurs forces dans la restauration de leur puissance magique et la lutte à Natueur. Les druides sont peu sociables, alors il est très difficile de trouver leurs représentants." - } - ] - } - ] - }, - { - "title": "Dieux", - "description": "Le monde est influencé par plusieurs dieux dans cet univers. La liste des dieux est statique, mais les religions sont multiples, chacune vénérant à sa façon les dieux existants, parfois mettant l'emphase sur l'un plutôt que sur l'autre. Voici la liste des dieux et leur description la plus consensuelle entre les religions.", - "section": [ - { - "title": "Brokrand", - "description": "Dieu de la justice, Brokrand est un dieu qui a gagné en importance avec l'avènement des grandes civilisations. Au temps du tribalisme, Brokrand était une figure invoquée par les chefs de tribus ou leur chaman quand venait le temps de trancher un conflit. Maintenant une figure imposante dans la plupart des sociétés, Brokrand est un dieu souvent représenté comme un vieil homme de stature imposante, les autres détails variant de culte en culte. L'ordre et la justice sont les thèmes les plus récurrents chez ce dieu : il est rare qu'il soit considéré comme un dieu mineur." - }, - { - "title": "Eccani", - "description": "Déesse aux multiples visages, les religions s'entendent peu sur le rôle exact de cette divinité. Sa représentation dans les écrits est aussi variable que son interprétation. La grande confusion provient du fait que le premier texte traitant de la déesse était dans un langage fort primitif, et le mot utilisé pour la décrire pouvait aussi bien se traduire par vie, fertilité ou nature. Ainsi, certains l'attribuent comme une déesse de la fertilité, d'autres de l'agriculture et même parfois comme une patronne des femmes. L'importance de cette déesse dans les religions varie grandement avec son rôle exact." - }, - { - "title": "Cesserak", - "description": "Souvent représenté comme une ombre ailée ou un insecte menaçant, Cesserak est le dieu de la nuit, des ténèbres, mais aussi parfois de la nature ou du sacrifice. Certaines religions font de lui un antagoniste ennemi des mortels et des autres dieux, d'autres lui vouent un culte par respect ou par crainte. Les légendes veulent que Cesserak soit le patron des créatures nocturnes, ainsi plusieurs militaires assignés à des quarts de nuit vont porter un symbole le représentant pour obtenir sa clémence. Les sages ont confirmé que certaines créatures intelligentes de la nuit lui vouent un culte exclusif." - }, - { - "title": "Nox", - "description": "Le grand et puissant Nox est un dieu dont l'interprétation demeure similaire d'une religion à l'autre. Nox est le dieu du courage et de la force, on le dit souvent le dieu des héros. Nox est peu représenté physiquement dans les écrits, bien qu'il arrive qu'il soit représenté en barbare ou en templier dépendamment des régions. Il n'est jamais décrit comme un stratège ou un soldat discipliné, et il encourage l'initiative personnelle à la guerre par ses enseignements." - }, - { - "title": "Malinkant", - "description": "Le dieu-rat Malinkant, on dit, est à l'origine des pestes et des épidémies. C'est un dieu dont on craint le courroux et la plupart des religions lui font sacrifice pour implorer sa clémence. Certaines religions vont le relier à toutes les catastrophes naturelles, d'autres simplement à la maladie. Il est souvent aussi un dieu du crime et souvent placé en opposition avec Brokrand." - }, - { - "title": "Finosia", - "description": "Dans les plus anciens écrits, les mots utilisés pour décrire la déesse Finosia sont déesse du cycle. Les interprétations de cette appellation sont vastes, mais la plupart des sages s'entendent pour une forte référence au cycle de la vie. Ainsi on la verra comme déesse de la mort, déesse de la vie, déesse de l'après-vie ou déesse de toutes ces choses à la fois. Sa représentation varie largement selon le rôle qu'on lui attribut." - }, - { - "title": "Nikatum", - "description": "Ce dieu est souvent lié à la guerre et à la politique. On le dit le dieu le plus près du pouvoir des hommes, celui qui planifie la guerre et dirige les peuples par son influence indirecte. Nikatum est souvent représenté comme un tacticien humain ou comme un puissant géant. Les anciens textes racontent le temps où Nikatum marchait sur la terre des hommes. La conclusion de l'histoire a depuis longtemps disparu des annales de l'histoire, et chaque religion la complète par extrapolation ou par opportunisme." - }, - { - "title": "Le cosmos", - "description": "Parfois vu comme un ensemble, parfois comme une divinité et autrefois comme une force, les anciens font référence au cosmos dans tous leurs récits religieux. Le cosmos peut être l'univers en soi, parfois un dieu réel et séparé mais à la fois englobant toutes choses et quelques fois même une simple force sans volonté propre de laquelle on tire un certain pouvoir. Le cosmos prend néanmoins une part très importante dans les religions du monde, c'est un élément mystique que l'on reconnaît comme essentiel à la solidité du monde." - }, - { - "title": "Les éléments", - "description": "Depuis les premières théories sur les phénomènes naturels, on les a attribués au divin. Si certaines tribus primitives vouent un culte aux volcans mêmes ou aux tempêtes, toutes les religions vont relier la météo et les forces naturelles à un aspect divin, une volonté supérieure. Certaines religions y vouent une importance assez forte pour donner un aspect divin aux élémentaux et aux golems, ce que d'autres religions rejettent fortement. On peut tout de même considérer les éléments comme une force divine même s’ils ne sont pas toujours perçus comme un dieu en eux-mêmes." - }, - { - "title": "Les autres forces", - "description": "Il existe nécessairement un grand nombre d'autres forces dans le monde qui sont soit spécifiques à une ou deux religions en particulier ou qui sont d'importance réduite. Tel est le cas des grandes castes extra planaires, comme les démons ou les anges. Ils ne sont pas toujours liés aux agendas des dieux, et souvent plutôt reliés à leurs propres desseins. Il est généralement admis que les anges et les démons sont en opposition, mais certaines religions postulent que ces créatures n'ont rien de divin et cherchent seulement à voler le cœur des hommes pour leurs desseins personnels." - } - ] - }, - { - "title": "Religions", - "section": [ - { - "title": "Le Maratisme", - "description": "La religion Marat est une des grandes religions du monde. Elle repose sur les écrits du prophète Skéran qui, il y a près de deux millénaires, a retrouvé les anciennes tablettes religieuses de la tribu sainte de Marat, première à avoir eu un contact avec les dieux. La prétention de ce mouvement est d'être le plus près de la réalité de l'humanité.", - "section": [ - { - "title": "Cosmologie", - "description": [ - "Cette religion accorde une part aussi importante à Brokrand, reconnu comme le dieu de la justice, Finosia, déesse de la mort et Eccani, déesse de la vie. Cette religion donne une grande importance aux dieux humanoïdes et peu aux dieux qui s'éloignent des humains. Ainsi, Brokrand est représenté comme un vieil homme sage de grande stature portant de riches vêtements. Finosia devient une déesse plus sinistre mais toujours empreinte d'une grande humanité. Eccani est une déesse radieuse représentée comme une jeune femme de grande beauté.", - "Cette religion rejette fortement Cesserak et Malinkant comme agents aidant l’humanité. On attribut à Malinkant les catastrophes naturelles ainsi que tous les maux provenant de la nature, alors que tous les monstres proviennent du dieu-monstre Cesserak, qui utilise le couvert de la nuit pour cacher sa grande laideur. L'imaginaire Maratisme ne peut pas imaginer un mal qui provienne de leur propre structure sociale; le souverain devient alors une figure d'inspiration divine qui, bien qu'il ne dirige pas la religion, détient une certaine légitimité de celle-ci. On dit de Nikatum qu'il était le premier souverain des hommes, par mandat divin, et qu'à sa mort il fut amené dans le monde des dieux pour veiller sur les souverains des hommes. Le cosmos pour les maratistes est le vide, l'espace entre les étoiles, ce qui tient le monde ensemble. Il ne s'agit pas d'une force consciente, mais plutôt de l'énergie résiduaire des dieux." - ] - }, - { - "title": "Hiérarchie", - "description": [ - "Au niveau clérical, la religion Marat possède une hiérarchie très stricte. Le chef de la religion est le Grand Oracle, dont la fonction est de connaître la religion et d'interpréter celle-ci pour que son application englobe toutes choses. Sous lui se trouvent les Oracles, grand savants eux-aussi, qui ont pour tâche d'aider les souverains dans leurs décisions et de produire des études religieuses. En dessous des Oracles se trouvent les évêques, qui doivent transmettre le divin à la population dans les grands collèges de la religion Marat. Plus souvent qu'autrement, ces professeurs sont aussi savants dans un grand nombre de matières qui leur permettent de faire l'éducation complète des gens à leur charge. Les évêques font aussi le lien entre les officiants plus près du peuple et les Oracles. Finalement, la religion possède un certain nombre d’officiants mineurs qui varient selon les régimes.", - "Parallèle au clergé régulier se trouvent les templiers. Cette organisation est spécifique à la nation de Canavim et ne devrait pas être vue comme une normalité dans la foi. Les templiers sont des soldats pieux au service du régime qui sont capables de certains pouvoirs reliés normalement aux thaumaturges. Un templier doit suivre les enseignements de la foi, mais il ne peut pas non plus désobéir aux lois de sa patrie. Les templiers sont décrits en profondeur dans le document de classe et le document de Canavim." - ] - }, - { - "title": "Dogme", - "description": [ - "La religion Marat est une religion de peu de dogmes, les Oracles gardant le contrôle sur ce qui est acceptable ou non. Cette flexibilité dans la foi permet à cette religion de demeurer adaptable au contexte dans lequel elle se trouve, mais en un même temps donne un grand pouvoir idéologique au Grand Oracle. Il est fort rare pour un souverain de se mettre à dos le chef de la foi, car ses édits ont de grands impacts sur la population. Les édits qui n’ont jamais été modifiés tiennent surtout sur le comportement du croyant. Chacun se doit de porter secours au faible, de ne pas tuer par plaisir, de ne pas voler ou piller et toutes ces choses qui semblent faire partie d’un certain droit naturel. Le Maratisme a sa fête majeure au nouvel an et à la fête des ancêtres. La fête des ancêtres souligne le souvenir de ceux qui ont trépassé; on en profite souvent pour visiter la tombe des défunts qui nous sont proche.", - "Le Maratisme a un fort dogme entourant l’union de deux être dans le but de fonder une famille, ainsi que pour honorer ceux qui sont morts. Les mariages ne peuvent jamais avoir lieu pendant une période de grand malheur, cela est réputé mauvais pour la progéniture issue de l’union. Le mariage prend généralement la forme d’une cérémonie très grandiose qui regroupe famille et amis dans un banquet. Le mariage doit être béni par un officier de la foi ou un magistrat de l’État. Le Maratisme croit qu’à la mort, l’âme est dispersée dans le monde et finit par rejoindre les dieux. Pour cette raison, on n’enterre jamais les morts, car cela emprisonne leur âme. La méthode traditionnelle consiste à brûler le cadavre dans un immense feu sur lequel on prépare un repas pour les proches de la victime. Cette tradition étrange ne trouve pas vraiment écho ailleurs." - ] - }, - { - "title": "Thaumaturgie", - "description": "Les thaumaturges de cette religion peuvent suivre trois voies divines. Cette voie est choisie au moment de leur intronisation parmi les rangs des thaumaturges. Chacune des voies est une référence à un dieu majeur du culte. La voie première est la voie de la justice (Brokrand), la seconde la voie de la vie (Eccani), et la dernière la voie de la mort (Finosia)." - } - ] - }, - { - "title": "Le Sravénisme", - "description": "Le livre de Sravène fut découvert il y a plusieurs millénaires pendant l’ère du tribalisme. Le livre écrit avec du sang animal sur de l’écorce de bouleau n’a pas survécu jusqu’à notre ère, ne laissant derrière que des copies souvent modifiées par les différentes hérésies que cette religion a connues. Cette religion est la plus répandue sur le continent, car elle prend racine dans le plus profond de l’ère du tribalisme (Les tablettes de la tribu Marat furent découvertes après ce livre prophétique). Le Sravénisme est donc une religion très âgée, qui est resté ancrée dans les dogmes du passé.", - "section": [ - { - "title": "Cosmologie", - "description": [ - "L’attachement au divin est fort particulier pour cette religion. En effet, la religion n’a pas de concept de dieu bon ou mauvais, les dieux sont tous des esprits qui transcendent notre réalité et représentent de grands concepts. La cosmologie sravène donne une importance très marquée aux éléments et au cosmos ainsi qu’à un nombre de forces extérieures. Le cosmos s’inscrit comme la force créatrice qui guide le monde sur une voie précise qui échappe à l’imaginaire des mortels. C’est ainsi que la plupart des croyants de cette religion en viennent à la conclusion que nous avons tous un destin déterminé par le cosmos, mais qu’il nous est impossible de prévoir notre propre destin. La succession des saisons et les changements météo sont vus comme l’œuvre active et raisonnée du cosmos. Si une région est touchée par une catastrophe naturelle, on attribut cela à un bris du destin : les mortels avec leur volonté propre sont allés, bien malgré eux, contre la volonté du cosmos, et les catastrophes naturelles viennent réguler les erreurs des hommes. Pour cette raison, il existe certaines régions qui ne reconstruisent pas après qu’un sinistre ait détruit un bâtiment, supposant qu’il s’agissait de la volonté du cosmos.", - "Les dieux ont quand même leur part à jouer dans ce système. En effet, ceux-ci sont les esprits gardiens des hommes. Brokrand est le patron de la civilisation et du progrès, il dirige les nations sur la voie de la prospérité et de la sécurité. Eccani est la déesse de la fertilité, protégeant contre les fausses-couches et les mauvaises récoltes. Cesserak veille sur le sommeil des créatures diurnes et sur les créatures nocturnes. Nox est le patron des héros de la guerre et des combattants en général, il inspire le courage et le leadership. Malinkant est le dieu du mal nécessaire, celui qui brise le cycle lorsque nécessaire. Finosia est la déesse du cycle, accompagnant les mortels de leur naissance à leur putréfaction. Nikatum est le dieu du pouvoir, il veille sur les détenteurs du pouvoir et ceux qui peuvent améliorer l’État. On implore ces dieux lorsqu’une situation se rattachant à eux demande l’intervention du divin ou de la chance.", - "Le concept du cycle est très important pour la religion sravène. Il y a cette croyance que tout est cyclique : Si une nation tombe, alors une autre prendra sa place. Si une personne meurt, son corps et son esprit se retrouveront dans la nature et reviendront sous une autre forme. Le cycle est, en quelque sorte, une divinité en lui-même." - ] - }, - { - "title": "Hiérarchie", - "description": "La religion sravène n’a pas de clergé à proprement parler. Il existe des autorités en l’objet des livres religieux et il existe ceux qui savent et ceux qui ne savent pas. On nomme souvent sage celui qui connaît les livres religieux. Les sages sont présents dans les communautés et ils ne sont pas toujours thaumaturge (et vice-versa). Les gens du peuple qui sont très pieux se rapportent souvent à leur sage pour avoir des conseils sur comment ils devraient se comporter ou quand vient le temps de prendre de grandes décisions. Le sage devient le conseiller d’un peu tout le monde. Il n’existe pas de supérieur au sage, et même si certains jouissent d’un grand prestige, nul n’est tenu de prendre leurs conseils comme divins. Ils n’ont pas d’emprise sur le pouvoir autre que l’influence qu’ils arrivent à avoir sur le petit peuple par endroit. Leur pouvoir est proportionnel à leur capacité à mobiliser le peuple. Il arrive souvent que les sages plus influents se retrouvent en compétition pour savoir qui est le plus connaissant. Rares sont les guerres de ce genre qui en viennent au sang, mais cela n’est pas impossible." - }, - { - "title": "Dogme", - "description": [ - "Le Sravénisme est lourd de dogmes et de fêtes. On fête l’arrivée du printemps avec la fête du renouveau pendant laquelle les jeunes sont à l’honneur. On fête l’arrivée de l’été avec le festival estival. On fête l’arrivée de l’hiver avec la fête des aïeuls où les plus vieux sont à l’honneur et finalement l’arrivée de l’automne avec la fête du cycle, l’automne étant considéré comme la fin du cycle des saisons. La religion étant très forte sur le concept du cycle, la mort est célébrée en enterrant le cadavre, permettant à son corps de rejoindre la nature. Pendant un enterrement, il est coutume de faire un grand exposé sur la vie du défunt. Il est très mal vu de parler des circonstances de sa mort. L’immersion est aussi une forme utilisée pour disposer d’un mort. L’union de deux personnes est généralement célébrée par un magistrat ou le sage de la communauté, quoi qu’il arrive que l’on cherche un thaumaturge pour bénir plus efficacement l’union.", - "Le Sravénisme interdit à ses membres de se nourrir d’une créature que l’on a tuée nous-mêmes, sauf si l’on vit en Hermite (bien qu’un Hermite soit généralement végétarien) ou que l’on est en situation de survie. Le Sravénisme interdit aussi de disposer du corps d’un animal ou d’une plante déracinée dans un milieu où elle ne pourra pas se décomposer et rejoindre la nature. Le Sravénisme est aussi très stricte sur la mécanique tribale. La hiérarchie des familles et des clans est fortement renforcée dans la religion et oblige le simple citoyen à se soumettre à la volonté du seigneur sauf en cas d’extrême oppression. Il vaut toujours mieux faire confiance au souverain local qu’à l’étranger. Le Sravénisme permet aussi l’esclavage, postulant que la liberté de l’homme doit être acquise par la victoire." - ] - }, - { - "title": "Thaumaturgie", - "description": "Les thaumaturges Sravènes sont fortement sollicités. Un thaumaturge gagne ses pouvoirs par la volonté du cosmos; il n’y a pas d’enseignement possible, seulement la découverte intérieure, ce qui rend le nombre très restreint. Bien qu’ils ne soient pas nécessairement sages, les thaumaturges sont des piliers de leur communauté et sont souvent portés à voyager beaucoup pour aider le plus de communautés possible. Le thaumaturge sravène est la seule personne qui a le droit de renverser le cycle de la vie par le moyen de la résurrection, bien que les nécromanciens soient tolérés dans certaines régions." - } - ] - } - ] - } - ] -} diff --git a/database/tl_manual.json b/database/tl_manual.json deleted file mode 100644 index 9a42e788..00000000 --- a/database/tl_manual.json +++ /dev/null @@ -1,2779 +0,0 @@ -{ - "manual": [ - { - "title": "Mécanique de base", - "section": [ - { - "title": "Mécanique de combat", - "section": [ - { - "title": "Points de vie", - "description": "Chaque joueur commence avec 3 points de vie et ne peut jamais en avoir plus que 10. Quand un joueur tombe à 0 point de vie, la prochaine touche sur un de ses membres le prive de ce membre (jusqu’à guérison d’un point de vie utilisé à cet effet). La prochaine touche au torse ou sur un membre coupé le rend assommé." - }, - { - "title": "Dégât", - "description": "Toutes les armes de mêlée ou de lancer infligent 1 point de dégât. Les flèches font 2 points de dégât. Il n’est jamais possible d’infliger plus de 3 points de dégât avec une arme." - }, - { - "title": "Armes et bouclier", - "description": "On considère à deux mains toute arme d’une longueur totale entre 130 cm et 210 cm, aucune arme ne peut dépasser cette dimension. Un joueur ne peut manier une autre arme, ou utiliser un bouclier, s’il utilise une arme à deux mains. Une arme brune est considérée comme étant en bois et ne fait aucun dégât. Tous les boucliers doivent mesurer moins de 100 cm dans leur plus grande dimension et être conçus de matériaux réalistes. Les boucliers bloquent tous les projectiles, incluant les sorts, mais pas ceux des engins de siège. L’organisation se réserve le droit de refuser toute arme ou tout bouclier qu’elle juge dangereux." - }, - { - "title": "Armure", - "description": "Un plastron de cuir confère 1 point de vie. Un plastron de métal confère 2 points de vie. Une protection aux jambes et aux bras confère 1 point de vie supplémentaire. Les points de vie de l’armure sont les premiers à être perdus en combat. Une armure ainsi brisée doit être réparée par la technique forgeron hors-combat au coût d’un point d’énergie pour conférer à nouveau ses points de vie." - }, - { - "title": "Le karma et la mort", - "description": "Un joueur assommé n’est pas mort. Pour tuer un joueur, il faut lui faire une touche au torse alors qu’il est assommé en déclarant achever ce joueur. Cette action incombe une pénalité d’un point au karma du joueur qui achève. Le karma est la mesure d’un personnage par rapport à la faveur divine. Quand un joueur meurt et qu’aucun autre joueur n’a la capacité de le ramener à la vie, il peut se rendre au cimetière 2 minutes après les hostilités, payer 5 points de karma et attendre 10 minutes au cimetière avant de retourner au jeu au maximum de sa capacité (excepté pour ses habiletés de production et rituel). Les joueurs commencent chaque jeu avec 10 points de karma et peuvent revenir à la vie au cimetière s’il leur reste au moins 1 point de karma. Si un joueur n’a plus de karma, ou un karma négatif, il peut abandonner son personnage ou attendre au sol d’être ramené à la vie par un autre joueur." - } - ] - }, - { - "title": "Création de personnage", - "section": [ - { - "title": "Départ", - "description": "Un nouveau personnage commence avec 2 points d’énergie et 6 points d’expérience à distribuer. Le joueur peut choisir jusqu’à deux disciplines parmi les 4 suivantes: combattante, sournoise, magique et professionnelle. Pour chaque discipline, il doit choisir deux habiletés. Il peut ensuite dépenser ses points d’expérience de départ parmi les habiletés choisies et celles de la discipline commune. Il n’est pas possible d’avoir plus de 3 points d’expérience dans une habileté donnée au départ." - }, - { - "title": "Progression", - "description": "À chaque jeu, un personnage obtient 1 point d’expérience. Le joueur investit ce point d’expérience dans l’une de ses habiletés, jusqu’à concurrence de 3 points par habileté. Quand un joueur atteint 3 points d’expérience dans chacune de ses habiletés, en dehors de la discipline commune, il obtient 4 nouvelles habiletés. Cela n’arrive qu’une seule fois dans la vie d’un personnage. Comme au départ, il choisit 2 habiletés pour 2 disciplines, ou deux fois la même discipline. Il est possible de choisir une habileté que l’on possède déjà. À ce moment, le joueur pourra investir 2 points d’expérience additionnels dans cette habileté. Il peut alors aller chercher les 5 techniques de l’habileté visée. Voir l’annexe 1 en page 14 pour un exemple de progression de personnage." - }, - { - "title": "Remplacement", - "description": "Un nouveau joueur peut toujours changer de personnage sans pénalité d’expérience à son second jeu. Il peut refaire les statistiques de son personnage ou le changer au complet à sa guise. À tout autre moment, un joueur peut créer un nouveau personnage selon la mécanique décrite en début de section. Il ajoute à ses points d’expérience de départ la moitié des points d’expérience cumulés par son personnage actuel. Un changement de personnage est irrévocable." - } - ] - }, - { - "title": "Magie", - "section": [ - { - "title": "Sorts", - "description": [ - "Toutes les techniques de la discipline magique n’étant pas passive sont considérées comme des sorts. Afin d’utiliser un sort, il est nécessaire de prononcer une formule de 12 syllabes au choix du joueur qui est attaché à la technique précise que l’on désire utiliser en plus de dépenser un point d’énergie. La formule doit être crédible dans l’univers de jeu de Traitre-Lame. Dans le cas d’une technique de production, il est nécessaire de produire une formule pour chacun des objets que l’on crée. Cette formule est consignée dans un livre de sort. Sans livre, un magicien ne peut pas faire usage de ses sorts.", - "Les sorts ayant la portée [touché] ou [balle] peuvent être transmis au moyen d’une arme de bois. Tout sort peut être invoqué et conservé pour utilisation future. Au moment de prononcer la formule d’un tel sort, le joueur doit lever sa main, son arme de bois ou sa balle au ciel. Tant que la main du joueur est au ciel, le sort est maintenu. Toute hostilité physique envers le joueur (ex. : une touche avec une arme ou avec la main) met fin au sort, gaspillant le point d’énergie. Cette règle est applicable aux rituels à portée similaire." - ] - }, - { - "title": "Rituel", - "description": [ - "Un rituel est un sort de pouvoir supérieur nécessitant une mémorisation avant usage. L’usage d’un rituel se décline en trois étapes : l’apprentissage, la mémorisation et l’utilisation.", - [ - "Apprentissage : Au moment de choisir l’habileté rituel, le joueur choisit une école de spécialisation parmi les suivantes : élémentaire, protection, nécromancie, démonologie ou naturelle. Il choisit ensuite 2 des 3 rituels présentés pour son école aux pages 12 et 13. Il peut aussi apprendre l’école de création avec la technique infusion, cette école répondant à des règles particulières présentées en page 13. Il existe aussi plusieurs rituels à apprendre en jeu. Tous les rituels et leurs détails doivent être inscrits dans un livre de sort (ou l’équivalent).", - "Mémorisation : Pour mémoriser un rituel, le joueur doit produire une cérémonie spécifique pour chacun de ces rituels connus. Ces cérémonies doivent cumuler autant de points de protocole que le nombre de composantes magiques nécessaires pour faire la cérémonie. La façon d’obtenir des points de protocole est décrite à l’annexe 2 : points de protocole en page 15. La cérémonie de mémorisation peut seulement être accomplie hors combat.", - "Utilisation : Une fois le rituel mémoriser, le joueur doit prononcer la formule de 12 syllabes qu’il a composée pour son rituel, en suivant les mêmes règles que pour tout autre sort. Il applique l’effet de son rituel selon sa description." - ], - "Note : les objets baguettes et baguettes enchantées permettent à un mage de mémoriser instantanément le dernier rituel mémorisé en payant son coût en composantes. Une baguette est détruite après usage, alors qu’une baguette enchantée peut être utilisée une fois par combat. L’usage de baguettes successives mémorisent toujours le même rituel." - ] - } - ] - }, - { - "title": "Effets de jeu", - "title_html": "Effets de jeu Super important!!!", - "description": [ - [ - "Assommé : tombe inconscient jusqu’à la fin des hostilités. Après 2 minutes de calme, le joueur revient à ses esprits sans énergie et avec 2 points de vie.", - "Aveugle : ferme les yeux pour 5 secondes.", - "Caché : ne peut être vu. Le joueur doit croiser les bras devant lui. Toute personne l’ayant vu croiser les bras est immunisée à l’effet.", - "Calme : enragé et peur prennent fin et sont sans effet tant que l’effet perdure.", - "Cassé : l’arme ou le bouclier doit être rangé. Il peut être réparé par la technique forgeron hors-combat pour un point d’énergie.", - "Démembré : le membre touché est inutilisable. Le regain d’un point de vie doit être consacré au membre pour le guérir.", - "Désarmé : jette son arme au sol ou la remet à sa ceinture (au choix de la cible).", - "Dominé : obéis aux ordres verbaux de l’initiateur.", - "Enragé : attaque la personne la plus près d’elle sans considération pour les autres. Prend fin quand le joueur ou sa cible est assommé.", - "Foudroyé : place un genou au sol.", - "Hors-combat : ne peut être utilisé si le joueur peut entendre une bataille.", - "Immatériel : est invisible et intangible. Le joueur croise les bras au-dessus de sa tête.", - "Mort : tombe au sol et ne prend aucune action. Voir le karma et la mort pour plus de détails.", - "Muet : perd l’usage de la parole. Revient au moment d’un repos.", - "Paralysé : ne peut pas bouger.", - "Peur : s’éloigne de l’initiateur au meilleur de ses capacités pour 10 pas.", - "Repos : méditation de 5 minutes hors-combat à la fin de laquelle le joueur reprend l’ensemble de ses points de vie (sauf ceux liés à l’armure) et d’énergie.", - "Nécrose : enlève un point de vie maximum. Cumulable." - ] - ] - }, - { - "title": "Objets de jeu et objets personnels", - "description": [ - "Il est possible de dépouiller un joueur assommé, mort ou paralysé. Pour se faire, il faut chercher le corps du joueur pour la représentation de cet objet et le déposer à côté de lui. Par exemple, une armure magique peut être dérobée en retirant l’armure du cadavre. Si un joueur traine des potions, il faudrait trouver ses récipients. Un objet sans représentation physique décorum (comme un bloc) est représenté par son carton. Une fois l’objet déposé aux côtés du corps, le joueur remet les cartons d’objet à son détrousseur, si applicable. Il n’est jamais permis de cacher des possessions dans ses sous-vêtements. Chaque joueur est responsable de représenter lui-même ses objets de jeu. Par conséquent, si un joueur vole une arme magique, il doit lui-même fournir une arme règlementaire pour la représenter.", - "Les objets remis par l’organisation, à l’exception des papiers d’objets, doivent toujours être redonnés à l’organisation à la fin du jeu (en personne ou au cabanon de rangement)." - ] - } - ] - }, - { - "title": "Habiletés et Techniques", - "title_html": "Habiletés et techniques", - "description": [ - "Les habiletés déterminent les possibilités d’un personnage en jeu. Chaque joueur peut puiser ses habiletés parmi 5 disciplines : commune, combattante, sournoise, magique et professionnelle.", - "Chaque habileté donne accès à 5 techniques selon deux modèles :", - "

Linéaire [l]

", - "Les techniques sont acquises dans l’ordre présenté. Elles ont une progression numérique régulière affectant une statistique ou une production précise.", - "

Choix [c]

", - "Chaque point d’expérience investi dans l’habileté permet de choisir une technique parmi les 5.", - "Les techniques représentent les possibilités du joueur en jeu. L’usage d’une technique coute toujours 1 point d’énergie. Ceci inclus, mais n’est pas limité à, l’usage d’une sauvegarde, la production d’un objet, annoncer une technique, mais rater sa cible, faire usage d’un rituel ou désarmer un piège.", - "

Légende

", - [ - "[Production] génère 4 blocs de production du nom de la technique à chaque jeu.", - "[Touche] frappe avec une arme à l’endroit précisé.", - "[Dague] frappe avec une arme de mêlée de moins de 60 cm.", - "[Sauvegarde] contre l’effet spécifié. Le joueur doit dire le mot «sauvegarde».", - "[Touché] main libre touche la cible.", - "[Balle] main libre, arme de bois ou une balle touche la cible.", - "[Passif] toujours active, ne nécessitant pas d’énergie ou d’activation.", - "[Toxine] il est nécessaire d’appliquer une toxine sur sa lame pour utiliser l’habileté." - ] - ], - "section": [ - { - "title": "Discipline Commune", - "section": [ - { - "title": "Endurance [l]", - "description": [ - [ - "+1 point de vie maximum", - "+1 point de vie maximum", - "+1 point de vie maximum" - ] - ] - }, - { - "title": "Énergie [l]", - "description": [ - [ - "+2 points d’énergie maximum", - "+2 points d’énergie maximum", - "+2 points d’énergie maximum" - ] - ] - } - ] - }, - { - "title": "Discipline Combattante", - "section": [ - { - "title": "Discipline [c]", - "description": [ - [ - "Résilience [passif] n’a pas à simuler la douleur en combat", - "Endurcie [passif] peur est remplacé par foudroyé", - "Loyauté [passif] sous domination, peut refuser de trahir sa faction", - "Doctrine [passif] choisit une cible qu’il peut atteindre en mêlée lorsqu’enragé", - "Vigilance [pointe la cible et attend 5 secondes] perce l’effet caché" - ] - ] - }, - { - "title": "Karma [l]", - "description": [ - [ - "+2 points de karma au début du jeu", - "+2 points de karma au début du jeu", - "+2 points de karma au début du jeu", - "+2 points de karma au début du jeu", - "+2 points de karma au début du jeu" - ] - ] - }, - { - "title": "Offense [c]", - "description": [ - [ - "Assaut [touche au torse] +1 dégât physique", - "Jambette [touche à la jambe] foudroyé", - "Désarmement [touche à l’avant-bras] désarmé", - "Coupe-souffle [touche au ventre] muet", - "Charge [après une course de 3 pas] +1 dégât physique" - ], - "Assaut", - { - "type": "image", - "src": "static/resources/media/assaut.png" - }, - "Jambette", - { - "type": "image", - "src": "static/resources/media/jambette.png" - }, - "Désarmement", - { - "type": "image", - "src": "static/resources/media/désarmement.png" - }, - "Coupe-souffle", - { - "type": "image", - "src": "static/resources/media/coupe-souffle.png" - } - ] - }, - { - "title": "Défense [c]", - "description": [ - [ - "Esquive [sauvegarde] foudroyé", - "Déflexion [sauvegarde] désarmé", - "Déviation [sauvegarde] démembré", - "Santé [sauvegarde] nécrose", - "Second Souffle [sauvegarde] assommé (sauf si à 0 point de vie)" - ] - ] - } - ] - }, - { - "title": "Discipline Sournoise", - "section": [ - { - "title": "Alchimie [l]", - "description": [ - [ - "Alchimie [production] poison, toxine et venin", - "+2 blocs de production alchimique", - "+2 blocs de production alchimique", - "+2 blocs de production alchimique", - "+2 blocs de production alchimique" - ] - ] - }, - { - "title": "Embuscade [c]", - "description": [ - [ - "Camouflage [immobile] peut se cacher en forêt", - "Dissimulation [immobile] peut se cacher dans le noir", - "Capture [avec une corde] la cible ne peut faire usage d’évasion", - "Piège [attacher les deux extrémités du piège] permet de poser un piège", - "Aveuglement [Toxine, Dague au torse] aveuglé" - ] - ] - }, - { - "title": "Fourberie [c]", - "description": [ - [ - "Attaque sournoise [touche au dos] +1 de dégât physique. Cumulatif avec venin.", - "Coup bas [Toxine, Dague à un rein] paralysé 5 secondes", - "Coup sonnant [Toxine, Dague entre les omoplates] assommé", - "Coupe-jarret [Dague derrière la jambe] démembré", - "Stylet [Dague à la main] démembré." - ], - "Coup bas", - { - "type": "image", - "src": "static/resources/media/coup_bas.png" - }, - "Coup sonnant", - { - "type": "image", - "src": "static/resources/media/coup_sonnant.png" - }, - "Coupe-jarret", - { - "type": "image", - "src": "static/resources/media/coupe-jarret.png" - }, - "Stylet", - { - "type": "image", - "src": "static/resources/media/stylet.png" - } - ] - }, - { - "title": "Travail de précision [c]", - "description": [ - [ - "Serrurier [touché, 30 secondes] ouvre un cadenas", - "Évasion [30 secondes sans bouger ou être touché, sauvegarde] paralysé", - "Désamorçage [touché, 30 secondes] désarme et détruit un piège", - "Torture [touché, 30 secondes] doit répondre à une question (pas un effet de vérité)", - "Vol à la tire : place un objet sans être vu dans une poche. Peut prendre le contenu de la poche ou y déposer un objet au choix." - ] - ] - } - ] - }, - { - "title": "Discipline Magique", - "section": [ - { - "title": "Artisanat Arcane [c]", - "description": [ - [ - "Mixture de potions [production] potions", - "Enchantement [production] objets enchantés", - "Infusion [passif] accès aux rituels de création", - "Réparation [touché] répare une armure ou un objet cassé", - "Disjonction [touché, 1 composante] retire l’aspect enchanté. Mets fin à un rituel." - ] - ] - }, - { - "title": "Rituel [l]", - "description": [ - [ - "+6 composantes au début du jeu", - "+3 composantes au début du jeu", - "+3 composantes au début du jeu", - "+3 composantes au début du jeu", - "+3 composantes au début du jeu." - ] - ] - }, - { - "title": "Sorcellerie [c]", - "description": [ - [ - "Frénésie [balle] enragé", - "Terreur [balle] apeuré", - "Noirceur [balle] aveuglé", - "Silence [balle] muet", - "Éclair [balle] 3 points de dégât." - ] - ] - }, - { - "title": "Thaumaturgie [c]", - "description": [ - [ - "Guérison [touché] redonne 1 PV", - "Réanimation [touché] réveille d’assommé avec 0 point de vie et tous ses membres", - "Résurrection [touché, hors combat] passe de mort à assommé.", - "Liberté [touché] met fin à la paralysie", - "Voix [touché] met fin à muet." - ] - ] - } - ] - }, - { - "title": "Discipline Professionnel", - "section": [ - { - "title": "Baratin [c]", - "description": [ - [ - "Diplomatie : permet de savoir si la dernière affirmation est vraie", - "Mensonge [sauvegarde] vérité et diplomatie", - "Revenu [passif] génère 10 piécettes par jeu", - "Verbomoteur [sauvegarde] muet", - "Discours [parle à la cible pendant 30 secondes] met fin à dominer" - ] - ] - }, - { - "title": "Marchandage [l]", - "description": [ - [ - "4 blocs de production ou composantes, au choix, par jeu", - "+2 blocs de production ou composantes, au choix, par jeu", - "+2 blocs de production ou composantes, au choix, par jeu", - "+2 blocs de production ou composantes, au choix, par jeu", - "+2 blocs de production ou composantes, au choix, par jeu" - ] - ] - }, - { - "title": "Médecine [c]", - "description": [ - [ - "Opération [touché, 30 secondes] retire une nécrose", - "Suture [touché, 30 secondes] redonne 2 points de vie", - "Psychiatrie [touché] la cible devient calme", - "Relaxation [touché, 5 secondes] met fin à muet", - "Pharmacie [touché, 5 secondes] peut utiliser les médicaments" - ] - ] - }, - { - "title": "Métier [c]", - "description": [ - [ - "Artisanat [production] filet, corde, baguette, fortification et piège.", - "Forge [production] arme , armure, bouclier, cadenas et engin", - "Herboristerie [production] médicaments", - "Spécialiste I : +2 blocs de production à une production existante (sauf alchimie)", - "Spécialiste II : +2 blocs de production à une production existante (sauf alchimie)." - ] - ] - } - ] - } - ] - }, - { - "title": "Production", - "description": [ - "Pour produire un objet, un joueur doit écrire sur son bloc de production l’objet qu’il désire créer selon les listes de la présente section. Il doit dépenser un point d’énergie et il doit être hors-combat.", - "Il existe deux types d’objet :", - [ - "Consommable [c]: l’objet est détruit après usage. L’objet de jeu est représenté par son carton lors d’un dépouillement", - "Permanent [p] : l’objet n’est pas détruit par l’usage. L’objet de jeu est représenté par une représentation crédible appartenant au joueur lors d’un dépouillement. Au moment de déposer la représentation au côté du propriétaire, le voleur peut réclamer le carton de jeu à son propriétaire." - ] - ], - "section": [ - { - "title": "Alchimie", - "description": [ - "Il est généralement nécessaire d’appliquer un produit alchimique sur sa lame en la frottant 5 secondes avec une main libre pour en faire usage.", - [ - "Poison [c] réduit de 2 points l’énergie de tous ceux qui ingèrent la boisson ou la nourriture contaminée jusqu’à ce qu’un antidote soit administré", - "Toxine [c] nécessaire à l’utilisation de certaines techniques sournoises.", - "Venin [c] ajoute 1 point de dégât sur la prochaine touche faite avec l’arme. Cumulatif avec attaque sournoise" - ] - ] - }, - { - "title": "Artisan", - "description": [ - [ - "Baguette [c] : permet, une fois par combat, de payer le coût en composante du dernier rituel mémorisé pour le mémoriser de nouveau. Usage unique", - "Corde [p] : permet de paralyser une cible désarmée. Peut être détruit par une lame", - "Filet [p] : permet de paralyser une cible dont on couvre la tête du filet. Peut être détruit par une lame.", - "Fortification [c] : redonne un point de vie à une forteresse. Une forteresse peut avoir jusqu’à 5 points de vie, mais commence le jeu avec 3 points ", - "Piège [c] corde avec une clochette : Inflige 2 points de dégât à qui touche la corde quand la clochette sonne." - ] - ] - }, - { - "title": "Mixture de potions", - "description": [ - "Utiliser une potion demande de boire une gorgée dans un contenant rempli d’un liquide quelconque que le joueur transporte sur lui.", - [ - "Fortitude [c] : donne une sauvegarde contre le poison", - "Guérison [c] : guérit 1 point de vie", - "Sagesse [c] : redonne 2 points d’énergie" - ] - ] - }, - { - "title": "Forgeron", - "description": [ - [ - "Armure [c] : permet au forgeron de réparer une armure en combat en 15 secondes", - "Arme [c] : remplace une arme cassée", - "Bouclier [c] : remplace un bouclier cassé", - "Cadenas et clef [p] : empêche l’ouverture d’un coffre sans la clef", - "Engin de siège [p] : inflige 1 point de dégât à une fortification. Une personne touchée par un projectile d’engin de siège est morte. Représenté par une baliste, une catapulte ou un bélier fonctionnel et sécuritaire." - ] - ] - }, - { - "title": "Herboriste", - "description": [ - "Appliquer un médicament demande de toucher la cible avec une main libre pour 5 secondes et d’avoir la technique pharmacie.", - [ - "Baume [c] : guérit 2 points de vie", - "Injection [c] : guérit assommé", - "Antidote [c] : contre les effets d’un poison." - ] - ] - }, - { - "title": "Enchantement", - "description": [ - "Voici la liste des objets sujets à l’enchantement et l’effet associé.", - [ - "Armure [p] : +1 point de vie à la valeur de l’armure. Ne peut plus être utilisée pour faire une réparation", - "Arme [p] : une sauvegarde contre cassé par combat", - "Baguette [p] : peut être utilisée une fois par combat", - "Bouclier [p] : une sauvegarde contre cassé par combat", - "Cadenas [p] : ne peut être ouvert par la technique serrurier", - "Corde [p] : ne peut être détruit par une lame (le nœud doit être défait pour libérer la cible)", - "Engin de siège [p] : inflige 2 points de dégât à une fortification", - "Filet [p] : ne peut être détruit par une lame (le filet doit être retiré de sur sa cible)", - "Fortification [p] : peut être fortifié jusqu’à 10 points de vie." - ] - ] - } - ] - }, - { - "title": "Rituels de départ", - "description": [ - "Au moment de choisir l’habileté rituel, un personnage peut inscrire deux des trois rituels disponibles à son école dans son livre de sort. Il peut faire de même pour l’école de création quand il choisit la technique infusion. Les rituels sont présentés comme suit. Le nombre entre parenthèses est le coût en composantes. Le nom du rituel suit. L’information entre crochets précise la durée et la portée du sort si applicable. La description conclue." - ], - "section": [ - { - "title": "Démonologie", - "description": [ - [ - "(4) Possession [jeu, personnel] le personnage accepte un démon en sa chaire. Il doit inclure une mutation à son costume ainsi qu’un motif rouge à son visage. Le joueur choisit une technique parmi toutes les habiletés qui lui sont disponibles et l’ajoute à son personnage.", - "(1) Dissipation [touché] met fin à tous les sorts non permanents sur la cible.", - "(1) Sacrifice [personnel] l’initiateur peut convertir ses points d’énergie actuels en points de vie, jusqu’à son maximum habituel. Il peut aussi choisir de transformer ses points de vie en énergie, toujours jusqu’à son maximum usuel." - ] - ] - }, - { - "title": "Élémentaire", - "description": [ - [ - "(2) Peau de pierre [jeu, personnel] l’initiateur doit porter un maquillage gris sur sa peau. La peau de l’initiateur est considérée comme une armure conférant 4 points de vie. Elle se répare avec une composante pendant un repos.", - "(1) Combustion [touché] cassé.", - "(1) Armes élémentaires [personnel] +1 point de dégât aux 2 prochaines touches." - ] - ] - }, - { - "title": "Naturelle", - "description": [ - [ - "(4) Armure d’écorces [jeu, personnel] la peau se couvre d’écorces, conférant une armure de 3 points de vie. Le joueur doit porter un maquillage brun ou une représentation d’écorces sur son costume. Réparer l’armure d’écorce coûte un repos.", - "(1) Séisme [à vue] la forteresse désignée reçoit 3 points de dégât.", - "(1) Camouflage [touché] caché tant qu’immobile." - ] - ] - }, - { - "title": "Nécromancie", - "description": [ - [ - "(4) Nécromorphose [jeu, personnel] l’initiateur doit porter un maquillage (un motif visible) ou un masque blanc. L’initiateur peut discuter avec les morts. Il est aussi immunisé à la peur et à la domination.", - "(1) Animation [touché, joueur mort] revient à la vie en tant que zombie sous la domination de l’initiateur avec 5 points de vie et aucune énergie. Un zombie est sans intelligence et ne peut pas communiquer, utiliser une technique ou retenir d’informations. Quand un zombie tombe assommé ou que 15 minutes se sont écoulées, le joueur se rend au cimetière où il revient à la vie selon les règles normales, mais sans coût de karma.", - "(1) Décomposition [touché] nécrose." - ] - ] - }, - { - "title": "Protection", - "description": [ - [ - "(2) Aura [jeu, personnel] l’initiateur est baigné d’un halo de lumière. Il doit porter un symbole blanc et jaune au visage. Sa peau est considérée comme une armure conférant 2 points de vie et se répare avec le sort réparation.", - "(1) Bouclier [touché] la cible gagne un point de vie additionnel.", - "(1) Vérité [hors-combat] la cible doit répondre à la prochaine question par la vérité." - ] - ] - }, - { - "title": "Création", - "description": [ - "L’école de création est un art rituel expérimental. Il permet aux joueurs de créer des objets magiques permanents ou temporaires et certaines formes de bénédictions et de malédictions. Le facteur qui détermine cela est l’objet cible du rituel, qui peut être n’importe quel objet se trouvant dans la section production. La puissance du rituel est à la hauteur du nombre de composantes sacrifiées dans celui-ci, avec le nombre de points de protocole équivalent. Dans un rituel de création, il est possible de remplacer jusqu’à la moitié des composantes requises par des blocs de production. Ces blocs doivent tous être du même type, et orientent le résultat final du rituel. Aucun rituel de création ne fonctionne en dessous de 6 composantes, et un investissement de plus de 30 composantes devrait toujours être associé à une piste en jeu. Tous les détails de l’expérience sont consignés au livre de sort du joueur. L’effet du rituel est déterminé par l’organisation après l’expérience. Le joueur pourra créer le même objet avec le même rituel une fois la première expérience complétée.", - "NOTE : Les rituels de création n’offrent aucune garantie. Il est possible de composer deux rituels différents et de finir avec le même effet. Il est aussi possible de maudire son propre personnage à travers un rituel de création." - ] - } - ] - }, - { - "title": "Rituels - Création", - "description": [ - "Sous-écoles: Artisanat, Forgeron, Mélange et Liquide, Portail et Zone." - ], - "section": [ - { - "title": "Artisanat", - "description": "La sous école Artisanat regroupe les objets créés par l’artisan, en plus des vêtements, accessoires et contenants. Les artisanats magiques sont les outils de stratégie par excellence. Que ce soit pour un plan de kidnapping, un vol de haut niveau ou la sécurité d’un objet précieux, les artisanats magiques ont la solution.", - "section": [ - { - "title": "Baguette", - "description": [ - "(Baguette: Permet, une fois par combat, de payer le coût en composante du dernier rituel mémorisé pour le mémoriser de nouveau. Usage unique.)", - "Une baguette créé par un Rituel de Création permet de stocker un rituel[sort] et de s’en servir plus tard. N’importe qui peut utiliser une baguette «magique» une fois fabriquée. Pour se faire la baguette doit être la cible du Rituel désiré, le faire en suivant ses protocoles et en payer les composantes. Le fabricant de la baguette «magique» n’est pas obligé d’être celui qui connaît le rituel à infuser. Puis, la même baguette doit être au centre du Rituel de Création, en suivre les protocoles et payer 5 fois les composantes magiques du rituel infusé. Il est possible d’augmenter les charges uniques de la baguette en payant chaque fois, 5 fois les composantes magiques du rituel infusé." - ] - }, - { - "title": "Filet", - "description": [ - "(Filet: Permet de paralyser une cible dont on couvre la tête du filet. Peut être cassé par une lame.)", - [ - "6 - 7 composantes: filet avec effet sur capture", - [ - "Aveugle", - "Foudroyé", - "Désarmé." - ], - "8 - 9 composantes: filet avec effet sur capture", - [ - "Calme", - "Caché", - "Muet." - ], - "10 - 11 composantes:", - [ - "Dominé (note: la cible est paralysée, mais peut marcher)", - "Assommé", - "Immatérielle." - ], - "12 à 15 composantes: Filet avec une capacité extraordinaire", - [ - "Affecte l’immatériel", - "Donne une nécrose sur capture", - "Drain complet de l’énergie", - "Animation [consommable] un cadavre se lève en goule sous votre contrôle (10PV, aucune énergie)" - ], - "16 à 30 composantes: objets économiques et scénario", - [ - "Filet de pêche: génère [composantes/10] rations par jeu", - "Filet d’esclavagiste: génère [composantes/15] esclaves par jeu", - "Attrapeur de rêves: génère [composantes/10] blocs d’enchantement par jeu" - ] - ] - ] - }, - { - "title": "Corde", - "description": [ - "(Corde: Permet de paralyser une cible désarmée. Peut être détruite par une lame.)", - [ - "6 - 7 composantes: corde avec effet sur capture", - [ - "Aveugle", - "Foudroyé", - "Désarmé" - ], - "8 - 9 composantes: corde avec effet sur capture", - [ - "Calme", - "Caché", - "Muet" - ], - "10 - 11 composantes: corde avec effet sur capture", - [ - "Dominé (note: la cible est paralysée, mais peut marcher)", - "Assommé", - "Immatérielle." - ], - "12 à 15 composantes: Filet avec une capacité extraordinaire", - [ - "Affecte l’immatériel", - "Donne une nécrose sur capture", - "Drain complet de l’énergie" - ], - "16 à 30 composantes: objets économiques et scénario", - [ - "Corde du pendu: permet de tuer de façon permanente un joueur accusé dans une cours de justice légitime", - "Collet: génère [composante/10] rations par jeu", - "Corde infinie: génère [composantes/10] blocs artisanaux par jeu." - ] - ] - ] - }, - { - "title": "Fortification", - "description": [ - "(Fortification: Redonne un point de vie à une forteresse. Une forteresse peut avoir jusqu’à 5 points de vie, mais commence le jeu avec 3.)", - [ - "6 - 7 composantes: effet à usage unique affectant tout le monde à l’intérieur d’un bâtiment", - [ - "Guérison 2", - "Regain d’énergie 1", - "Dégât 1", - "Drain d’énergie 1." - ], - "8 - 9 composantes: effet à usage unique affectant tout le monde à l’intérieur d’une enceinte", - [ - "Guérison 2", - "Regain d’énergie 1", - "Dégât 1", - "Drain d’énergie 1." - ], - "10 - 11 composantes: effet à usage unique affectant tout le monde à l’intérieur d’un bâtiment", - [ - "Réanimation", - "Résurrection", - "Foudroyé", - "Nécrose" - ], - "12 - 13 composantes: effet à usage unique affectant tout le monde à l’intérieur d’une enceinte", - [ - "Réanimation", - "Résurrection", - "Foudroyé", - "Nécrose" - ], - "14 - 15 composantes: effet permanent affectant tout le monde prenant un repos dans le bâtiment", - [ - "Énergie maximum. Perdure jusqu’à utilisation", - "Répare l’armure (ne fonctionne pas sur les rituels d’armure)", - "+1 point de vie maximum. Perdure jusqu’à ce que perdu", - "Nécrose" - ], - "16 - 17 composantes: effet permanent affectant tout le monde prenant un repos dans l’enceinte", - [ - "Répare l’armure (ne fonctionne pas sur les rituels d’armure)", - "+1 point de vie maximum. Perdure jusqu’à ce que perdu", - "Nécrose" - ], - "18 - 19 composantes: effet permanent affectant tout le monde prenant un repos dans le bâtiment", - [ - "+1 énergie maximum. Perdure jusqu’à utilisation", - "-1 karma", - "Répare une armure rituelle." - ], - "20 - 30 composantes: transformation d’un bâtiment en commerce de niveau [(composantes - 15)/5]", - [ - "Distillerie", - "Forge", - "Atelier", - "Cercle runique", - "Laboratoire", - "Pharmacie." - ] - ], - "*Un bâtiment est une construction qui dispose d’un toit, de quatre murs et d’un plancher", - "**Une enceinte est une construction qui est ceinturée d’un mur, mais qui n’a pas nécessairement un toit. L’organisation peut désigner tout lieu comme étant réputé être une enceinte.", - "Par exemple: l’arène." - ] - }, - { - "title": "Piège", - "description": [ - "(Piège: corde avec une clochette. Inflige 2 points de dégât à qui touche la corde quand la clochette sonne)", - [ - "6 - 7 composantes: piège avec effet additionnel sur déclenchement (l’effet est sur un papier sur la corde)", - [ - "Désarmé", - "Foudroyé", - "Muet" - ], - "8 - 9 composantes: piège avec effet additionnel sur déclenchement (l’effet est sur un papier sur la corde)", - [ - "Démembré", - "Peur", - "Aveugle" - ], - "10 - 11 composantes: piège avec effet additionnel sur déclenchement (l’effet est sur un papier sur la corde)", - [ - "Enragé", - "Calme", - "Cassé" - ], - "12 - 15 composantes: piège avec effet additionnel sur déclenchement (l’effet est sur un papier sur la corde)", - [ - "Assommé", - "Paralysé (5 minutes)", - "Nécrose" - ], - "16 - 20 composantes: Pièges avec faculté extraordinaire", - [ - "Ne peut être désarmé", - "Inflige 4 de dégât", - "Affecte l’éthéré seulement (le piège est alors représenté par un laser)" - ], - "21 à 30 composantes: Pièges à usage économique ou alternatif.", - [ - "Fausse d’esclavagiste: génère [composantes/15] esclaves par jeu;", - "Piège de poche: si un token de vol à la tire est inséré dans la même poche que le piège, l’initiateur du vol à la tir tombe assommé", - "Contingence: permet de lier l’activation d’un rituel à un mot ou un geste." - ] - ] - ] - } - ] - }, - { - "title": "Forgeron", - "description": [ - "Cette sous-école rassemble les rituels affectant tous les objets de métal. Elle comprend également les rituels touchant à l’or, l’argent et les métaux spéciaux. La magie des bijoux provient généralement de cette sous-école." - ], - "section": [ - { - "title": "Armures", - "description": [ - "(Armure : permet au forgeron de réparer une armure en combat en 15 secondes.)", - [ - "6 - 18 composantes: effet permanent sur le porteur (c = nombre de composantes)", - [ - "+[c/6]karma au début du jeu", - "-[c/6]karma au début du jeu", - "+[c/6] points de vie (entre 6 et 9, +0)", - "+[c/6] énergie maximum (entre 6 et 9, +0);", - "+[c/6] nécroses;", - "-[c/6] énergie maximum;" - ], - "19 - 20 composantes: protège contre certains assauts", - [ - "Protection contre la possession", - "Protection contre les attaques venant de l’immatériel", - "Protection contre la perte d’objet quand le joueur devient immatériel" - ], - "21 - 30 composantes: armure à signification scénario", - [ - "Protection contre le psychisme", - "Protection contre un environnement hostile:" - ], - [ - "Zone de l’enfer:", - [ - "Abadon", - "Nirvana", - "Néposd" - ], - "Plan élémentaire (eau, terre air ou feu):", - [ - "Cosmos", - "Néant." - ], - "Protection contre les assauts cosmiques." - ] - ] - ] - }, - { - "title": "Armes", - "description": [ - "(Arme : remplace une arme cassée)", - [ - "6 - 7 composantes: imite une technique gratuitement, une fois par combat", - [ - "Attaque sournoise", - "Jambette", - "Désarmement" - ], - "8 - 9 composantes: imite une technique gratuitement, une fois par combat", - [ - "Assaut", - "Coupe-jarret;", - "Stylet" - ], - "10 - 11 composantes: donne accès à une technique, utilisant l’énergie du porteur", - [ - "Attaque sournoise", - "Jambette", - "Désarmement" - ], - "12 - 13 composantes: donne accès à une technique, utilisant l’énergie du porteur", - [ - "Assaut", - "Coupe-jarret", - "Stylet" - ], - "14 - 15 composantes: imite une technique gratuitement, une fois par combat (inclus la toxine)", - [ - "Aveuglement", - "Coup bas", - "Coup sonnant." - ], - "16 - 20 composantes: l’arme inflige 1 point de dégât additionnel sur un type de créature.", - [ - "Mort-vivant", - "Naturel (trucs druidiques)", - "Nature corrompue (Natueur et cie)", - "Démons", - "Gardiens", - "Anges." - ], - "21 - 30 composantes: Armes à effet sur une touche. Utilise l’énergie du joueur jusqu’à un maximum de [1+((c-20)/5)] fois par combat.", - [ - "Nécrose", - "Drain 1 (inflige 1 de dégât et regagne 1 point de vie)", - "-1 énergie sur la cible", - "Peur", - "Calme." - ] - ] - ] - }, - { - "title": "Bouclier", - "description": [ - "(Bouclier : remplace un bouclier cassé)", - [ - "6 - 7 composantes: imite une technique gratuitement, une fois par combat", - [ - "Esquive", - "Déflexion", - "Déviation" - ], - "8 - 9 composantes: imite une technique gratuitement, une fois par combat", - [ - "Santé", - "Second-souffle", - "Verbomoteur" - ], - "10 - 11 composantes: donne accès à une technique, utilisant l’énergie du porteur", - [ - "Esquive", - "Déflexion", - "Déviation" - ], - "12 - 13 composantes: donne accès à une technique, utilisant l’énergie du porteur", - [ - "Santé", - "Second-souffle", - "Verbomoteur" - ], - "14 - 15 composantes: le bouclier prend la forme d’un bijou. Donne accès une technique, utilisant l’énergie du porteur. ", - [ - "Mensonge", - "Évasion", - "Dissimulation", - "Camouflage" - ], - "16 - 20 composantes: Sauvegarde les rituels provenant d’une école de rituel, au coût d’un d’énergie.", - [ - "Protection", - "Création", - "Nécromancie", - "Démonologie", - "Élémentaire", - "Naturel." - ], - "21 - 30 composantes: sauvegarde contre les mécaniques de combat et scénario d’une nature.", - [ - "École rituel précise", - "Pouvoir de source infernal", - "Pouvoir de source divine", - "Pouvoir de source cosmique", - "Pouvoir de source druidique", - "Pouvoir de source naturelle corrompue" - ] - ] - ] - }, - { - "title": "Cadenas", - "description": [ - "(Cadenas et clef : empêche l’ouverture d’un coffre sans la clef)", - "Un cadenas magique ne peut être ouvert que par un mot de commande écrit dans une enveloppe dans le coffre qu’il sert à barrer. Pour crocheter le cadenas, il faut d’abord briser l’enchantement. Un mage avec infusion et disjonction doit ramasser un nombre de composantes égal au nombre cumulé pour faire le cadenas et faire un rituel cumulant autant de points de protocole.", - "Le cadenas redevient alors normal, et peut être crocheté. Le nombre de composantes investi dans le rituel de création du cadenas doit être clairement indiqué sur la représentation du cadenas. Dès qu’un cadenas magique est créé, la mécanique pour les briser doit faire partie du speech de départ. Le Poste de Traite informe aussi les voleurs curieux de la procédure." - ] - }, - { - "title": "Engin de siège", - "description": [ - "(Engin de siège : inflige 1 de dégât à une fortification. Une personne touchée par un projectile d’engin de siège est morte.)", - [ - "Inflige 1+[c/6] dégât à une fortification (entre 2 et 6). Cumulable avec l’effet d’un enchantement." - ] - ] - }, - { - "title": "Joyaux", - "description": [ - "(Joyau : effets scénario divers. Limite de 3 joyaux par personnage dans un même jeu)", - [ - "6 - 7 composantes: effet sur le porteur, instantané, une fois par jeu.", - [ - "+1 karma", - "Guérison 2 points", - "Regain de 2 d’énergie" - ], - "8 - 9 composantes: effets économique mineur", - [ - "Génère 1 bloc par jeu", - "Génère 10 pièces par jeu", - "Génère 1 composante par jeu" - ], - "10 - 11 composantes: effet au touché, instantané, une fois par jeu.", - [ - "Muet", - "Aveugle", - "Démembré." - ], - "12 - 13 composantes: technique octroyée au porteur", - [ - "Mensonge", - "Résilience", - "Loyauté" - ], - "14 - 15 composantes: effet permanent sur le porteur", - [ - "Protection contre l’animation", - "Protection contre la possession", - "+2 points de vie quand ne porte pas d’armure" - ], - "16 - 30 composantes: effet économique majeur", - [ - "Génère [c/10] blocs, au choix, par jeu", - "Génère 1 + [c/10] composantes par jeu", - "Génère [c/10] pierres d’âme par jeu;", - "Génère [c/5] pierres de sanglite par jeu", - "Génère [c/5] pierres de bluame par jeu", - "Génère [c/5] pierres de malachite par jeu" - ] - ] - ] - } - ] - }, - { - "title": "Mélanges et Liquides", - "description": [ - "Cette sous-école correspond aux habiletés d’alchimie, herboristerie et concoctions de potion. Les rituels à base d’un objet d'alchimie crée des maladies. Les rituels à base d’herboristerie crée des antidotes et des remèdes aux maladies magiques. Les rituels à base de potion sont quand à eux variés, certains permettent d'améliorer une potion à des niveaux de magie hallucinante, d’autres de provoquer des effets spectaculaires et uniques." - ], - "section": [ - { - "title": "Alchimie", - "description": [ - "(Ressources : champignon, venin ou plante. Poison: réduit de 2 l’énergie. Toxine: foudroie, Venin: ajoute 1 de dégât.)", - "Légende :", - [ - "[Sang] Transmise quand on touche le joueur assommé ou mort.", - "[Air] Transmise quand le joueur touche à main nue la peau d’un autre joueur.", - "[Salive] Transmise quand un joueur partage de la nourriture ou un breuvage.", - "[Famine] Un joueur est infecté pour chaque point en carence alimentaire dans la géopolitique (selon leur faction).", - "[6] Peste commune: [sang] 1 nécrose inopérable.", - "[6] Spore parasite: [air] - 2 à l’énergie.", - "[6] Grippe: [air] -2 au karma.", - "[8] Peste noire: [sang] 2 nécroses inopérables.", - "[8] Spore débilitant: [salive] -4 à l’énergie.", - "[8] Choléra: [salive] -4 au karma.", - "[10] Peste Figaro: [sang] 3 nécroses inopérables.", - "[10] Démence: [famine] -6 à l’énergie.", - "[10] Malédiction: [famine] -6 au karma.", - "[15] Virus de Valcenne: [air] 1 nécrose inopérable, -2 à l’énergie et au karma.", - "[15] Bactérie cannibale: [sang] 2 nécroses inopérables et -4 au karma.", - "[15] Décomposition nerveuse: [famine] 2 nécroses inopérables et -4 à l’énergie.", - "[15] Dépression: [famine] -4 karma et énergie.", - "[20] Lèpre: [sang] 2 nécroses inopérables, -4 à l’énergie et au karma.", - "[20] Cancer: [famine] 3 nécroses inopérables et -6 au karma.", - "[20] Bactérie nervophage: [salive] 3 nécroses inopérables et -6 à l’énergie.", - "[20] Nécrose de l’âme: [famine] -6 à l’énergie et au karma.", - "[30] Vengeance de Finosia: [sang] 3 nécroses inopérables, -6 au karma et à l’énergie.", - "[30] Drain de l’âme: [famine] Ne peut pas utiliser de techniques." - ] - ] - }, - { - "title": "Herboristerie", - "description": [ - "(Ressources : calmant, stimulant et contrepoison. Baume : guérit 2 points de vie. Injection : guérit assommé. Antidote : contrepoison.)", - "Les rituels avec les médicaments permettent de guérir les gens des maladies créées par les rituels de création sur l’alchimie. La force doit être équivalente à celle utilisée pour créer la maladie. Le rituel crée une recette qu’un herboriste peut utiliser pour synthétiser un bloc de production en remède pour la maladie précise. Chaque fois que le rituel est répété, il est possible d’enseigner la recette à un autre herboriste.", - "Le rituel va toujours donner le bon remède si le joueur peut identifier la maladie et la puissance nécessaire pour la révoquer. Un joueur peut aussi essayer de transformer un remède sans connaître de maladie. À ce moment, le joueur apprend une recette au hasard capable de soigner une des maladies de la liste alchimie. Appliquer la maladie la plus près en puissance." - ] - }, - { - "title": "Potions", - "description": [ - "(Ressources : philtre, poudre et essence. Fortitude: une sauvegarde poison. Guérison : guérit 1 pv. Sagesse : redonne 2 d’énergie.)", - "Les potions utilisées en rituel de création permettent de bénir un certain nombre de participants pour la durée du jeu, à moins d’avertissement contraire. Les avantages ne peuvent pas surpasser les maximums établis du système. Certains rituels font effet dès qu’ils sont terminés, d’autres permettent de garder l’effet en bouteille.", - "Légende:", - [ - "[bénédiction] L’effet du rituel est instantané et affecte le nombre de joueurs inscrits dans la description.", - "[bouteille] Le rituel se conserve en bouteille et doit être bu pour faire effet.", - "[6] Nuit liquide: [bouteille] Usage unique. Une fois lancée au sol, la fiole libère un nuage de noirceur qui aveugle toutes les cibles dans un rayon de 5 pas de la bouteille. (Utiliser une bouteille incassable)", - "[6] Bénédiction I: [bénédiction] +1 point de vie pour 3 personnes.", - "Méditation I: [bénédiction] +1 énergie pour 3 personnes.", - "[6] Prière I: [bénédiction] +2 karma pour 3 personnes.", - "[8] Bénédiction I: [bénédiction] +1 point de vie pour 6 personnes.", - "[8] Méditation I: [bénédiction] +1 énergie pour 6 personnes.", - "[8] Prière I: [bénédiction] +2 karma pour 6 personnes.", - "[10] Bénédiction I: [bénédiction] +1 point de vie pour 9 personnes.", - "[10] Méditation I: [bénédiction] +1 énergie pour 9 personnes.", - "[10] Prière I: [bénédiction] +2 karma pour 9 personnes.", - "[15] Bénédiction II: [bénédiction] +2 points de vie pour 3 personnes.", - "[15] Méditation II: [bénédiction] +2 énergie pour 3 personnes.", - "[15] Prière II: [bénédiction] +4 karma pour 3 personnes.", - "[20] Bénédiction II: [bénédiction] +2 points de vie pour 6 personnes.", - "[20] Méditation II: [bénédiction] +2 énergie pour 6 personnes.", - "[20] Prière II: [bénédiction] +4 karma pour 6 personnes.", - "[25] Bénédiction II: [bénédiction] +2 points de vie pour 9 personnes.", - "[25] Méditation II: [bénédiction] +2 énergie pour 9 personnes.", - "[25] Prière II: [bénédiction] +4 karma pour 9 personnes.", - "[20] Bénédiction III: [bénédiction] +3 points de vie pour 3 personnes.", - "[20] Méditation III: [bénédiction] +3 énergie pour 3 personnes.", - "[20] Prière III: [bénédiction] +6 karma pour 3 personnes.", - "[30] Bénédiction III: [bénédiction] +3 points de vie pour 6 personnes.", - "[30] Méditation III: [bénédiction] +3 énergie pour 6 personnes.", - "[30] Prière III: [bénédiction] +6 karma pour 6 personnes.", - "[40] Bénédiction III: [bénédiction] +3 points de vie pour 9 personnes.", - "[40] Méditation III: [bénédiction] +3 énergie pour 9 personnes.", - "[40] Prière III: [bénédiction] +6 karma pour 9 personnes.", - "[30] Gloire: [bénédiction] +1 point de vie, +1 d’énergie et +2 karma pour tous les participants.", - "[30] Pouvoir de Brokrand: [bénédiction] +2 points de vie et +2 d’énergie pour tous les participants.", - "[30] Lumière d’Eccani: [bénédiction] +2 d’énergie et +4 karma pour tous les participants.", - "[30] Force de Nox: [bénédiction] +2 points de vie et +4 karma pour tous les participants." - ] - ] - } - ] - } - ] - }, - { - "title": "Rituel", - "description": "Les rituels sont divisés en 5 écoles et sous-école : Démonologie, Élémentaire, Nature, Nécromantie et Protection.", - "section": [ - { - "title": "Démonologie", - "under_level_color": "panel-danger", - "section": [ - { - "title": "Possession", - "section": [ - { - "description": [ - "Augmente les points de vie à 10, donne accès à la technique Décapitation. ", - [ - "Coût : 6", - "Durée : 1 combat", - "Type : Rage" - ], - "Frénésie non-sauvegardable" - ], - "title": "Le dévoreur de tête" - }, - { - "description": [ - "Confère 4 point de vie de type armure. Réparable avec une composante magique (hors-combat).", - [ - "Coût : 12", - "Durée : 24h", - "Type : Bonus" - ], - "Silence durant toute la possession et maquillage gris ou masque." - ], - "title": "La dame de fer" - }, - { - "description": [ - "Donne les techniques: Assaut, Résilience, Endurcie et Coup-bas pour la durée du rituel. 1 composante par bénéficiaire.", - [ - "Coût : 4+\n", - "Durée : 1 combat", - "Type : Bonus" - ], - "Frénésie non-sauvegardable. 1 crâne(mort) par bénéficiaire." - ], - "title": "Le crâne rieur" - }, - { - "description": [ - "Donne les 5 techniques de l'habileté Sorcellerie. Permet de faire 2 techniques de Sorcellerie gratuitement par combat.", - [ - "Coût : 25", - "Durée : 24h", - "Type : Bonus" - ], - "Le maximum de point de vie possible du personnage est maintenant de 8 (et non 10)." - ], - "title": "Sorcier des enfers" - }, - { - "description": [ - "Permet une fois/combat de faire un Aveuglement de masse. Immunise à aveuglement.", - [ - "Coût : 12", - "Durée : 24h", - "Type : Bonus" - ], - "Doit éviter la lumière en tout temps." - ], - "title": "L'Ombre noire" - }, - { - "description": [ - "Donne accès à une technique Sournoise (max 5)", - [ - "Coût : (# habileté) X 6", - "Durée : 24h", - "Type : Bonus" - ], - "Les techniques ne peuvent pas changer durant la possession." - ], - "title": "Diablotin" - }, - { - "description": [ - "Donne accès à une technique Combattante (max 5)", - [ - "Coût : (# habileté) X 6", - "Durée : 24h", - "Type : Bonus" - ], - "Les techniques ne peuvent pas changer durant la possession." - ], - "title": "Calambin" - }, - { - "description": [ - "Donne accès à une technique Magique (max 5)", - [ - "Coût : (# habileté) X 6", - "Durée : 24h", - "Type : Bonus" - ], - "Les techniques ne peuvent pas changer durant la possession." - ], - "title": "Arcaniste" - }, - { - "description": [ - "Donne accès à une technique Professionnelle (max 5)", - [ - "Coût : (# habileté) X 6", - "Durée : 24h", - "Type : Bonus" - ], - "Les techniques ne peuvent pas changer durant la possession." - ], - "title": "Incube" - } - ] - }, - { - "title": "Contrat", - "section": [ - { - "description": [ - "Permet de signer un contrat aux conséquences magiques pour 2 personnes.", - [ - "Coût : 4", - "Durée : 1 saison max", - "Type : Deal" - ], - "Conséquence pré-établie." - ], - "title": "Contrat Mineur" - }, - { - "description": [ - "Permet de signer un contrat aux conséquences magiques pour 4 personnes ou moins.", - [ - "Coût : 8", - "Durée : 1 saison max", - "Type : Deal" - ], - "Conséquence pré-établie." - ], - "title": "Contrat Moyen" - }, - { - "description": [ - "Permet de signer un contrat aux conséquences magiques pour 10 personnes ou moins.", - [ - "Coût : 16", - "Durée : 1 saison max", - "Type : Deal" - ], - "Conséquence pré-établie." - ], - "title": "Contrat Majeur" - }, - { - "description": [ - "Permet de signer un contrat aux conséquences magiques pour 2 factions.", - [ - "Coût : 30", - "Durée : 1 saison max", - "Type : Deal" - ], - "Conséquence pré-établie." - ], - "title": "Contrat de Faction" - }, - { - "description": [ - "Permet de créer un contrat de paiement auto-magiquement effectué.", - [ - "Coût : 6", - "Durée : Payment total", - "Type : Deal" - ], - "Conséquence pré-établie. Paiement au début du jeu à l'acceuil sinon vous subissez les conséquences." - ], - "title": "Contrat monétaire" - }, - { - "description": [ - "Permet de tuer Permanent un joueur de SA faction après un procès.", - [ - "Coût : 50", - "Durée : Mort Permanente", - "Type : Effet" - ] - ], - "title": "Arrêt de Mort" - }, - { - "description": [ - "Permet de rendre une page magique pour chaque 4 composantes investies. Chaque signature sur ces pages permet de poser une question hors-jeu sur le joueur.", - [ - "Coût : x4", - "Durée : 1 utilisation", - "Type : Bonus" - ], - "Avoir la vraie signature sur un papier physique. Le papier doit être enchanté avant de recevoir la signature. " - ], - "title": "Signature" - } - ] - } - ] - }, - { - "title": "Élémentaire", - "under_level_color": "panel-warning", - "section": [ - { - "title": "Terre", - "section": [ - { - "description": [ - "La cible est paralysée et immunisée aux dégâts jusqu’à ce qu’elle subisse 6 touches ou que 10 minutes se soient écoulées.", - [ - "Coût : 6", - "Durée : 1 utilisation", - "Type : Effet" - ], - "Toucher" - ], - "title": "Pétrification" - }, - { - "description": [ - "Permet de générer 1 bloc de Forgeron, +1 bloc par composante utilisée (maximum 6 composantes pour 5 blocs).", - [ - "Coût : 2+", - "Durée : Instantanée", - "Type : Production" - ] - ], - "title": "Prospection" - }, - { - "description": [ - "Transforme des blocs d'Artisant en bloc de Forgeron (max 20)", - [ - "Coût : 3", - "Durée : Instantanée", - "Type : Production" - ] - ], - "title": "Minéralisation" - }, - { - "description": [ - "Donne accès à l'habileté Sorcellerie: Gravité pour la durée du rituel. Effet: Foudroyé.", - [ - "Coût : 12", - "Durée : Permanent", - "Type : Bonus" - ] - ], - "title": "Gravité" - }, - { - "description": [ - "Permet d'achever ses victimes sans perdre de point de karma (maximum de 10 achevements).", - [ - "Coût : 8", - "Durée : 1 combat", - "Type : Bonus" - ], - "-3 karma au lancement du sort." - ], - "title": "Coeur de pierre" - }, - { - "description": [ - "Rend immunisé à Foudroyé ou tout effet déplaçant le personnage contre son gré.", - [ - "Coût : 2", - "Durée : 1 combat", - "Type : Bonus" - ] - ], - "title": "Jambe de roc" - }, - { - "description": [ - "Permet de faire tomber une météorite à proximité. La météorite peut être composée de Bluam, de Malachite ou de Sanglite, mais il n'y a aucune manière de le choisir.", - [ - "Coût : 25", - "Durée : Instantanée", - "Type : Effet" - ], - "Doit avertir l'organisation avant de faire le rituel. Doit inclure un feu d'artifice" - ], - "title": "Pluie de météore" - } - ] - }, - { - "title": "Feu", - "section": [ - { - "description": [ - "Donne la capacité de produire de la lumière avec un objet décorum.", - [ - "Coût : 1", - "Durée : 24h", - "Type : Bonus" - ] - ], - "title": "Lumière" - }, - { - "description": [ - "Détruit un objet ni magique, ni enchanté. N’affecte pas les bâtiments (forteresse, commerce, etc…).", - [ - "Coût : 1", - "Durée : 1 utilisation", - "Type : Bonus" - ] - ], - "title": "Incinération" - }, - { - "description": [ - "Permet de lancer 3 projectiles qui font 3 points de dégât chacun.", - [ - "Coût : 2", - "Durée : 1 utilisation", - "Type : Bonus" - ] - ], - "title": "Rayons Ardents" - }, - { - "description": [ - "Permet d'infliger 1 point de dégât à toute personne se trouvant sous le même toît.", - [ - "Coût : 3", - "Durée : 1 utilisation", - "Type : Effet" - ] - ], - "title": "Fumigation" - }, - { - "description": [ - "Permet d'infliger 3 points de dégât à toute personne se trouvant sous le même toît.", - [ - "Coût : 8", - "Durée : 1 utilisation", - "Type : Effet" - ], - "Doit utiliser un fumigène." - ], - "title": "Fumée Ardente" - }, - { - "description": [ - "Donne la technique Sorcellerie:Boule de feu pour la durée du rituel. Effet: 3 points de dégât", - [ - "Coût : 3", - "Durée : 1 combat", - "Type : Bonus" - ], - "Doit se maquiller les mains rouges." - ], - "title": "Boule de Feu" - }, - { - "description": [ - "Permet de détruire un cadavre et de l'envoyer directement au cimetière.", - [ - "Coût : 2", - "Durée : 1 utilisation", - "Type : Effet" - ], - "Doit utiliser un corps mort (achevé)." - ], - "title": "Crémation" - }, - { - "description": [ - "Permet à la fin d'un combat de brûler tous les cadavres d'un champ de bataille, les envoyant tous directement au cimetière.", - [ - "Coût : 25", - "Durée : 1 utilisation", - "Type : Effet" - ], - "Doit utiliser un corp mort (achevé)." - ], - "title": "Bûcher Funéraire" - } - ] - }, - { - "title": "Air", - "section": [ - { - "description": [ - "Donne la technique Sorcellerie:Bourrasque pour la durée du rituel. Effet: la cible recule de 10 pas (comme terreur, mais pas de sauvegarde).", - [ - "Coût : 3", - "Durée : 1 combat", - "Type : Bonus" - ] - ], - "title": "Bourrasque" - }, - { - "description": [ - "Au toucher, permet de Paralyser une cible, le lanceur et la personne ciblée sont paralysées. Le lanceur peut mettre fin au sort quand il le désire.", - [ - "Coût : 2", - "Durée : 1 utilisation", - "Type : Bonus" - ] - ], - "title": "Contact Paralysant" - }, - { - "description": [ - "Le lanceur inflige 2 points de dégât avec une arme de son choix. Cet effet ne peut être jumelé avec aucune autre technique ou rituel, incluant les techniques demandant de cibler avec une arme tel qu’assaut, jambette ou attaque sournoise.", - [ - "Coût : 6", - "Durée : 1 combat", - "Type : Bonus" - ], - "L'arme doit porter un foulard ou un ruban jaune." - ], - "title": "Électrification" - }, - { - "description": [ - "Permet d'envoyer un message à n'importe quel NPC.", - [ - "Coût : 4", - "Durée : 1 lettre", - "Type : Effet" - ], - "Doit fournir une lettre de maximum 100 mots." - ], - "title": "Messager du vent" - }, - { - "description": [ - "Crée un cercle de protection contre les projectiles de 2 mètres de diamètre. Tous ceux dans la zone reçoivent toujours 0 point de dégât des projectiles, incluant les sort de dégât avec balle.", - [ - "Coût : 5", - "Durée : cercle", - "Type : Effet" - ], - "Le cercle doit rester intact." - ], - "title": "Colonne de Vent" - } - ] - }, - { - "title": "Eau", - "section": [ - { - "description": [ - "Permet de générer 1 bloc de Potion, +1 bloc par composante utilisée (maximum 6 composantes pour 5 blocs).", - [ - "Coût : 2+", - "Durée : 1 utilisation", - "Type : Production" - ] - ], - "title": "Infusion magique" - }, - { - "description": [ - "Le lanceur est Immatériel tant qu’il suit un cours d’eau.", - [ - "Coût : 1", - "Durée : 1 utilisation", - "Type : Effet" - ], - "Doit rester à 1m ou moins du cours d'eau." - ], - "title": "Forme Aquatique" - }, - { - "description": [ - "Affecte un contenant. Si une personne boit le liquide, il meurt sur le champ. Jeté sur la peau, le liquide inflige 3 points de dégât. N’affecte pas plus que l’équivalent d’un verre de la taverne.", - [ - "Coût : 5", - "Durée : 1 utilisation", - "Type : Effet" - ], - "Doit obligatoirement être un liquide H-J." - ], - "title": "Fiole d'Acide" - }, - { - "description": [ - "Une lance devient en glace et frappe maintenant de 2 points de dégât.", - [ - "Coût : 5", - "Durée : 1 combat", - "Type : Bonus" - ], - "La lance doit porter un foulard ou un ruban bleu." - ], - "title": "Lance de Glace" - }, - { - "description": [ - "Au toucher la cible du sort est Paralysée pendant 30 secondes.", - [ - "Coût : 3", - "Durée : 1 utilisation", - "Type : Effet" - ] - ], - "title": "Toucher Glacial" - }, - { - "description": [ - "Rend une personne immunisée à l'achèvement.", - [ - "Coût : 5", - "Durée : 1 combat", - "Type : Bonus" - ] - ], - "title": "Coeur de Glace" - } - ] - } - ] - }, - { - "title": "Nature", - "under_level_color": "panel-success", - "section": [ - { - "title": "Métamorphose", - "section": [ - { - "description": [ - "Donne l'équivalent de 2 armes (armes naturelles) qui ne peuvent être désarmées. Peut être réparé par un sort de guérison.", - [ - "Coût : 6", - "Durée : Permanent", - "Type : Bonus" - ], - "Implique une modification au costume." - ], - "title": "Appel du totem" - } - ] - }, - { - "title": "Métamorphose", - "section": [ - { - "description": [ - " +1 point de vie ou 1 sauvegarde par combat contre la paralysie.", - [ - "Coût : 10", - "Durée : Permanent", - "Type : Bonus" - ], - "Implique une modification au costume." - ], - "title": "Incarnation totémique" - }, - { - "description": [ - [ - "Ailes/Queue : sauvegarde Foudroyé", - "Bec/Museau : +2 blocs d'Alchimie par jeu", - "Yeux perçant : Donne la technique Vigilance", - "Branchies [croise les bras] : peut se cacher quand il touche à un court d’eau", - "Diurne: Donne la technique Camouflage", - "Nocturne: Donne la technique Dissimulation", - "Herbivore : + 2 blocs d’Herboristerie par jeu", - "Carnivore : + 1 ration par jeu", - "Omnivore : + 2 blocs d'Artisanat par jeu." - ], - [ - "Coût : 20", - "Durée : Permanent", - "Type : Bonus" - ], - "Implique une modification au costume." - ], - "title": "Aspect animal" - }, - { - "description": [ - "Donne un deuxième pouvoir selon la liste.", - [ - "Coût : 35", - "Durée : Permanent", - "Type : Bonus" - ], - "Implique une modification au costume." - ], - "title": "Aspect animal 2" - }, - { - "description": [ - "Le joueur est de type animal en permanence. Il est sujet à la magie naturelle affectant les animaux. Il gagne 1 point de vie additionnel et la capacité de supprimer son apparence animale à volonté. Quand il reprend une forme humaine, le druide perd accès à ses habiletés de métamorphose, incluant l’apothéose.", - [ - "Coût : 50", - "Durée : Permanent", - "Type : Bonus" - ], - "Implique une modification au costume." - ], - "title": "Apothéose" - } - ] - }, - { - "title": "Flore", - "section": [ - { - "description": [ - "Permet de générer 1 bloc d’herboristerie, +1 bloc par composante utilisée (maximum 6 composantes pour 5 blocs).", - [ - "Coût : 2+", - "Durée : Instantanée", - "Type : Production" - ] - ], - "title": "Pousses Prodigieuses" - }, - { - "description": [ - "Permet de générer 1 bloc d'Alchimie, +1 bloc par composante utilisée (maximum 6 composantes pour 5 blocs).", - [ - "Coût : 2+", - "Durée : Instantanée", - "Type : Production" - ] - ], - "title": "Toxicité" - }, - { - "description": [ - "+1 ration par jeu.", - [ - "Coût : 20", - "Durée : Permanent", - "Type : Production" - ], - "Doit avoir des éléments végétaux sur le costume." - ], - "title": "Forme Végétale" - }, - { - "description": [ - "Donne 2 blocs d'Herboristerie et 2 blocs d'Alchimie", - [ - "Coût : 20", - "Durée : Permanent", - "Type : Production" - ], - "Doit avoir des éléments végétaux sur le costume." - ], - "title": "Forme Fongique" - }, - { - "description": [ - "Confère 4 points de vie de type armure. Réparable avec une composante magique (hors-combat).", - [ - "Coût : 12", - "Durée : Permanent", - "Type : Production" - ], - "*(Prérequis)Forme Végétale ou Fongique" - ], - "title": "Armure végétale" - }, - { - "description": [ - "Donne la technique Camouflage.", - [ - "Coût : 1", - "Durée : Jusqu'à un déplacement", - "Type : Bonus" - ], - "Rester immobile" - ], - "title": "Camouflage" - }, - { - "description": [ - "Permet de poser une question à un végétal légendaire (ex: la racine mère ou le père champignon), pour connaître un rituel végétal, une recette alchimique ou d'herboristerie.", - [ - "Coût : 15", - "Durée : Instantanée", - "Type : Effet" - ], - "Ne peux être fait qu'une fois par jeu." - ], - "title": "Communion avec les plantes" - } - ] - }, - { - "title": "Faune", - "section": [ - { - "description": [ - "Donne une apparence animal à une cible. La cible peut maintenant être affectée par les sorts qui affectent les animaux.", - [ - "Coût : 3", - "Durée : 1 combat", - "Type : Bonus" - ], - "Costume obligatoire" - ], - "title": "Métamorphose temporaire" - }, - { - "description": [ - "Permet d'apposer une marque de chasseur. La cible de la marque ne peut plus utiliser de technique ou de pouvoir pour devenir invisible ou dissimulé.", - [ - "Coût : ", - "Durée : Jusqu'à ce que la cible soit assomée", - "Type : Bonus" - ], - "Toucher" - ], - "title": "Chasse" - }, - { - "description": [ - "Immunise à l'effet Aveugle.", - [ - "Coût : 8", - "Durée : Jeu", - "Type : Bonus" - ] - ], - "title": "Flaire" - }, - { - "description": [ - "Permet de faire un rugissement; Tout le monde qui entend le rugissement est Apeuré (Peur). (Après avoir crié très fort)", - [ - "Coût : 25", - "Durée : Instantanée", - "Type : Effet" - ] - ], - "title": "Rugissement" - } - ] - } - ] - }, - { - "title": "Nécromantie", - "under_level_color": "panel-primary", - "section": [ - { - "title": "Animation", - "section": [ - { - "description": [ - "Permet d'animer un squelette. Le squelette a 3 points de vie.", - [ - "Coût : 1", - "Durée : Jusqu'à la mort", - "Type : Animation" - ], - "Doit utiliser un corp mort (achevé). Le mort est détruit une fois à 0 pv et va directement au cimetière." - ], - "title": "Squelette" - }, - { - "description": [ - "Permet d'animer une goule. La goule a 10 points vie et peut utiliser ses armes.", - [ - "Coût : 2", - "Durée : Jusqu'à la mort", - "Type : Animation" - ], - "Doit utiliser un corp mort (achevé). Le mort est détruit une fois a 0 pv et va directement au cimetière." - ], - "title": "Goule" - }, - { - "description": [ - "Comme une goule, mais avec 2 points d'énergie en plus. Peut récupérer 1 point d'énergie en mangeant une victime pendant 5 secondes. Obtient assault.", - [ - "Coût : 3", - "Durée : Jusqu'à la mort", - "Type : Animation" - ], - "Doit utiliser un corp mort (achevé). Le mort est détruit une fois a 0 pv et va directement au cimetière." - ], - "title": "Goule cannibale" - }, - { - "description": [ - "Permet d'animer une momie. Les statistiques d'une momie sont les mêmes que le joueur animé en momie.", - [ - "Coût : 5", - "Durée : Jusqu'à la mort", - "Type : Animation" - ], - "Doit utiliser un corp mort (achevé). Le mort est détruit une fois a 0 pv et va directement au cimetière." - ], - "title": "Momie" - }, - { - "description": [ - "Permet de questionner un joueur mort permanent.", - [ - "Coût : 2", - "Durée : 5 minutes", - "Type : Animation" - ], - "aucun" - ], - "title": "Ancêtre" - }, - { - "description": [ - "Ramène en mort-vivant un esclave qui conserve sa production.", - [ - "Coût : 10", - "Durée : Permanent", - "Type : Production" - ], - "Doit posséder un esclave mort" - ], - "title": "Recyclage d'esclave" - }, - { - "description": [ - "Transforme un cadavre en spectre capable de passer au travers des murs, d'entendre et de parler, mais qui n'est pas invisible et qui ne peut pas faire autre chose. ", - [ - "Coût : 5", - "Durée : 15 minutes", - "Type : Animation" - ], - "Doit utiliser un corp mort (achevé)." - ], - "title": "Spectre" - }, - { - "description": [ - "Comme un spectre, mais complètement invisible. De plus le fantôme ne peut pas mourir permanent de cette façon.", - [ - "Coût : 8", - "Durée : 15 minutes", - "Type : Animation" - ], - "Doit utiliser un corp mort (achevé)." - ], - "title": "Esprit Fantôme" - }, - { - "description": [ - "Permet à un animateur de relever tous les personnages morts ou assomés en Momie.", - [ - "Coût : ----", - "Durée : Jusqu'à la mort", - "Type : Animation" - ], - "Doit utiliser un corp mort (achevé). Le mort est détruit une fois a 0 pv et va directement au cimetière." - ], - "title": "Cimetière de liche" - }, - { - "description": [ - "Permet d'invoquer le Golem d'Ossuaire.", - [ - "Coût : QUEST", - "Durée : Jusqu'à la mort", - "Type : Animation" - ], - "Ajoute les victimes à sa structure." - ], - "title": "Nécronaute" - } - ] - }, - { - "title": "Magie Noire", - "section": [ - { - "description": [ - "Permet de détruire 3 mort-vivants pour créer 10 composantes nécromantiques.", - [ - "Coût : 5", - "Durée : Instantanée", - "Type : Production" - ], - "Doit avoir 3 mort-vivants sous son contrôle ou celui des assistants." - ], - "title": "Nécro-ingénérie" - }, - { - "description": [ - "Permet de devenir mort-vivant (doit choisir 3 pouvoirs dans la classe Maître Undead). Permet aussi d'obtenir un phylactère et de ne plus souffrir d'un manque de Karma.", - [ - "Coût : 120", - "Durée : Instantanée", - "Type : Bonus" - ], - "Devient mort-vivant" - ], - "title": "Liche" - }, - { - "description": [ - "Drain de 3 points de vie et de 3 d'énergie.", - [ - "Coût : 6", - "Durée : 1 utilisation", - "Type : Bonus" - ], - "-2 karma au lanceur du sort" - ], - "title": "Toucher Vampirique" - }, - { - "description": [ - "Effet: Mort (balle). La cible peut être réssuscitée", - [ - "Coût : 8", - "Durée : 1 utilisation", - "Type : Effet" - ], - "-1 karma au lanceur du sort" - ], - "title": "Rayon Mortel" - }, - { - "description": [ - "Les personnages protégés par ce sort qui sont assommés et qui se font achever, explosent en tuant leur assassin.", - [ - "Coût : (# cibles) X 5", - "Durée : Jusqu'à effet du sort", - "Type : Bonus" - ], - "Le joueur qui explose et son assassin vont au cimetiere" - ], - "title": "Mort Piegée" - }, - { - "description": [ - "Le rituel permet de tracer un cercle de 2 mètres de diamètre. Tous les personnages ayant l'habileté Rituel (Nécromancie) reçoivent alors 5 points d'énergie en Bonus et une sauvegarde contre un effet affectant le Karma.", - [ - "Coût : 15", - "Durée : Cercle", - "Type : Bonus" - ], - "Doit rester dans le cercle." - ], - "title": "Cercle du mal" - }, - { - "description": [ - "Permet de renforcer une arme en la rendant mortelle pour les humains. L'arme cause 2 points de dégât aux créatures vivantes.", - [ - "Coût : 4", - "Durée : 1 combat", - "Type : Bonus" - ], - "L'arme doit porter un tissu blanc comme un voile." - ], - "title": "Lance du mal" - }, - { - "description": [ - "Permet de poser une question à un mort sans que celui-ci ne puisse mentir ou utiliser Mensonge. 4 composantes pour poser une question supplémentaire (max 5 questions).", - [ - "Coût : 4+", - "Durée : 1 question", - "Type : Effet" - ], - "Doit utiliser un corps mort (achevé)" - ], - "title": "Communion avec les morts" - }, - { - "description": [ - "Transforme 5 esclaves en 1 ration.", - [ - "Coût : 5 (sacrifice)", - "Durée : Instantanée", - "Type : Production" - ], - "Détruit 5 esclaves" - ], - "title": "La moulé pour les chien" - }, - { - "description": [ - "Donne une nécrose.", - [ - "Coût : 1", - "Durée : 1 utilisation", - "Type : Effet" - ], - "Toucher" - ], - "title": "Décomposition" - }, - { - "description": [ - "Donne 2 nécroses.", - [ - "Coût : 2", - "Durée : 1 utilisation", - "Type : Effet" - ], - "Toucher" - ], - "title": "Putréfaction" - }, - { - "description": [ - "Donne 3 nécroses.", - [ - "Coût : 3", - "Durée : 1 utilisation", - "Type : Effet" - ], - "Toucher, -1 karma au lanceur." - ], - "title": "Kyste" - }, - { - "description": [ - "Détruit une nécrose et regagne 1 point de vie et 1 point d'énergie.", - [ - "Coût : 1", - "Durée : 1 utilisation", - "Type : Effet" - ] - ], - "title": "Décomposeur" - }, - { - "description": [ - "Dévore un cadavre et récupère 3 points de vie (1 minute).", - [ - "Coût : 1", - "Durée : 1 utilisation", - "Type : Effet" - ], - "La victime doit être morte avant de la dévorer." - ], - "title": "Charognard" - } - ] - } - ] - }, - { - "title": "Protection", - "under_level_color": "panel-info", - "section": [ - { - "title": "Bénédiction", - "section": [ - { - "description": [ - "Pour le prochain combat les sorts de guérison font 2.", - [ - "Coût : 4", - "Durée : 1 combat", - "Type : Bonus" - ] - ], - "title": "Imposition des mains 1" - }, - { - "description": [ - "Pour le prochain combat les sorts de guérison font 3.", - [ - "Coût : 10", - "Durée : 1 combat", - "Type : Bonus" - ] - ], - "title": "Imposition des mains 2" - }, - { - "description": [ - "Pour le prochain combat les sorts de guérison font 4.", - [ - "Coût : 20", - "Durée : 1 combat", - "Type : Bonus" - ] - ], - "title": "Imposition des mains 3" - }, - { - "description": [ - "Donne la technique Sorcellerie: Rayon de vie 2 point de vie pour la durée du rituel.", - [ - "Coût : 3", - "Durée : 1 combat", - "Type : Bonus" - ], - "Doit toucher avec une balle" - ], - "title": "Rayon de vie 1" - }, - { - "description": [ - "Donne la technique Sorcellerie: Rayon de vie 3 points de vie pour la durée du rituel.", - [ - "Coût : 7", - "Durée : 1 combat", - "Type : Bonus" - ], - "Doit toucher avec une balle" - ], - "title": "Rayon de vie 2" - }, - { - "description": [ - "Donne la technique Sorcellerie: Rayon de vie 4 points de vie pour la durée du rituel.", - [ - "Coût : 15", - "Durée : 1 combat", - "Type : Bonus" - ], - "Doit toucher avec une balle" - ], - "title": "Rayon de vie 3" - }, - { - "description": [ - "Permet de retirer tous les effets de jeu sur une cible.", - [ - "Coût : 3", - "Durée : 1 utilisation", - "Type : Heal" - ], - "Toucher" - ], - "title": "Restauration" - }, - { - "description": [ - "Permet de récupérer 1 Karma après le rituel. 1 composante / participant.", - [ - "Coût : 1+", - "Durée : Instantanée", - "Type : Bonus" - ], - "Minimum 15 minutes. Minimum 6 personnes" - ], - "title": "Cérémonie" - }, - { - "description": [ - "Permet de récupérer 3 Karma après le rituel. 3 composantes / participant.", - [ - "Coût : 3+", - "Durée : Instantanée", - "Type : Bonus" - ], - "Minimum 15 minutes. Minimum 6 personnes" - ], - "title": "Messe" - }, - { - "description": [ - "Donne une sauvegarde contre l'effet Dominé.", - [ - "Coût : 3", - "Durée : 1 utilisation", - "Type : Bonus" - ] - ], - "title": "Barrière mental" - }, - { - "description": [ - "Permet d'être immunisé à l'effet Dominé.", - [ - "Coût : 15", - "Durée : 1 combat", - "Type : Bonus" - ], - "Doit avoir une pierre collée dans le front." - ], - "title": "Protection Mental" - } - ] - }, - { - "title": "Barrière", - "section": [ - { - "description": [ - "Permet de faire prisonnier, un strône, un squelette, ou une créature végétale.", - [ - "Coût : 8", - "Durée : cercle", - "Type : Paralyse" - ], - "Le cercle doit rester intact." - ], - "title": "Cercle de confinement 1" - }, - { - "description": [ - "Permet de faire prisonnier, un gardien, un dry-arbre, ou une goule.", - [ - "Coût : 16", - "Durée : cercle", - "Type : Paralyse" - ], - "Le cercle doit rester intact." - ], - "title": "Cercle de confinement 2" - }, - { - "description": [ - "Permet de faire prisonnier, un demon, un illithide, ou une liche.", - [ - "Coût : 32", - "Durée : cercle", - "Type : Paralyse" - ], - "Le cercle doit rester intact." - ], - "title": "Cercle de confinement 3" - }, - { - "description": [ - "Permet de faire prisonnier n'importe quoi.", - [ - "Coût : 64", - "Durée : cercle", - "Type : Paralyse" - ], - "Le cercle doit rester intact." - ], - "title": "Cercle de confinement 4" - }, - { - "description": [ - "Cercle de 2 mètres où tous les sorts de soins voient leurs Effets doubler.", - [ - "Coût : 5", - "Durée : cercle 5 minutes", - "Type : Bonus" - ], - "Le cercle doit rester intact." - ], - "title": "Cercle de vie" - }, - { - "description": [ - "Permet de créer une zone où la mort ne peut être réversible. Le cercle est aussi grand que la corde noire utilisée lors du rituel.", - [ - "Coût : 12", - "Durée : cercle 1 combat", - "Type : Effet" - ], - "Le cercle doit rester intact." - ], - "title": "Cercle de mort" - }, - { - "description": [ - "Crée une zone personnelle paralysant la cible, mais la protégeant des 10 prochaines touches. La personne protégée doit compter les touches reçu à voix haute. La cible peut parler, mais ni ses bras, ni ses jambe ne peuvent bouger.", - [ - "Coût : 2", - "Durée : 1 utilisation", - "Type : Effet" - ] - ], - "title": "Cercle de protection personnel" - }, - { - "description": [ - "Crée une zone personnelle paralysant la cible, mais la protégeant des 10 prochaines touches. La personne protégée doit compter les touches reçues à voix haute. La cible peut parler, mais ni ses bras, ni ses jambes ne peuvent bouger. Chaque personne supplémentaire coûte 2 composantes au moment de préparer le rituel. (Max 5).", - [ - "Coût : 3+", - "Durée : 1 utilisation", - "Type : Effet" - ] - ], - "title": "Cercle de protection personnel, de groupe" - }, - { - "description": [ - "Permet de créer une zone de 3 mètres de diamètre où toutes les choses invisibles ou immatérielles sont dévoilées.", - [ - "Coût : 6", - "Durée : 1 utilisation", - "Type : Effet" - ] - ], - "title": "Cercle de dévoilement" - } - ] - } - ] - } - ] - }, - { - "title": "Techniques de maître", - "description": [ - "Les techniques de maître sont des techniques qui peuvent être choisi qu'après avoir reçu une formation en-jeu avec un PNJ. Un personnage peut avoir dix techniques de maître sur sa fiche de personnage en plus des espaces offertes par les habiletés complètes (5 talents)." - ], - "section": [ - { - "title": "Discipline commune", - "description": [ - "On peut choisir Endurance et Énergie deux autre fois supplémentaire." - ], - "section": [ - { - "title": "Endurance [l]", - "description": [ - [ - "+1 point de vie maximum", - "+1 point de vie maximum" - ] - ] - }, - { - "title": "Énergie [l]", - "description": [ - [ - "+2 points d’énergie maximum", - "+2 points d’énergie maximum" - ] - ] - } - ] - }, - { - "title": "Discipline combattante", - "section": [ - { - "title": "Discipline [c]", - "description": [ - [ - "Lévitation [passif] immunisé à foudroyé", - "Formation [Dire «formation»] Entre 3 et 6 joueurs prennent position dans une formation. Tant qu’ils tiennent cette formation, ils ont 1 point de vie maximum supplémentaires", - "Survie [passif] génère une ration par jeu. Est considéré comme ayant suture.", - "Entretien [sauvegarde] cassé." - ] - ] - }, - { - "title": "Karma [l]", - "description": [ - [ - "Bénédiction [passif] +3 karma", - "Phoenix [-5 karma] le joueur revient à la vie avec toute son énergie et tous ses points de vie", - "Sacrifice [passif] peut échanger 1 point de karma contre 1 point d’énergie", - "Sanguinaire [passif] Quand le personnage perd du karma, il est guérit d’un point de vie" - ] - ] - }, - { - "title": "Offense [c]", - "description": [ - [ - "Décapitation [touche au plexus et touche au dos] mort", - "Brise-Bouclier [sur une charge de 3 pas] bouclier touché est cassé", - "Casse-Lame [3 touches consécutives sur l’arme] arme touchée est cassée", - "Titan [passif] Arme à deux mains inflige 2 de dégât (sauf les lances)" - ] - ] - }, - { - "title": "Défense [c]", - "description": [ - [ - "Robuste [passif] +1 point de vie", - "Volonté de fer [sauvegarde] domination", - "Estomac d’acier [sauvegarde] poison et maladies transmises par la salive", - "Santé de fer [sauvegarde] maladies transmises par l’air, le sang ou la famine" - ] - ] - } - ] - }, - { - "title": "Discipline sournoise", - "section": [ - { - "title": "Alchimie [l]", - "description": [ - [ - "Alchimiste prolifique [passif] +3 blocs alchimiques", - "Alchimiste versatile [passif] peut utiliser un bloc d’herboristerie comme un bloc d’alchimie", - "Suicide [passif] à tout moment, le joueur peut choisir d’être mort", - "Distillerie [production] potions" - ] - ] - }, - { - "title": "Embuscade [c]", - "description": [ - [ - "Nocturne [passif] +1 point de vie la nuit", - "Maître des pièges [sauvegarde] pièges et toiles", - "Commotion [sur une personne assommée] oublie les 5 dernières minutes", - "Déguisement [passif] permet d’avoir un autre costume et d’affirmer être un autre personnage" - ] - ] - }, - { - "title": "Fourberie [c]", - "description": [ - [ - "Main de fer [sur une poignée de main] démembré", - "Égorgement [couteau passé sous la gorge] mort Bloqué par l’administration", - "Étouffement [dague au torse] muet", - "Feinte [sauvegarde] touche (ne fonctionne que sur les attaques normales, ne bloquent pas les techniques, ni les rituels)" - ] - ] - }, - { - "title": "Travail de précision [c]", - "description": [ - [ - "Chanceux [passif] génère 6 blocs au choix par jeu", - "Escroc [passif] génère 50 piécettes par jeu", - "Orfèvre [production] joyau", - "Triche [instantané] permet d’avoir une chance additionnelle dans un jeu de hasard" - ] - ] - } - ] - }, - { - "title": "Discipline magique", - "section": [ - { - "title": "Artisanat Arcane [c]", - "description": [ - [ - "Chimiste prolifique [passif] +3 blocs de production de potions", - "Chimiste versatile [passif] peut utiliser les blocs d’enchantement pour faire des potions", - "Enchanteur prolifique [passif] +3 blocs de production d’enchantement", - "Enchanteur versatile [passif] peut utiliser les blocs de potion pour enchanter" - ] - ] - }, - { - "title": "Rituel [l]", - "description": [ - [ - "Versatilité I [passif] donne accès à une sous-écoles d’une école de magie différente de la spécialité du joueur. Il doit utiliser des composantes de cette école de magie pour invoquer les rituels.", - "Versatilité II [passif] idem", - "Versatilité III [passif] idem", - "Maître des rituels [passif] +5 composantes par jeu" - ] - ] - }, - { - "title": "Sorcellerie [c]", - "description": [ - [ - "Télékinésie [balle] la cible recule de 10 pas", - "Tonnerre [balle] 2 de dégât, foudroyé", - "Sorcier débrouillard[passif] peut utiliser un bloc de potion pour lancer un sort de sorcellerie", - "Désintégration [balle à un membre] démembré" - ] - ] - }, - { - "title": "Thaumaturgie [c]", - "description": [ - [ - "Réincarnation [touché] ramène un mort à la vie avec 3 points de vie et 0 énergie", - "Guérison suprême [touché] guérit 2 points de vie", - "Guérisseur débrouillard [passif] peut utiliser un bloc d’enchantement pour lancer un sort de thaumaturgie", - "Restauration [touché] régénère les 4 membres" - ] - ] - } - ] - }, - { - "title": "Discipline professionnelle", - "section": [ - { - "title": "Baratin [c]", - "description": [ - [ - "Alcoolique [passif] une consommation redonne un point de vie", - "Triche [instantané] permet d’avoir une chance additionnelle dans un jeu de hasard", - "Rumeurs [passif] le joueur obtiens quelques informations sur 3 scènes du jeu suivant. Il obtient notamment l’heure approximative, les factions impliquées, et une idée de l’évènement principal", - "Éloquence [suite à un discours] Les joueurs défendant la même cause que l’initiateur reçoivent +1 énergie jusqu’à ce que la cause ait été défendue une fois" - ] - ] - }, - { - "title": "Marchandage [l]", - "description": [ - [ - "Marchand prolifique [passif] +3 bloc au choix par jeu", - "Échange [passif] le marchand peut échanger n’importe quel deux blocs ou composantes pour un autre type de bloc ou une composante magique", - "Contrebande [passif] chaque jeu, le joueur peut participé à une enchère avant le jeu avec les autres escrocs pour des objets prédéfinies", - "Caravane [passif] génère 100 pièces par jeu", - "Noblesse [passif] génère 50 pièces par jeu" - ] - ] - }, - { - "title": "Médecine [c]", - "description": [ - [ - "Techniques médicales [passif] permet d’apprendre les techniques de médecine avancées. Chacun doit être apprise en jeu lors d’un séminaire donné par un acteur.", - [ - "Diagnostic [Stravinsky] Permet à un joueur ayant l’habileté médecine de faire un diagnostique complet d’un patient en passant 15 minutes avec lui. Après la session, le joueur doit révéler au médecin l’ensemble des effets qui pèsent sur lui, incluant tous les effets passifs qui lui sont conférés par des objets ou des techniques. Si le joueur est ignorant d’une condition pesant sur lui, le docteur considère qu’il ne l’a simplement pas découverte. Un diagnostique ne peut pas être perpétrer sur une personne contre son gré.", - "Psychanalyse [Stravinsky] Un patient ayant fait ses consultations régulières pour 3 jeux consécutifs est assez près de son médecin pour recevoir ses conseils psychologiques. Chaque jeu, le médecin peut poser une question à son patient qui doit lui répondre la vérité. Le médecin infère cette information des discussions et observations sur son patient, l’habileté mensonge n’est donc pas efficace.", - "Autopsie [Stravinsky] permet de déterminer la cause de la mort d’un joueur" - ], - "Techniques chirurgicales [passif] permet d’apprendre les techniques de chirurgie avancées. Chacune doit être apprise en jeu lors d’un séminaire donné par un acteur.", - [ - "Amputation [Von Brett] le médecin peut retirer un membre d’un joueur mort ou assommé de façon propre et professionnelle. Si le docteur retire un bras, la cible ne peut plus se servir de ce bras avant d’être guérit magiquement (guérison est efficace)", - "Ablation [Von Brett] permet de retirer un organe à un joueur mort ou assommé. Un joueur assommé meurt à la fin de la procédure. Quand le joueur revient à la vie, il a une pénalité d’un d’énergie jusqu’à ce qu’il prend un remède.", - "Transfusion [Von Brett] permet de faire une transfusion sanguine entre deux joueurs. Les potions, poisons et maladies qui affectent le joueur donateur sont transmises au joueur receveur. Toute potion ou remède administré 5 minutes avant la transfusion prend effet dans le corps receveur aussi." - ], - "Recherche pharmacologique [passif] le joueur apprend les recettes avancées de médicaments", - "Dextérité [passif] réduit le temps d’opération et suture de 10 secondes" - ] - ] - }, - { - "title": "Métier [c]", - "description": [ - [ - "Artisan versatile [passif] permet d’utiliser un bloc de forge comme un bloc d’artisanat, et inversement", - "Forgeron prolifique [passif] +3 blocs de forge", - "Artisan prolifique [passif] +3 blocs artisanaux", - "Herboriste prolifique [passif] +3 blocs d’herboristerie" - ] - ] - } - ] - } - ] - }, - { - "title": "Fortification", - "title_html": "Fortification Nouveau", - "description": [ - "La résistance d’une porte, que ce soit une porte de forteresse ou de taverne, dépend du nombre de bloc de production (bp) artisanat : Fortification.", - "1 porte de Forteresse : 50 bp artisanat : Fortification maximum", - "1 porte de bâtiment : 10 bp artisanat : Fortification maximum,", - "1 bp artisanat : Fortification = 1 point de vie de fortification.", - "1 bp artisanat : Fortification Enchantée = 2 points de vie de fortification.", - "1 bp forgeron : Engin de siège = 1 projectile d’arme de siège ou 1 coup de bélier.", - "* Un projectile d’arme de siège cause 1 point de dégât de fortification et fait le sortilège ''mort'' sur un personnage, sans possibilité de sauvegarde. Le projectile doit absolument avoir été tiré par un engin de siège décorum. Un coup de bélier cause 1 point de dégât de fortification.", - "1 bp forgeron : Engin de siège enchanté = 1 projectile enchanté d’arme de siège ou 1 coup de bélier enchanté**.", - "** Un projectile d’arme de siège enchanté cause 2 points de dégât de fortification et fait le sortilège ''mort'' sur un personnage, sans possibilité de sauvegarde. Le projectile doit absolument avoir été tiré par un engin de siège décorum. Un coup de bélier enchanté cause 2 points de dégât de fortification.**", - "Engin de siège en bluam : Permet de désenchanter une fortification. Une seule utilisation.", - "Engin de siège en sanglite : Permet de causer 50 points de dégât de fortification à une fortification. Une seule utilisation.", - "Engin de siège en malachite : Une fortification détruite par un engin de siège en malachite ne peut pas être reconstruite pour la durée d'une lune entière.", - "Rituels Inédits : Ces rituels sont uniques et disponibles uniquement en jeu. Ils affectent toutes les fortifications. Exemples: Mur de pierre, Mur de fer, Mur de force, Séisme, Trou Noir, Forteresse Immédiate Balmonts, Mur des enfer, Mur d’ombre. Forteresse de Bluam, Forteresse Végétale, ect.", - "Tous les rituels de création affectant une fortification peuvent prendre l’apparence d’un Symbole Magique. Celui-ci peut alors être activé par n’importe qui ayant participé au rituel. Ce Symbole doit mesurer minimum 30 cm." - ] - }, - { - "title": "Distribution de points de mérite", - "description": "Il est possible d'obtenir des points de mérite selon son implication sur l'activité de Traître-Lame. Dans cette section, on vous montre ce qu'on peut obtenir en échange de point(s) de mérite.", - "section": [ - { - "title": "Coût de 1 point de mérite", - "section": [ - { - "title": "Expérience", - "description": "Donne 1 point d'expérience sur la fiche du personnage actif." - }, - { - "title": "Karma", - "description": "Donne 10 point de Karma temporaire pour le jeu. (max 1/jeu)." - }, - { - "title": "Énergie Star", - "description": "Donne 4 points d'énergie temporaire pour le jeu. (max 1/jeu)." - }, - { - "title": "Money Bag", - "description": "Donne un bonus unique de 50 pièces d'or au début du jeu." - }, - { - "title": "Ritualiste", - "description": "Donne un bonus unique de 20 composantes magiques au début du jeu." - }, - { - "title": "Artisan", - "description": "Donne un bonus unique de 30 blocs de production au choix au début du jeu." - }, - { - "title": "Métal Rare", - "description": "Donne un bonus unique d'un lingot rare au choix au début du jeu." - } - ] - }, - { - "title": "Coût de 3 points de mérite", - "section": [ - { - "title": "Marché Public", - "description": "Donne accès a un commerce de Malédastarone gratuitement." - }, - { - "title": "Marché Tribale", - "description": "Donne accès a un commerce de la Sarsonne gratuitement." - }, - { - "title": "Money Chest", - "description": "Donne un bonus unique de 200 pièces d'or au début du jeu." - }, - { - "title": "Cercle de Mage", - "description": "Donne un bonus unique de 50 composantes magiques au début du jeu." - }, - { - "title": "Atelier en série", - "description": "Donne un bonus unique de 100 blocs de production au choix au début du jeu." - } - ] - }, - { - "title": "Coût de 5 points de mérite", - "section": [ - { - "title": "Marché Noir", - "description": "Donne accès a un commerce Illicite gratuitement." - }, - { - "title": "Appuis: Commercial", - "description": "Donne un bonus de 3 Appuis: Commercial au début du jeu. (Faction, Sous-Faction ou Personnelle)" - }, - { - "title": "Appuis: Militaire", - "description": "Donne un bonus de 3 Appuis: Militaire au début du jeu. (Faction, Sous-Faction ou Personnelle)" - }, - { - "title": "Appuis: Info", - "description": "Donne un bonus de 3 Appuis: Information au début du jeu. (Faction, Sous-Faction ou Personnelle)" - }, - { - "title": "Appuis: Politique", - "description": "Donne un bonus de 3 Appuis: Politique au début du jeu. (Faction, Sous-Faction ou Personnelle)" - }, - { - "title": "Appuis: Criminel", - "description": "Donne un bonus de 3 Appuis: Criminel au début du jeu. (Faction, Sous-Faction ou Personnelle)" - } - ] - }, - { - "title": "Coût de 10 points de mérite", - "section": [ - { - "title": "Résurection", - "description": "Permet de ramener un personnage mort permanent à la vie sans même avoir une miette du défunt." - }, - { - "title": "Coffre du Roi", - "description": "Donne un bonus de 500 pièces d'or, 5 joyaux et une couronne." - }, - { - "title": "Nexus de magie", - "description": "Donne un bonus unique de 150 composantes magiques et une sphère de magie." - }, - { - "title": "Atelier du Père Noël", - "description": "Donne un bonus de 200 blocs de production au choix et 3 lingots rares aux choix." - } - ] - } - ] - } - ] -} diff --git a/src/web/__main__.py b/src/web/__main__.py index d3c02927..0d4335ec 100644 --- a/src/web/__main__.py +++ b/src/web/__main__.py @@ -14,7 +14,6 @@ DB_DEFAULT_PATH = os.path.join(WEB_ROOT_DIR, "..", "..", "database", "tl_user.json") DB_DEMO_PATH = os.path.join(WEB_ROOT_DIR, "..", "..", "database", "demo_user.json") DB_MANUAL_PATH = os.path.join(WEB_ROOT_DIR, "..", "..", "database", "tl_manual.json") -DB_LORE_PATH = os.path.join(WEB_ROOT_DIR, "..", "..", "database", "tl_lore.json") DB_AUTH_PATH = os.path.join(WEB_ROOT_DIR, "..", "..", "database", "auth.json") GOOGLE_API_SECRET_PATH = os.path.join(WEB_ROOT_DIR, "..", "..", "database", "client_secret.json") CONFIG_PATH = os.path.join(WEB_ROOT_DIR, "..", "..", "database", "config.json") @@ -78,7 +77,8 @@ def parse_args(): help='Active to disable login module.') group.add_argument('--disable_admin', default=False, action='store_true', help='Active to disable admin module.') - group.add_argument('--disable_custom_css', default=False, action='store_true', + # TODO Force to disable this feature until it's improve + group.add_argument('--disable_custom_css', default=True, action='store_true', help='Active to disable custom css module.') group.add_argument('--hide_menu_login', default=False, action='store_true', help='Active to hide login module from menu.') @@ -86,7 +86,6 @@ def parse_args(): _parser = parser.parse_args() _parser.db_demo_path = DB_DEMO_PATH _parser.db_manual_path = DB_MANUAL_PATH - _parser.db_lore_path = DB_LORE_PATH _parser.db_auth_keys_path = DB_AUTH_PATH _parser.db_google_API_path = GOOGLE_API_SECRET_PATH _parser.db_config_path = CONFIG_PATH diff --git a/src/web/base_handler.py b/src/web/base_handler.py index 1e11f6f2..359c7b9e 100644 --- a/src/web/base_handler.py +++ b/src/web/base_handler.py @@ -9,7 +9,6 @@ class BaseHandler(tornado.web.RequestHandler): _debug = None _manual = None - _lore = None _db = None _invalid_login = None _redirect_http_to_https = None @@ -22,7 +21,6 @@ def initialize(self, **kwargs): self._debug = kwargs.get("debug") self._db = kwargs.get("db") self._manual = kwargs.get("manual") - self._lore = kwargs.get("lore") self._invalid_login = self.get_argument("invalid", default="disable_login" if kwargs.get("disable_login") else None) self._redirect_http_to_https = kwargs.get("redirect_http_to_https") diff --git a/src/web/bower.json b/src/web/bower.json index 204eb93e..bfed3523 100644 --- a/src/web/bower.json +++ b/src/web/bower.json @@ -26,6 +26,7 @@ "ngPrint": "master", "components-font-awesome": "master", "angular-qrcode": "master", - "jsSHA": "master" + "jsSHA": "master", + "angular-moment": "master" } } diff --git a/src/web/handlers.py b/src/web/handlers.py index 84b19336..9b58d256 100644 --- a/src/web/handlers.py +++ b/src/web/handlers.py @@ -578,21 +578,35 @@ def post(self): if not user and delete_user_by_id: user = {"user_id": delete_user_by_id} + # admin when has admin permission, but not consider admin when updated by himself + updated_by_admin = self.is_permission_admin() and user.get("user_id") != self.current_user.get("user_id") + self._db.update_user(user, character, delete_user_by_id=delete_user_by_id, - delete_character_by_id=delete_character_by_id) + delete_character_by_id=delete_character_by_id, updated_by_admin=updated_by_admin) + + self.write({"status": "success"}) + self.finish() class ManualHandler(jsonhandler.JsonHandler): @tornado.web.asynchronous def get(self): - self.write(self._manual.get_str_all()) + str_value = self._manual.get_str_all(is_admin=False) + self.write(str_value) self.finish() -class LoreHandler(jsonhandler.JsonHandler): +class ManualAdminHandler(jsonhandler.JsonHandler): @tornado.web.asynchronous def get(self): - self.write(self._lore.get_str_all()) + if not self.is_permission_admin(): + print("Insufficient permissions from %s" % self.request.remote_ip, file=sys.stderr) + # Forbidden + self.set_status(403) + self.send_error(403) + raise tornado.web.Finish() + str_value = self._manual.get_str_all(is_admin=True) + self.write(str_value) self.finish() @@ -754,11 +768,10 @@ def get(self): file_url = self._doc_generator_gspread.get_url() email_google_service = self._doc_generator_gspread.get_email_service() is_auth = self._doc_generator_gspread.is_auth() - can_generate = bool(doc_generator and - not self._doc_generator_gspread.has_error() and - not doc_generator.has_error() and - has_access_perm and is_auth - ) + can_generate = bool(doc_generator and has_access_perm and is_auth) + + last_updated_date = self._manual.get_last_date_updated() + last_updated_date_for_js = last_updated_date * 1000 info = { "file_url": file_url, @@ -766,7 +779,8 @@ def get(self): "user_has_writer_perm": has_user_writer_perm, "has_access_perm": has_access_perm, "email_google_service": email_google_service, - "can_generate": can_generate + "can_generate": can_generate, + "last_local_doc_update": last_updated_date_for_js } if self._doc_generator_gspread.has_error(): @@ -876,12 +890,47 @@ def post(self): status = doc_generator.generate_doc() if status: document = doc_generator.get_generated_doc() + info = {} if "manual" in document: doc_part = document.get("manual") - self._manual.update({"manual": doc_part}, save=True) + info["manual"] = doc_part + # self._manual.update({"manual": doc_part}, save=True) if "lore" in document: doc_part = document.get("lore") - self._lore.update({"lore": doc_part}, save=True) + info["lore"] = doc_part + # self._manual.update({"lore": doc_part}, save=True) + if "schema_user" in document or "schema_char" in document or "form_user" in document \ + or "form_char" in document or "admin_form_user" in document or "admin_form_char" in document: + dct_char_rule = {} + if "schema_user" in document: + doc_part = document.get("schema_user") + dct_char_rule["schema_user"] = doc_part + if "schema_char" in document: + doc_part = document.get("schema_char") + dct_char_rule["schema_char"] = doc_part + if "form_user" in document: + doc_part = document.get("form_user") + dct_char_rule["form_user"] = doc_part + if "form_char" in document: + doc_part = document.get("form_char") + dct_char_rule["form_char"] = doc_part + if "admin_form_user" in document: + doc_part = document.get("admin_form_user") + dct_char_rule["admin_form_user"] = doc_part + if "admin_form_char" in document: + doc_part = document.get("admin_form_char") + dct_char_rule["admin_form_char"] = doc_part + + info["char_rule"] = dct_char_rule + + info["point"] = document["point"] + info["skill_manual"] = document["skill_manual"] + + # Link manual and form + info = self._manual.generate_link(info) + + # Write to database + self._manual.update(info, save=True) status = {"status": "Generated with success. Database updated."} else: status = doc_generator.get_error(force_error=True) @@ -890,6 +939,29 @@ def post(self): self.finish() +class CharacterApprobationHandler(jsonhandler.JsonHandler): + @tornado.web.asynchronous + @tornado.web.authenticated + def post(self): + if not self.is_permission_admin(): + print("Insufficient permissions from %s" % self.request.remote_ip, file=sys.stderr) + # Forbidden + self.set_status(403) + self.send_error(403) + raise tornado.web.Finish() + + self.prepare_json() + + user_id = self.get_argument("user_id") + character_name = self.get_argument("character_name") + approbation_status = self.get_argument("approbation_status") + + status = self._db.set_approbation(user_id, character_name, approbation_status) + + self.write(status) + self.finish() + + class StatSeasonPass(jsonhandler.JsonHandler): @tornado.web.asynchronous def get(self): diff --git a/src/web/partials/_base.html b/src/web/partials/_base.html index 1c3ae2c4..8205f5b9 100644 --- a/src/web/partials/_base.html +++ b/src/web/partials/_base.html @@ -78,7 +78,7 @@ {% end %} {% if not disable_login and not disable_admin and current_user and current_user.get("permission") == "Admin" %} -
  • Admin
  • +
  • Admin
  • {% end %} {% if not disable_login and (not hide_menu_login or current_user) %} @@ -166,12 +166,12 @@

    {% end %} + + + - - - diff --git a/src/web/partials/admin/_base.html b/src/web/partials/admin/_base.html index da9bbb77..a50564e8 100644 --- a/src/web/partials/admin/_base.html +++ b/src/web/partials/admin/_base.html @@ -166,12 +166,12 @@

    {% end %} + + + - - - diff --git a/src/web/partials/admin/character.html b/src/web/partials/admin/character.html index dd657307..133b07fe 100644 --- a/src/web/partials/admin/character.html +++ b/src/web/partials/admin/character.html @@ -20,18 +20,20 @@
    Nb joueur : {{!ddb_user.length}} - -
    - Vue de fiche:
    -
    -
    - + Rafraîchir
    + Appuyez avant de choisir une fiche. + + + + + + + + +
    @@ -50,49 +52,477 @@
    - Point d'XP
    - Total : {{! countTotalXp() }} + Point d'XP + {{! xp_spend }} / {{! xp_receive }}
    - Xp consommé : {{! countTotalCostXp() }} {{! - showDiffTotalXp() }} + Point de Mérite + {{! merite_spend }} / {{! merite_receive }}
    -
    -
    - debug Xp naissance : +
    +
    +
    + {{! key }} : {{! value }} +
    -
    +
    -
    -
    -
    -
    -
    -
    -
    {{!prettyModelUser}}
    -
    -
    {{!prettyModelChar}}
    -
    -
    {{!prettyPlayer}}
    -
    -
    http://www.traitrelame.ca/character#/?id_player={{! player.id }}
    -
    {{! url_qr_code }}
    +
    +
    + Liste des joueurs. + +
    + Nb joueur : {{!ddb_user.length}} + + Rafraîchir
    + Appuyez avant de choisir une fiche.
    + + + + + + + + +
    - -
    - Xp : {{! countTotalCostXp() }} {{! - showDiffTotalXp() }}
    -
    - {% include "../character_sheet_print.html" %} + + +

    Information pour le joueur

    + +
    +

    Approbation

    +

    + Approbation de la fiche : + ✪ nouveau + ✓ approuvé + ✗ en attente + ✞ inactif + ✐ à corriger +
    + Date d'approbation : {{! model_char.approbation.date * 1000 | UTCToNow: true }}
    + + + + + +

    +
    + +

    État de la fiche

    +

    + Date de création : {{! model_char.date_creation * 1000 | UTCToNow: true }}
    + Date de mise à jour : {{! model_char.date_modify * 1000 | UTCToNow: true }} +

    + +
    + + La fiche est à jourest à améliorerest à corriger. + Il y a {{! -(xp_total) }} XP à enlever. + Il y a {{! xp_total }} XP à placer. + Il y a {{! -(merite_total) }} Mérite à enlever. + Il y a {{! merite_total }} Mérite en banque. + Il y a {{! -(diff_sous_ecole) }} sous-école à enlever. + Il y a {{! diff_sous_ecole }} sous-école à choisir. + Nom du personnage est manquant. + Faction manquante. + Il y a {{! count_master_tech - (xp_receive - xp_default + 1) }} technique de maitre à enlever. +
    + +
    +
    + Point d'XP + {{! xp_spend }} / {{! xp_receive }} +
    +
    + Point de Mérite + {{! merite_spend }} / {{! merite_receive }} +
    +
    +
    +
    + {{! key }} : {{! value }} +
    +
    +
    + +
    + + + + + + + + + + + + + + + + + + + +
    + +
    +
    +
    {{! prettyModelUser }}
    +
    +
    {{! prettyModelChar }}
    +
    +
    {{! prettyPlayer }}
    +
    +
    http://www.traitrelame.ca/character#/?id_player={{! player.id }}
    +
    {{! url_qr_code }}
    +
    +
    +
    diff --git a/src/web/partials/admin/editor.html b/src/web/partials/admin/editor.html index 425d9f5c..016aa364 100644 --- a/src/web/partials/admin/editor.html +++ b/src/web/partials/admin/editor.html @@ -6,42 +6,44 @@

    Gestionnaire de documentation

    Générateur de documentation à partir de Google Drive Spreadsheet

    - Mise à jour des informations. + Téléchargement des informations.
    Cet outil permet d'ouvrir un fichier sur Google Drive Spreadsheet, d'itérer dans le document pour extraire les données, valider le formatage du document et générer la base de donnée des documents.
    -
    - - {{! model_editor.modulestate.error }} - +
    + + {{! model_editor.module_state.error }} +
    -
    +
    - Le lien du document est manquant. + Le lien du document est manquant.
    - + {{! model_editor.generated_doc.status.text }} - +
    {% end %} diff --git a/src/web/partials/admin/setting.html b/src/web/partials/admin/setting.html index a6fe5bbb..40a8c4e0 100644 --- a/src/web/partials/admin/setting.html +++ b/src/web/partials/admin/setting.html @@ -10,9 +10,9 @@

    Générateur d'archive du projet

    Télécharger le fichier archive. - + {{! model_setting.downloading_archive.status.text }} - +
    diff --git a/src/web/partials/character.html b/src/web/partials/character.html index 4f8dc121..43e05ab7 100644 --- a/src/web/partials/character.html +++ b/src/web/partials/character.html @@ -30,18 +30,18 @@

    Formulaire de nouvelle fiche de personnage


    -
    -
    -
    -
    -
    -
    -
    - Xp : {{! countTotalCostXp() }} {{! - showDiffTotalXp() }} + +
    + + + +

    Information pour le joueur

    + +
    +

    Approbation

    +

    + Status de validation de la fiche par le maître du jeu : + ✪ nouveau + ✓ approuvé + ✗ en attente + ✞ inactif + ✐ à corriger +
    + Date de changement du status : {{! model_char.approbation.date * 1000 | UTCToNow: true }} +

    +
    + +

    État de la fiche

    +

    + Date de création : {{! model_char.date_creation * 1000 | UTCToNow: true }}
    + Date de mise à jour : {{! model_char.date_modify * 1000 | UTCToNow: true }} +

    + +
    + + La fiche est à jourest à améliorerest à corriger. + Il y a {{! -(xp_total) }} XP à enlever. + Il y a {{! xp_total }} XP à placer. + Il y a {{! -(merite_total) }} Mérite à enlever. + Il y a {{! merite_total }} Mérite en banque. + Il y a {{! -(diff_sous_ecole) }} sous-école à enlever. + Il y a {{! diff_sous_ecole }} sous-école à choisir. + Nom du personnage est manquant. + Faction manquante. + Il y a {{! count_master_tech - (xp_receive - xp_default + 1) }} technique de maitre à enlever.
    -
    - {% include "character_sheet_print.html" %} +
    +
    + Point d'XP + {{! xp_spend }} / {{! xp_receive }} +
    +
    + Point de Mérite + {{! merite_spend }} / {{! merite_receive }} +
    +
    +
    +
    + {{! key }} : {{! value }} +
    +
    +
    + +
    + + + + + + + + + + + + + + + + + + + +
    +
    + +
    +
    +
    {{! prettyModelUser }}
    +
    +
    {{! prettyModelChar }}
    +
    +
    {{! prettyPlayer }}
    +
    +
    http://www.traitrelame.ca/character#/?id_player={{! player.id }}
    +
    {{! url_qr_code }}
    +
    +
    {% end %} -{% end %} \ No newline at end of file +{% end %} diff --git a/src/web/partials/character_sheet_print.html b/src/web/partials/character_sheet_print.html index 5641e271..ad44f85c 100644 --- a/src/web/partials/character_sheet_print.html +++ b/src/web/partials/character_sheet_print.html @@ -1,10 +1,10 @@
    -
    - - - - -
    + + + + + +
    diff --git a/src/web/partials/lore.html b/src/web/partials/lore.html index 3a46578e..ad3bcce6 100644 --- a/src/web/partials/lore.html +++ b/src/web/partials/lore.html @@ -91,35 +91,33 @@

    -
    +
    -
    - -

    - -
    +
    +
    + +

    + +
    -

    +
    - -
    - + +
    + -
    +
    +
    -
    +
    -
    -

    - -
    +

    diff --git a/src/web/partials/manual.html b/src/web/partials/manual.html index e89074ff..80899dae 100644 --- a/src/web/partials/manual.html +++ b/src/web/partials/manual.html @@ -91,43 +91,40 @@

    -
    +
    -
    - -

    - -
    +
    +
    + +

    + +
    -

    +
    - -
    - + +
    + -
    +
    +
    -
    +
    -
    -

    - -
    +

    +
    -
    {% end %} diff --git a/src/web/partials/news.html b/src/web/partials/news.html index 5d1b851f..0b2a0808 100644 --- a/src/web/partials/news.html +++ b/src/web/partials/news.html @@ -7,7 +7,7 @@
    -

    Bienvenu.e sur le site web du grandeur nature Traître-Lame

    +

    Site web du grandeur nature Traître-Lame

    Une activité au Québec axée sur les capacités réelles des joueur.se.s plutôt que sur l'ancienneté de leur personnage.

    @@ -17,49 +17,55 @@

    Prochain événement <

    Type d'événement: GN - Tout le terrain

    -

    Date: 7 octobre 2017

    +

    Date: 21 Septembre à 20h au 22 Septembre à 20h, 2018

    Lieu: Au terrain 1801 Chemin Béthanie, Béthanie (Québec), J0H 1E1, (450) 548-2720

    -

    Prix: 30$

    - +

    Prix: 45$

    +

    Prix nouveau: 35$

    +

    Âge: 18+

    +

    - Événement Facebook + Événement Facebook

    @@ -70,27 +76,26 @@

    Qu'est-ce que Traître-Lame?

    ou d'adresse au combat.

    - Traître-Lame se veut une activité orientée pour les adultes. La consommation d'alcool y est permise de façon responsable et mature. Le jeu est principalement propulsé par un scénario profond et - complexe s'appuyant sur la structure à quatre factions de l'univers de Traître-Lame. Les joueur.se.s sont invité.e.s à la création de leur personnage à choisir l'une de quatre factions jouables - de la - Sarsonne qui détermine le style de leur jeu. -

    -

    - L'empire de Vanicant offre un style de jeu actif et combatif, dans une atmosphère politique tordue où les jeux de pouvoir et les trahisons sont nombreux. -

    -

    - Le Royaume de Canavim offre la possibilité aux joueur.se.s d'évoluer dans un cadre militaire plus organisé au coeur d'une société moderne et hiérarchisée. Il s'agit aussi d'une faction où - l'anglais - est très présent. -

    -

    - La tribu Sarsar propose aux joueur.se.s d'incarner un groupe de combattant en constante guérilla où la politique et la religion tribale sont intimement liées. -

    -

    - Finalement, les exilés du village de Vallam sont au centre des querelles économiques et politiques de l'économie réelle et parallèle et propose aux joueur.se.s un jeu moins orienté vers le - combat et - plus vers l'intrigue et l'ésotérisme. + Traître-Lame se veut une activité orientée pour les adultes. La consommation d'alcool y est permise de façon responsable et mature.

    +

    Dates 2018

    +
      +
    • 1-2 Juin: TL (Zone 1+2)
    • +
    • 6-7 Juillet: TL (Zone 1+2)
    • +
    • 24-25 Août: TL (Zone 1+2)
    • +
    • 21-22 Septembre: TL (Zone 1+2)
    • +
    +

    Tarifs 2018

    +
      +
    • Billet: 45$
    • +
    • Billet nouveau joueur: 35$
    • + +
    +

    Localisation

    +
    @@ -121,30 +126,11 @@

    Univers

    Politiques, guildes et groupe NPC.
    -

    Dates 2017

    -
      -
    • 7 octobre: TL (Zone 1+2)
    • -
    -

    Tarifs 2017

    -
      -
    • Billet: 30$
    • -
    • Accès aux dortoirs: Dortoir non accessible pour la saison 2017
    • -
    -

    Localisation

    -

    Remerciement

    Merci aux organisateur.trice.s et aux bévénoles.
    Merci aux contributeur.trice.s du logiciel web.
    Merci à www.calendriergn.ca pour leur projet de calendrier du GN au Québec.
    Visitez ici pour plus d'information sur le développement du logiciel.
    -

    Dates 2017 de constructions sur le terrain

    -
      -
    • 18-19 novembre
    • -
    • 9-10 décembre
    • -
    diff --git a/src/web/py_class/db.py b/src/web/py_class/db.py index 055afe42..7134056d 100644 --- a/src/web/py_class/db.py +++ b/src/web/py_class/db.py @@ -172,11 +172,12 @@ def user_exist(self, email=None, user_id=None, username=None): user_id and not self._db_user.get(self._query_user.user_id == user_id)) and not ( username and not self._db_user.get(self._query_user.username == username)) - def update_user(self, user_data, character_data=None, delete_user_by_id=None, delete_character_by_id=None): + def update_user(self, user_data, character_data=None, delete_user_by_id=None, delete_character_by_id=None, + cancel_update_date=False, updated_by_admin=False): if not isinstance(user_data, dict): print("Cannot update user if user is not dictionary : %s" % user_data) return - actual_date = datetime.datetime.utcnow().timestamp() + actual_date = datetime.datetime.now(datetime.timezone.utc).timestamp() # if None, it's new user user_id = user_data.get("user_id") # if None, it's new character @@ -209,7 +210,10 @@ def transform(element): elif character_data: lst_character[i] = character_data # update last modify date - character_data["date_modify"] = datetime.datetime.utcnow().timestamp() + if not cancel_update_date: + character_data["date_modify"] = actual_date + if "date_creation" not in character_data and "date_modify" in character_data: + character_data["date_creation"] = character_data["date_modify"] break i += 1 else: @@ -228,15 +232,49 @@ def transform(element): # 2. validate user exist, else create it. Ignore if delete action # TODO validate user_data field user_data["user_id"] = uuid.uuid4().hex + if character_data: + character_data["date_creation"] = actual_date + character_data["date_modify"] = actual_date user_data["character"] = [character_data] if character_data else [] user_data["date_modify"] = user_data["date_creation"] = actual_date self._db_user.insert(user_data) elif user_data or character_data or delete_character_by_id: - # 3. validate character exist for update, else create it, or delete it. - user_data["date_modify"] = actual_date + if not cancel_update_date: + if character_data and "approbation" in character_data: + # When approved and update the character, it's unapproved + approbation = character_data.get("approbation") + # Only approved or "to correct" become unapproved + # When an admin save, it become automatic approved + if updated_by_admin: + character_data["approbation"] = {"status": 1, "date": actual_date} + elif approbation.get("status") in [1, 4]: + character_data["approbation"] = {"status": 2, "date": actual_date} + + # 3. validate character exist for update, else create it, or delete it. + user_data["date_modify"] = actual_date self._db_user.update(_update_character(), self._query_user.user_id == user_id) def stat_get_total_season_pass(self): # self._db_user.search(tinydb.Query().character.all(tinydb.Query().xp_gn_1_2016 == True)) # Cannot work if change '== True' for 'is True' - return {"total_season_pass_2017": len(self._db_user.search(self._query_user.passe_saison_2017 == True))} + return {"total_season_pass_2018": len(self._db_user.search(self._query_user.passe_saison_2018 == True))} + + def get_character(self, user_id, character_name): + user = self.get_user(user_id=user_id) + if not user: + return + for char in user.get("character"): + if char.get("name") == character_name: + return char + + def set_approbation(self, user_id, character_name, approbation_status): + user = self.get_user(user_id=user_id) + actual_date = datetime.datetime.now(datetime.timezone.utc).timestamp() + approbation = {"status": approbation_status, "date": actual_date} + for char in user.get("character"): + if char.get("name") == character_name: + char["approbation"] = approbation + break + + self.update_user(user, character_data=char, cancel_update_date=True) + return {"status": "Success", "data": approbation} diff --git a/src/web/py_class/doc_generator/doc_connector_gspread.py b/src/web/py_class/doc_generator/doc_connector_gspread.py index 5a03c758..3f9f5874 100644 --- a/src/web/py_class/doc_generator/doc_connector_gspread.py +++ b/src/web/py_class/doc_generator/doc_connector_gspread.py @@ -2,6 +2,66 @@ # -*- coding: utf-8 -*- import sys import gspread +from enum import Enum + + +class DocType(Enum): + """ + Enumerator to support external documentation from Google Spread Sheet. + Contain static information about different type of documentation. + """ + # To generate documentation + DOC = 0 + # To create form on client + FORM = 1 + # To generate database model + SCHEMA = 2 + + # To manage event + # EVENT = 3 + + def get_header(self): + """ + List of header per type of document. + :return: list of string with header of sheet. + """ + if self.value == self.DOC.value: + header = [ + "Level", "Admin", "Key", "Title", "Description", "Bullet Description", "Second Bullet Description", + "Under Level Color", "Sub Key", "Model", "Point", "HidePlayer" + ] + elif self.value == self.FORM.value: + header = [ + "Level", "Admin", "Key", "Placeholder", "Type", "Options", "Value", "Name", "Category", "Add", "Style", + "Model", "ReadByPlayer", "ReadOnlyPlayer" + ] + elif self.value == self.SCHEMA.value: + header = [ + "Level", "Name", "Type", "Title", "minLength", "pattern", "required", "minItems", "maxItems", + "uniqueItems", "Description" + ] + else: + header = [] + return header + + def get_cb_parser(self, doc_connector_gspread): + """ + Search good callback to parse the sheet. + :param doc_connector_gspread: object of DocConnectorGSpread + :return: cb of method to parse the sheet. + """ + if not isinstance(doc_connector_gspread, DocConnectorGSpread): + return None + + if self.value == self.DOC.value: + cb = doc_connector_gspread._parse_sheet_type_doc + elif self.value == self.FORM.value: + cb = doc_connector_gspread._parse_sheet_type_form + elif self.value == self.SCHEMA.value: + cb = doc_connector_gspread._parse_sheet_type_schema + else: + cb = None + return cb class DocConnectorGSpread: @@ -9,7 +69,7 @@ class DocConnectorGSpread: DocConnectorGSpread manage doc generation parsing and Google spreadsheet functionality. Use DocGeneratorGSpread to get instance of DocGeneratorGSpread. - This is more secure for multi-thread execution + This is more secure for multi-thread execution. """ def __init__(self, gc, gc_doc, msg_share_invite): @@ -20,19 +80,20 @@ def __init__(self, gc, gc_doc, msg_share_invite): self._error = None self._connector_is_valid = True self._msg_share_invite = msg_share_invite - - self._info_sheet_name = ["manual", "lore"] - self._info_header = [ - "Title H1", "Title H1 HTML", "Description H1", "Bullet Description H1", "Second Bullet Description H1", - "Under Level Color H1", - "Title H2", "Title H2 HTML", "Description H2", "Bullet Description H2", "Second Bullet Description H2", - "Under Level Color H2", - "Title H3", "Title H3 HTML", "Description H3", "Bullet Description H3", "Second Bullet Description H3", - "Under Level Color H3", - "Title H4", "Title H4 HTML", "Description H4", "Bullet Description H4", "Second Bullet Description H4", - "Under Level Color H4", - "Title H5", "Title H5 HTML", "Description H5", "Bullet Description H5", "Second Bullet Description H5", - "Under Level Color H5" + self._doc_point = {} + self._doc_manual_skill = {} + + self._info_sheet = [ + {"type": DocType.DOC, "name": "manual", "permission": ["anyone"]}, + {"type": DocType.DOC, "name": "manual", "permission": ["admin"], "is_admin": True}, + {"type": DocType.DOC, "name": "lore", "permission": ["anyone"]}, + {"type": DocType.DOC, "name": "lore", "permission": ["admin"], "is_admin": True}, + {"type": DocType.SCHEMA, "name": "schema_user", "permission": ["user"]}, + {"type": DocType.SCHEMA, "name": "schema_char", "permission": ["user"]}, + {"type": DocType.FORM, "name": "form_user", "permission": ["user"], "is_admin": False}, + {"type": DocType.FORM, "name": "form_char", "permission": ["user"], "is_admin": False}, + {"type": DocType.FORM, "name": "form_user", "permission": ["admin"], "is_admin": True}, + {"type": DocType.FORM, "name": "form_char", "permission": ["admin"], "is_admin": True}, ] def has_error(self): @@ -80,6 +141,15 @@ def get_permission_document(self): lst_info.append(info) return lst_info + def get_generated_doc(self): + """ + Property of generated_doc + :return: return False if the document is not generated, else return the dict + """ + if self._generated_doc: + return self._generated_doc + return False + def check_has_permission(self): """ Return bool if success or fail. Return dict when got error. @@ -149,275 +219,654 @@ def generate_doc(self): sh = self._g_file worksheet_list = sh.worksheets() dct_doc = {} + self._doc_point = {} + self._doc_manual_skill = {} + + for sheet_info in self._info_sheet: + sheet_name = sheet_info.get("name") + sheet_type = sheet_info.get("type") + + # Validate sheet_type + if sheet_type not in list(DocType): + self._error = "Internal error, not supported definition of type %s" % sheet_type + print(self._error, file=sys.stderr) + return False - for doc_sheet_name in self._info_sheet_name: # Find working sheet for sheet in worksheet_list: - if sheet.title == doc_sheet_name: + if sheet.title == sheet_name: manual_sheet = sheet break else: lst_str_worksheet = [sheet.title for sheet in worksheet_list] - self._error = "Sheet '%s' not exist. Existing sheet: %s" % (doc_sheet_name, lst_str_worksheet) + self._error = "Sheet '%s' not exist. Existing sheet: %s" % (sheet_name, lst_str_worksheet) print(self._error, file=sys.stderr) return False # Validate the header header_row = manual_sheet.row_values(1) - if self._info_header != header_row: + if sheet_type.get_header() != header_row: self._error = "Header of sheet %s is %s, and expected is %s" % ( - doc_sheet_name, header_row, self._info_header) + sheet_name, header_row, sheet_type.get_header()) print(self._error, file=sys.stderr) return False # Fetch all line all_values = manual_sheet.get_all_values() - info = self._parse_doc(doc_sheet_name, all_values) + + # Parse sheet + cb = sheet_type.get_cb_parser(self) + if cb: + info = cb(sheet_info, sheet_name, all_values) + else: + self._error = "Internal error, cannot find method to parse the sheet with type %s." % sheet_type + print(self._error, file=sys.stderr) + return False + if info is None: + # Error in parsing return False - dct_doc[doc_sheet_name] = info + # Compilation of unique result + is_form_admin = sheet_info.get("is_admin", False) + adapted_sheet_name = sheet_name if not is_form_admin else "admin_" + sheet_name + dct_doc[adapted_sheet_name] = info + + # Add extra compilation about point page + dct_doc["point"] = self._doc_point + dct_doc["skill_manual"] = self._doc_manual_skill self._generated_doc = dct_doc return True - def get_generated_doc(self): - """ - Property of generated_doc - :return: return False if the document is not generated, else return the dict - """ - if self._generated_doc: - return self._generated_doc - return False - - def _parse_doc(self, doc_sheet_name, all_values): + def _parse_sheet_type_schema(self, sheet_info, doc_sheet_name, all_values): """ Read each line of the doc from the spreadsheet and generate the structure. + :param sheet_info: Sheet information :param doc_sheet_name: Sheet name :param all_values: List of all row from the spreadsheet - :return: List of section to the doc or None when got error + :return: Dict of schema or None when got error """ - lst_doc_section = [] + dct_value = None line_number = 1 - first_section = None - second_section = None - third_section = None - status = False - - lst_value = all_values[1:] - if not lst_value: - # List is empty - status = True + lst_line = all_values[1:] + last_iter_level = 0 + line_value = {} - for row in lst_value: + # This is use to keep reference on last object dependant on level + lst_level_object = [] + for lst_item in lst_line: line_number += 1 - is_first_section = any(row[0:5]) - is_second_section = any(row[6:11]) - is_third_section = any(row[12:17]) - is_fourth_section = any(row[18:23]) - is_fifth_section = any(row[24:29]) - - # Check error - sum_section = sum( - (is_first_section, is_second_section, is_third_section, is_fourth_section, is_fifth_section)) - if sum_section == 0: - # Ignore empty line + level = lst_item[0] + name = lst_item[1] + s_type = lst_item[2] + title = lst_item[3] + min_length = lst_item[4] + pattern = lst_item[5] + required = lst_item[6] + min_items = lst_item[7] + max_items = lst_item[8] + unique_items = lst_item[9] + description = lst_item[10] + + if not level: continue - if sum_section > 1: - self._error = "L.%s S.%s: Cannot contain more than 1 section at time. " \ - "H1: %s, H2: %s, H3: %s, H4: %s, H5: %s." % ( - line_number, doc_sheet_name, is_first_section, is_second_section, is_third_section, - is_fourth_section, - is_fifth_section) - print(self._error, file=sys.stderr) - return - - if is_first_section: - status = self._extract_section(0, row, line_number, doc_sheet_name, lst_doc_section) + # Validation section - elif is_second_section: - second_section = None - third_section = None - first_section = lst_doc_section[-1] + # Check parameter for line + # Level is obligated + if not level.isdigit(): + msg = "The case Level need to be an integer, receive %s" % level + self._error = "L.%s S.%s: %s" % (line_number, doc_sheet_name, msg) + print(self._error, file=sys.stderr) + return None + level = int(level) + + # The next parameter is optional + # Check if integer + if min_length: + if not min_length.isdigit(): + msg = "The case MinLength need to be an integer, receive %s" % level + self._error = "L.%s S.%s: %s" % (line_number, doc_sheet_name, msg) + print(self._error, file=sys.stderr) + return None + min_length = int(min_length) - # Get section from last section - if "section" in first_section: - lst_section = first_section.get("section") - else: - lst_section = [] - first_section["section"] = lst_section + if min_items: + if not min_items.isdigit(): + msg = "The case MinItems need to be an integer, receive %s" % level + self._error = "L.%s S.%s: %s" % (line_number, doc_sheet_name, msg) + print(self._error, file=sys.stderr) + return None + min_items = int(min_items) - status = self._extract_section(1, row, line_number, doc_sheet_name, lst_section) + if max_items: + if not max_items.isdigit(): + msg = "The case MaxItems need to be an integer, receive %s" % level + self._error = "L.%s S.%s: %s" % (line_number, doc_sheet_name, msg) + print(self._error, file=sys.stderr) + return None + max_items = int(max_items) + + if unique_items == "TRUE" or unique_items == "VRAI": + unique_items = True + elif unique_items == "FALSE" or unique_items == "FAUX": + unique_items = False + + # First iteration + if dct_value is None: + if level != 1: + msg = "First element is not a Level 1." + self._error = "L.%s S.%s: %s" % (line_number, doc_sheet_name, msg) + print(self._error, file=sys.stderr) + return None + dct_value = line_value + lst_level_object.append(line_value) + else: + # All other level - elif is_third_section: - third_section = None - if not first_section: - self._error = "L.%s S.%s: Missing section H1 to insert section H3." % (line_number, doc_sheet_name) + # Validation + if level == 1: + msg = "Cannot support multiple Level 1 in same sheet." + self._error = "L.%s S.%s: %s" % (line_number, doc_sheet_name, msg) print(self._error, file=sys.stderr) - return + return None - lst_section = first_section.get("section") - if not lst_section: - self._error = "L.%s S.%s: Missing section H2 to insert section H3." % (line_number, doc_sheet_name) + if required: + msg = "Cannot support required when not Level 1." + self._error = "L.%s S.%s: %s" % (line_number, doc_sheet_name, msg) print(self._error, file=sys.stderr) - return + return None + + # First element in iteration + if not last_iter_level: + last_iter_level = 1 + + # Validate iteration level progression + nb_level = len(lst_level_object) + diff_level = level - last_iter_level + if diff_level > 1: + msg = "Cannot increase more than 1 level, but can downgrade more than 1." + self._error = "L.%s S.%s: %s" % (line_number, doc_sheet_name, msg) + print(self._error, file=sys.stderr) + return None + elif diff_level < 0: + # Validate last level is completed before pop it + last_object = lst_level_object[-1] + last_object_type = last_object.get("type") + if last_object_type == "object": + if "properties" not in last_object: + msg = "Need to be a child property of last line %i, object type. " \ + "Need to be a level %i." % (line_number - 1, nb_level) + self._error = "L.%s S.%s: %s" % (line_number, doc_sheet_name, msg) + print(self._error, file=sys.stderr) + return None + elif last_object_type == "array": + if "items" not in last_object: + msg = "Need to be a child item of last line %i, array type. " \ + "Need to be a level %i." % (line_number - 1, nb_level) + self._error = "L.%s S.%s: %s" % (line_number, doc_sheet_name, msg) + print(self._error, file=sys.stderr) + return None + + # Need to pop the list for each level, because we downgrade the list + rm_diff_level = nb_level + 1 - level + for i in range(rm_diff_level): + lst_level_object.pop() + + # Use properties of last element if object, else use item if array + last_object = lst_level_object[-1] + last_object_type = last_object.get("type") + line_value = {} + if last_object_type == "object": + if "properties" not in last_object: + last_object["properties"] = {name: line_value} + else: + last_object["properties"][name] = line_value + elif last_object_type == "array": + if "items" not in last_object: + last_object["items"] = line_value + else: + msg = "Array cannot contain many sub-item." + self._error = "L.%s S.%s: %s" % (line_number, doc_sheet_name, msg) + print(self._error, file=sys.stderr) + return None + + # Push in stack when add new object or array + if s_type in ["object", "array"]: + lst_level_object.append(line_value) + + last_iter_level = level + + # Fill data in line_value + if s_type: + line_value["type"] = s_type + if pattern: + line_value["pattern"] = pattern + if title: + line_value["title"] = title + if required: + line_value["required"] = required.split() + if type(min_length) is int: + line_value["minLength"] = min_length + if type(min_items) is int: + line_value["minItems"] = min_items + if type(max_items) is int: + line_value["maxItems"] = max_items + if type(unique_items) is bool: + line_value["uniqueItems"] = unique_items + if description: + line_value["description"] = description + + return dct_value + + def _parse_sheet_type_form(self, sheet_info, doc_sheet_name, all_values): + """ + Read each line of the doc from the spreadsheet and generate the structure. + :param sheet_info: Sheet information + :param doc_sheet_name: Sheet name + :param all_values: List of all row from the spreadsheet + :return: List of section to the doc or None when got error + """ + lst_value = [] + line_number = 1 + lst_line = all_values[1:] + line_value = {} - second_section = lst_section[-1] + is_form_admin = sheet_info.get("is_admin", False) - # Get section from last section - if "section" in second_section: - lst_section = second_section.get("section") - else: - lst_section = [] - second_section["section"] = lst_section + # This is use to keep reference on last object dependant on level + lst_level_object = [lst_value] + for lst_item in lst_line: + line_number += 1 - status = self._extract_section(2, row, line_number, doc_sheet_name, lst_section) + level = lst_item[0] + is_admin = lst_item[1] + s_key = lst_item[2] + placeholder = lst_item[3] + s_type = lst_item[4] + options = lst_item[5] + value = lst_item[6] + name = lst_item[7] + category = lst_item[8] + add = lst_item[9] + style = lst_item[10] + model = lst_item[11] + read_by_player = lst_item[12] + read_only_player = lst_item[13] + + if not level: + continue - elif is_fourth_section: - if not first_section: - self._error = "L.%s S.%s: Missing section H1 to insert section H4." % (line_number, doc_sheet_name) - print(self._error, file=sys.stderr) - return + if is_admin == "TRUE" or is_admin == "VRAI": + is_admin = True + else: + is_admin = False - if not second_section: - self._error = "L.%s S.%s: Missing section H2 to insert section H4." % (line_number, doc_sheet_name) - print(self._error, file=sys.stderr) - return + # Ignore admin field when sheet is not admin + if not is_form_admin and is_admin: + continue + + # Validation section + # Level is obligated + if not level.isdigit(): + msg = "The case Level need to be an integer, receive %s" % level + self._error = "L.%s S.%s: %s" % (line_number, doc_sheet_name, msg) + print(self._error, file=sys.stderr) + return None + level = int(level) - # Create third_section - lst_section = second_section.get("section") - if not lst_section: - self._error = "L.%s S.%s: Missing section H3 to insert section H4." % (line_number, doc_sheet_name) + # Insert level 1 element + if level == 1: + line_value = {} + lst_value.append(line_value) + else: + # level 2 and more + if not lst_value: + msg = "First element is not a Level 1." + self._error = "L.%s S.%s: %s" % (line_number, doc_sheet_name, msg) print(self._error, file=sys.stderr) - return + return None + + # Validate level with stack + diff_level = level - len(lst_level_object) + if diff_level == 1: + # All good, fill children + pass + elif diff_level > 1: + msg = "Problem with level, maybe you jump a number?" + self._error = "L.%s S.%s: %s" % (line_number, doc_sheet_name, msg) + print(self._error, file=sys.stderr) + return None + else: + # Downgrade the stack + pos_diff_level = abs(diff_level) + 1 + for i in range(pos_diff_level): + lst_level_object.pop() + + # Get last element from last element in stack + last_element = lst_level_object[-1][-1] + last_element_type = last_element.get("type") + # Section for select + if last_element_type in ["select", "strapselect"]: + title_map = last_element.get("titleMap") + line_value = {} + if title_map: + title_map.append(line_value) + else: + title_map = [line_value] + last_element["titleMap"] = title_map + + if value: + line_value["value"] = value + if name: + name = name.strip() + if name[0] == '"': + # Suppose start and end with " + name = name[1:-1] + line_value["name"] = name + if category: + line_value["category"] = category + + elif last_element_type == "array": + items = last_element.get("items") + line_value = {} + if items: + items.append(line_value) + else: + items = [line_value] + last_element["items"] = items + # Add items in stack + lst_level_object.append(items) + + if s_key: + line_value["key"] = s_key + if s_type: + line_value["type"] = s_type + if placeholder: + # Exception for type submit + if s_type == "submit": + line_value["title"] = placeholder + else: + line_value["placeholder"] = placeholder + if add: + line_value["add"] = add + if options: + lst_option = options.split(",") + dct_option = {} + str_option = "" + for option in lst_option: + count_separator = option.count(":") + if count_separator > 1: + msg = "Need 1 ':' in options to separate key and value." + self._error = "L.%s S.%s: %s" % (line_number, doc_sheet_name, msg) + print(self._error, file=sys.stderr) + return None + elif count_separator == 1: + k, v = option.split(":") + v = v.strip() + if v[0] == '"': + # Suppose start and end with " + v = v[1:-1] + elif v[0] == "[": + # TODO need to create list for many element with split(",") + lst_v = [] + under_v = v[1:-1] + if under_v[0] == '"': + under_v = under_v[1:-1] + lst_v.append(under_v) + v = lst_v + elif v.isdigit(): + v = int(v) + dct_option[k] = v + else: + str_option = option + + if dct_option: + line_value["options"] = dct_option + else: + line_value["options"] = str_option + + if style: + lst_style = style.split(",") + dct_style = {} + str_style = "" + for obj_style in lst_style: + count_separator = obj_style.count(":") + if count_separator > 1: + msg = "Need 1 ':' in style to separate key and value." + self._error = "L.%s S.%s: %s" % (line_number, doc_sheet_name, msg) + print(self._error, file=sys.stderr) + return None + elif count_separator == 1: + k, v = obj_style.split(":") + v = v.strip() + if v[0] == '"': + # Suppose start and end with " + v = v[1:-1] + elif v[0] == "[": + # TODO need to create list for many element with split(",") + lst_v = [] + under_v = v[1:-1] + if under_v[0] == '"': + under_v = under_v[1:-1] + lst_v.append(under_v) + v = lst_v + elif v.isdigit(): + v = int(v) + + dct_style[k] = v + else: + str_style = style + + if dct_style: + line_value["style"] = dct_style + else: + line_value["style"] = str_style - third_section = lst_section[-1] + return lst_value - # Get section from last section - if "section" in third_section: - lst_section = third_section.get("section") - else: - lst_section = [] - third_section["section"] = lst_section + def _parse_sheet_type_doc(self, sheet_info, doc_sheet_name, all_values): + """ + Read each line of the doc from the spreadsheet and generate the structure. + :param sheet_info: Sheet information + :param doc_sheet_name: Sheet name + :param all_values: List of all row from the spreadsheet + :return: List of section to the doc or None when got error + """ + lst_doc_section = [] + lst_level_object = [] + line_number = 1 + lst_value = all_values[1:] - status = self._extract_section(3, row, line_number, doc_sheet_name, lst_section) + for row in lst_value: + line_number += 1 - elif is_fifth_section: - if not first_section: - self._error = "L.%s S.%s: Missing section H1 to insert section H5." % (line_number, doc_sheet_name) - print(self._error, file=sys.stderr) - return + level = row[0] + # Ignore if level is empty + if not level: + continue - if not second_section: - self._error = "L.%s S.%s: Missing section H2 to insert section H5." % (line_number, doc_sheet_name) + elif level.isdigit(): + # Validate level value + level = int(level) + if not (0 < level <= 5): + msg = "The field level need to be an integer 1 to 5. Got %s" % level + self._error = "L.%s S.%s: %s" % (line_number, doc_sheet_name, msg) print(self._error, file=sys.stderr) return + else: + msg = "The field level need to be an integer 1 to 5. Type String and got %s" % level + self._error = "L.%s S.%s: %s" % (line_number, doc_sheet_name, msg) + print(self._error, file=sys.stderr) + return - if not third_section: - self._error = "L.%s S.%s: Missing section H3 to insert section H5." % (line_number, doc_sheet_name) - print(self._error, file=sys.stderr) - return + check_contain_value = any(row[1:]) + if not check_contain_value: + # Ignore empty line + continue - # Create third_section - lst_section = third_section.get("section") - if not lst_section: - self._error = "L.%s S.%s: Missing section H4 to insert section H5." % (line_number, doc_sheet_name) - print(self._error, file=sys.stderr) - return + admin = row[1] - fourth_section = lst_section[-1] + is_form_admin = sheet_info.get("is_admin", False) + if admin == "TRUE" or admin == "VRAI": + is_admin = True + else: + is_admin = False - # Get section from last section - if "section" in fourth_section: - lst_section = fourth_section.get("section") + # Ignore admin field when sheet is not admin + if not is_form_admin and is_admin: + continue + + try: + # Insert level 1 element + if level == 1: + status = self._extract_section(row, line_number, doc_sheet_name, lst_doc_section, sheet_info) + # line_value = lst_doc_section + # lst_level_object = lst_doc_section + if status: + lst_level_object = [lst_doc_section[-1]] + elif status is None: + continue else: - lst_section = [] - fourth_section["section"] = lst_section + # level 2 and more + if not lst_level_object: + msg = "First element is not a Level 1." + self._error = "L.%s S.%s: %s" % (line_number, doc_sheet_name, msg) + print(self._error, file=sys.stderr) + return None + + # Validate level with stack + diff_level = level - len(lst_level_object) + if diff_level == 1: + # All good, fill children + pass + elif diff_level > 1: + msg = "Problem with level, maybe you jump a number?" + self._error = "L.%s S.%s: %s" % (line_number, doc_sheet_name, msg) + print(self._error, file=sys.stderr) + return None + else: + # Downgrade the stack + pos_diff_level = abs(diff_level) + 1 + for i in range(pos_diff_level): + lst_level_object.pop() + + last_section = lst_level_object[-1] + + if "section" in last_section: + lst_section = last_section.get("section") + else: + lst_section = [] + last_section["section"] = lst_section + + status = self._extract_section(row, line_number, doc_sheet_name, lst_section, sheet_info) + + if status: + lst_level_object.append(lst_section[-1]) + elif status is None: + continue + + if not status: + return - status = self._extract_section(4, row, line_number, doc_sheet_name, lst_section) + except Exception as e: + msg = "Unknown case of error: %s" % e + self._error = "L.%s S.%s: %s" % (line_number, doc_sheet_name, msg) + print(self._error, file=sys.stderr) + return - if not status: - return + if not status: + return return lst_doc_section - def _extract_section(self, level, row, line_number, doc_sheet_name, lst_section): + def _extract_section(self, row, line_number, doc_sheet_name, lst_section, sheet_info): """ Fill the recent section when read the spreadsheet row. - :param level: The level of section, 0 to 4. :param row: The spreadsheet row. :param line_number: The row's index of spreadsheet. :param lst_section: list of parent section, to append new section. :param doc_sheet_name: Sheet name + :param sheet_info: Sheet information :return: True if success, else False """ - if not (0 <= level <= 4): - self._error = "L.%s S.%s: Internal error, support only level 1 to 5 and got: %s" % ( - line_number, doc_sheet_name, level + 1) - print(self._error, file=sys.stderr) - return False - nb_column = 6 - i_column = level * nb_column + level = row[0] + admin = row[1] + key = row[2] + title = row[3] + description = row[4] + bullet_description = row[5] + second_bullet_description = row[6] + under_level_color = row[7] + sub_key = row[8] + model = row[9] + point = row[10] + hide_player = row[11] + + is_form_admin = sheet_info.get("is_admin", False) + if admin == "TRUE" or admin == "VRAI": + is_admin = True + else: + is_admin = False - title = row[i_column] - title_html = row[i_column + 1] - description = row[i_column + 2] - bullet_description = row[i_column + 3] - second_bullet_description = row[i_column + 4] - under_level_color = row[i_column + 5] + # Ignore admin field when sheet is not admin + if not is_form_admin and is_admin: + return # Check error - if title_html and not title: - self._error = "L.%s S.%s: Need title when fill title html for H%s." % ( - line_number, doc_sheet_name, i_column) + if title and not key: + msg = "Need key when fill title for H%s." % level + self._error = "L.%s S.%s: %s" % (line_number, doc_sheet_name, msg) print(self._error, file=sys.stderr) return False if description and bullet_description: - self._error = "L.%s S.%s: Cannot have a description and a bullet description " \ - "on same line for H%s." % (line_number, doc_sheet_name, i_column) + msg = "Cannot have a description and a bullet description on same line for H%s." % level + self._error = "L.%s S.%s: %s" % (line_number, doc_sheet_name, msg) print(self._error, file=sys.stderr) return False if description and second_bullet_description: - self._error = "L.%s S.%s: Cannot have a description and a second bullet description " \ - "on same line for H%s." % (line_number, doc_sheet_name, i_column) + msg = "Cannot have a description and a second bullet description on same line for H%s." % level + self._error = "L.%s S.%s: %s" % (line_number, doc_sheet_name, msg) print(self._error, file=sys.stderr) return False if bullet_description and second_bullet_description: - self._error = "L.%s S.%s: Cannot have a bullet description and a second bullet description " \ - "on same line for H%s." % (line_number, doc_sheet_name, i_column) + msg = "Cannot have a bullet description and a second bullet description on same line for H%s." % level + self._error = "L.%s S.%s: %s" % (line_number, doc_sheet_name, msg) print(self._error, file=sys.stderr) return False # Begin to fill this section # If contain title, it's a new section. Else, take the last on the list. - if title: + if key: # New section - section = {"title": title} + section = {"title": key} lst_section.append(section) else: section = lst_section[-1] # Cannot continue if contain child section, because the data will be append to parent # this will cause a view error if "section" in section: - self._error = "L.%s S.%s: Cannot add information on this section when contain sub header " \ - "on same line for H%s." % (line_number, doc_sheet_name, i_column) + msg = "Cannot add information on this section when contain sub header on same line for " \ + "H%s." % level + self._error = "L.%s S.%s: %s" % (line_number, doc_sheet_name, msg) print(self._error, file=sys.stderr) return False # Special title, contain html to improve view - if title_html: + if title: if "title_html" in section: - self._error = "L.%s S.%s: Cannot manage many title_html for H%s." % ( - line_number, doc_sheet_name, i_column) + msg = "Cannot manage many title_html for H%s." % level + self._error = "L.%s S.%s: %s" % (line_number, doc_sheet_name, msg) print(self._error, file=sys.stderr) return False - section["title_html"] = title_html + section["title_html"] = title # Description can be append for the same section if description: @@ -451,8 +900,8 @@ def _extract_section(self, level, row, line_number, doc_sheet_name, lst_section) if second_bullet_description: if "description" not in section: - self._error = "L.%s S.%s: Cannot create second-bullet description missing description " \ - "for H%s." % (line_number, doc_sheet_name, i_column) + msg = "Cannot create second-bullet description missing description for H%s." % level + self._error = "L.%s S.%s: %s" % (line_number, doc_sheet_name, msg) print(self._error, file=sys.stderr) return False lst_description = section.get("description") @@ -461,8 +910,9 @@ def _extract_section(self, level, row, line_number, doc_sheet_name, lst_section) if lst_description: lst_bullet_description = lst_description[-1] else: - self._error = "L.%s S.%s: Cannot create second-bullet description when not precede to bullet " \ - "description for H%s." % (line_number, doc_sheet_name, i_column) + msg = "Cannot create second-bullet description when not precede to bullet description for " \ + "H%s." % level + self._error = "L.%s S.%s: %s" % (line_number, doc_sheet_name, msg) print(self._error, file=sys.stderr) return False @@ -483,10 +933,95 @@ def _extract_section(self, level, row, line_number, doc_sheet_name, lst_section) if under_level_color: # Add color for header if "under_level_color" in section: - self._error = "L.%s S.%s: Already contain value of 'Under Level Color'for H%s." % ( - line_number, doc_sheet_name, i_column) + msg = "Already contain value of 'Under Level Color'for H%s." % level + self._error = "L.%s S.%s: %s" % (line_number, doc_sheet_name, msg) print(self._error, file=sys.stderr) return False section["under_level_color"] = under_level_color + # HACK with model + updated_sub_key = sub_key + if "habilites" in model: + updated_sub_key = "habilites_" + sub_key + elif "technique_maitre" in model: + updated_sub_key = "technique_maitre_" + sub_key + elif "merite" in model: + updated_sub_key = "merite_" + sub_key + elif "esclave" in model: + updated_sub_key = "esclave_" + sub_key + elif "marche" in model: + updated_sub_key = "marche_" + sub_key + + if sub_key: + section["sub_key"] = sub_key + + # Add manual skill + if not is_form_admin: + if bullet_description: + self._doc_manual_skill[updated_sub_key] = bullet_description + elif second_bullet_description: + self._doc_manual_skill[updated_sub_key] = second_bullet_description + + if model: + section["model"] = model + if not is_form_admin and point and sub_key: + dct_point = self._transform_point(line_number, doc_sheet_name, point) + if dct_point is None: + return False + section["point"] = dct_point + # if not sub_key: + # msg = "sub_key is empty." + # self._error = "L.%s S.%s: %s" % (line_number, doc_sheet_name, msg) + # print(self._error, file=sys.stderr) + # return False + + if updated_sub_key in self._doc_point: + # HACK ignore "Contrebande" duplication + # TODO send a warning about duplication and not a failure + if "Contrebande" not in updated_sub_key: + msg = "Duplicated sub_key : %s" % updated_sub_key + self._error = "L.%s S.%s: %s" % (line_number, doc_sheet_name, msg) + print(self._error, file=sys.stderr) + return False + + self._doc_point[updated_sub_key] = dct_point + if hide_player: + section["hide_player"] = hide_player + if admin: + section["admin"] = admin + return True + + def _transform_point(self, line_number, doc_sheet_name, str_point): + dct_point = {} + lst_point = str_point.split(";") + + for str_single_point in lst_point: + if not str_single_point: + continue + + if str_single_point.count(":") != 1: + msg = "Column 'Point' is wrong. Missing character ':' to separate key with value. " \ + "Point : %s" % str_point + self._error = "L.%s S.%s: %s" % (line_number, doc_sheet_name, msg) + print(self._error, file=sys.stderr) + return + + key, value = str_single_point.split(":") + if key in dct_point: + msg = "Duplication key %s. Point : %s" % (key, str_point) + self._error = "L.%s S.%s: %s" % (line_number, doc_sheet_name, msg) + print(self._error, file=sys.stderr) + return + + try: + int_value = int(value) + except ValueError: + msg = "Value is not a digital : %s" % value + self._error = "L.%s S.%s: %s" % (line_number, doc_sheet_name, msg) + print(self._error, file=sys.stderr) + return + + dct_point[key] = int_value + + return dct_point diff --git a/src/web/py_class/lore.py b/src/web/py_class/lore.py deleted file mode 100644 index e6ccc899..00000000 --- a/src/web/py_class/lore.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -import json - - -class Lore(object): - """Contain knowledge who not necessary for the play.""" - - def __init__(self, parser): - self._str_lore = "" - self._lore_path = parser.db_lore_path - with open(self._lore_path, encoding='utf-8') as lore_file: - self._str_lore = json.load(lore_file) - - def update(self, dct_lore, save=False): - # Transform the object in json string - self._str_lore = json.dumps(dct_lore) - - # Save on file - if save: - with open(self._lore_path, mode="w", encoding='utf-8') as lore_file: - json.dump(dct_lore, lore_file, indent=2) - - def get_str_all(self): - return self._str_lore diff --git a/src/web/py_class/manual.py b/src/web/py_class/manual.py index fbe1d322..4f81ac50 100644 --- a/src/web/py_class/manual.py +++ b/src/web/py_class/manual.py @@ -2,25 +2,67 @@ # -*- coding: utf-8 -*- import json +import os class Manual(object): """Contain all gaming rule.""" def __init__(self, parser): - self._str_manual = "" + # self._str_manual = "" + self._manual = {} self._manual_path = parser.db_manual_path - with open(self._manual_path, encoding='utf-8') as manual_file: - self._str_manual = json.load(manual_file) + if os.path.isfile(self._manual_path): + with open(self._manual_path, encoding='utf-8') as manual_file: + self._manual = json.load(manual_file) + else: + self._manual = {"manual": [], "lore": [], "char_rule": {}, "point": {}, "skill_manual": {}} def update(self, dct_manual, save=False): # Transform the object in json string - self._str_manual = json.dumps(dct_manual) + self._manual.update(dct_manual) + # self._str_manual = json.dumps(self._manual) # Save on file if save: with open(self._manual_path, mode="w", encoding='utf-8') as manual_file: - json.dump(dct_manual, manual_file, indent=2) + json.dump(self._manual, manual_file, indent=2) - def get_str_all(self): - return self._str_manual + def get_all(self, is_admin=False): + if not self._manual: + return {} + + tmp_rule = { + "char_rule": {}, + "manual": self._manual["manual"], + "lore": self._manual["lore"], + "point": self._manual["point"], + "skill_manual": self._manual["skill_manual"] + } + if is_admin: + tmp_rule["char_rule"]["schema_user"] = self._manual["char_rule"]["schema_user"] + tmp_rule["char_rule"]["schema_char"] = self._manual["char_rule"]["schema_char"] + tmp_rule["char_rule"]["form_user"] = self._manual["char_rule"]["admin_form_user"] + tmp_rule["char_rule"]["form_char"] = self._manual["char_rule"]["admin_form_char"] + else: + tmp_rule["char_rule"]["schema_user"] = self._manual["char_rule"]["schema_user"] + tmp_rule["char_rule"]["schema_char"] = self._manual["char_rule"]["schema_char"] + tmp_rule["char_rule"]["form_user"] = self._manual["char_rule"]["form_user"] + tmp_rule["char_rule"]["form_char"] = self._manual["char_rule"]["form_char"] + + return tmp_rule + + def get_str_all(self, is_admin=False): + obj = self.get_all(is_admin=is_admin) + return json.dumps(obj) + + def get_last_date_updated(self): + if os.path.isfile(self._manual_path): + f = os.path.getmtime(self._manual_path) + else: + f = 0 + return f + + @staticmethod + def generate_link(manual): + return manual diff --git a/src/web/resources/css/_base_dark.css b/src/web/resources/css/_base_dark.css index b0f8df82..ba75981b 100644 --- a/src/web/resources/css/_base_dark.css +++ b/src/web/resources/css/_base_dark.css @@ -73,4 +73,8 @@ input { .omb_signUpForm > .help_block { color: red; +} + +.close.pull-right { + color: white; } \ No newline at end of file diff --git a/src/web/resources/js/tl_module/character_ctrl/character_ctrl.js b/src/web/resources/js/tl_module/character_ctrl/character_ctrl.js index 085399fd..f916830f 100644 --- a/src/web/resources/js/tl_module/character_ctrl/character_ctrl.js +++ b/src/web/resources/js/tl_module/character_ctrl/character_ctrl.js @@ -13,6 +13,7 @@ characterApp.controller("character_ctrl", ["$scope", "$q", "$http", "$window", / // todo move this variable in json $scope.xp_default = 6; + $scope.enable_debug = false; $scope.sheet_view = {}; $scope.sheet_view.mode = "form_write"; @@ -28,7 +29,29 @@ characterApp.controller("character_ctrl", ["$scope", "$q", "$http", "$window", / $scope.new_player = false; $scope.new_character = false; $scope.no_character = true; + $scope.character_point = {}; + $scope.character_reduce_point = {}; + $scope.character_skill = []; + $scope.character_merite = []; + $scope.character_esclave = []; + $scope.character_marche = []; + $scope.xp_receive = 0; + $scope.xp_spend = 0; + $scope.xp_total = 0; + + $scope.capacity_sous_ecole = 0; + $scope.count_sous_ecole = 0; + $scope.diff_sous_ecole = 0; + + $scope.merite_receive = 0; + $scope.merite_spend = 0; + $scope.merite_total = 0; + + $scope.count_master_tech = 0; + $scope.validated_count_master_tech = false; + + $scope.model_database = {}; $scope.model_user = {}; $scope.schema_user = {}; $scope.form_user = []; @@ -44,8 +67,133 @@ characterApp.controller("character_ctrl", ["$scope", "$q", "$http", "$window", / $scope.cs_setting = "filled"; $scope.cs_checks = []; + $scope.status_send = { + enabled: false, + is_error: false, + text: "" + }; + + $scope.approbation_status = { + enabled: false, + is_error: false, + text: "" + }; + + $scope.refresh_page = function () { + location.reload(); + }; + // fill user and character schema and form - TL_Schema($scope); + $scope.update_character = function (e) { + var char_rule_url = $scope.is_admin ? "/cmd/manual_admin" : "/cmd/manual"; + $http({ + method: "get", + url: char_rule_url, + headers: {"Content-Type": "application/json; charset=UTF-8"}, + timeout: 5000 + }).then(function (response/*, status, headers, config*/) { + console.info(response); + var data = response.data.char_rule; + $scope.schema_user = data.schema_user; + $scope.schema_char = data.schema_char; + $scope.form_user = data.form_user; + $scope.form_char = data.form_char; + $scope.model_database = response.data; + $scope.update_point(); + }, function errorCallback(response) { + console.error(response); + }); + + }; + $scope.update_character(); + + $scope.is_approbation_new = function (user) { + return user && (isUndefined(user.character[0].approbation) || user.character[0].approbation.status == 0); + }; + + $scope.is_approbation_approved = function (user) { + return user && isDefined(user.character[0].approbation) && user.character[0].approbation.status == 1; + }; + + $scope.is_approbation_unapproved = function (user) { + return user && isDefined(user.character[0].approbation) && user.character[0].approbation.status == 2; + }; + + $scope.is_approbation_inactive = function (user) { + return user && isDefined(user.character[0].approbation) && user.character[0].approbation.status == 3; + }; + + $scope.is_approbation_to_correct = function (user) { + return user && isDefined(user.character[0].approbation) && user.character[0].approbation.status == 4; + }; + + $scope.get_timestamp_approbation_date = function (user) { + if (user) { + return user.character[0].approbation.date; + } + return -1; + }; + + $scope.get_text_select_character = function (user) { + var txt_append = ""; + if ($scope.is_approbation_new(user)) { + txt_append = '✪'; + } else if ($scope.is_approbation_approved(user)) { + txt_append = '✓'; + } else if ($scope.is_approbation_unapproved(user)) { + txt_append = '✗'; + } else if ($scope.is_approbation_inactive(user)) { + txt_append = '✞'; + } else if ($scope.is_approbation_to_correct(user)) { + txt_append = '✐'; + } else { + txt_append = '?'; + } + return user.name + " " + txt_append; + }; + + $scope.send_approbation = function (status) { + var data = {}; + data.user_id = $scope.model_user.user_id; + data.character_name = $scope.model_char.name; + data.approbation_status = status; + + $http({ + method: "post", + url: "/cmd/character_approbation", + headers: {"Content-Type": "application/json; charset=UTF-8"}, + data: data, + timeout: 5000 + }).then(function (response/*, status, headers, config*/) { + var data = response.data; + if (isDefined(response.error)) { + $scope.approbation_status.enabled = true; + $scope.approbation_status.is_error = true; + $scope.approbation_status.text = data.error; + } else { + $scope.approbation_status.enabled = true; + $scope.approbation_status.is_error = false; + $scope.approbation_status.text = "Succès."; + + var data_approbation = {"date": data.data.date, "status": data.data.status}; + $scope.character.approbation = data_approbation; + } + + }, function errorCallback(response) { + console.error(response); + + $scope.approbation_status.enabled = true; + $scope.approbation_status.is_error = true; + + if (response.status == -1) { + // Timeout + $scope.approbation_status.text = "Timeout request."; + } else { + // Error from server + $scope.approbation_status.text = "Error from server : " + response.status; + } + }); + }; $scope.onSubmit = function (form) { // First we broadcast an event so all fields validate themselves @@ -56,19 +204,46 @@ characterApp.controller("character_ctrl", ["$scope", "$q", "$http", "$window", / var data = {}; data.player = $scope.model_user; data.character = $scope.model_char; + $http({ method: "post", url: "/cmd/character_view", headers: {"Content-Type": "application/json; charset=UTF-8"}, data: data, timeout: 5000 + }).then(function (response/*, status, headers, config*/) { + $scope.status_send.enabled = true; + + if (isDefined(response.data.error)) { + $scope.status_send.text = response.data.error; + $scope.status_send.is_error = true; + } else { + $scope.status_send.is_error = false; + $scope.status_send.text = "Succès."; + // TODO not suppose to need to reload the page, block by socket update + $window.location.reload(); + } + + }, function errorCallback(response) { + console.error(response); + + $scope.status_send.enabled = true; + $scope.status_send.is_error = true; + + if (response.status == -1) { + // Timeout + $scope.status_send.text = "Timeout request."; + } else { + // Error from server + $scope.status_send.text = "Error from server : " + response.status; + } }); - // TODO not suppose to need to reload the page, block by socket update - $window.location.reload(); + } }; $scope.$watch("model_user", function (value) { + $scope.update_point(); if (value) { $scope.prettyModelUser = JSON.stringify(value, undefined, 2); } @@ -77,13 +252,500 @@ characterApp.controller("character_ctrl", ["$scope", "$q", "$http", "$window", / }, true); $scope.$watch("model_char", function (value) { + $scope.update_point(); if (value) { $scope.prettyModelChar = JSON.stringify(value, undefined, 2); } - // todo : update player - // $scope.player = value; }, true); + $scope.update_point = function () { + $scope.character_point = {}; + $scope.character_reduce_point = {}; + $scope.character_skill = []; + $scope.character_merite = []; + $scope.count_master_tech = 0; + + if (isDefined($scope.model_char.energie)) { + for (var i = 0; i < $scope.model_char.energie.length; i++) { + var sub_key = "Energie_1"; + + if (sub_key in $scope.model_database.skill_manual) { + $scope.character_skill.push($scope.model_database.skill_manual[sub_key]); + } + + if (sub_key in $scope.model_database.point) { + var dct_key_point = $scope.model_database.point[sub_key]; + + for (var key_point in dct_key_point) { + if (dct_key_point.hasOwnProperty(key_point)) { + var point_value = dct_key_point[key_point]; + if (key_point in $scope.character_point) { + $scope.character_point[key_point] += point_value; + } else { + $scope.character_point[key_point] = point_value; + } + } + } + } + } + } + + if (isDefined($scope.model_char.endurance)) { + for (var i = 0; i < $scope.model_char.endurance.length; i++) { + var sub_key = "Endurance_1"; + + if (sub_key in $scope.model_database.skill_manual) { + $scope.character_skill.push($scope.model_database.skill_manual[sub_key]); + } + + if (sub_key in $scope.model_database.point) { + var dct_key_point = $scope.model_database.point[sub_key]; + + for (var key_point in dct_key_point) { + if (dct_key_point.hasOwnProperty(key_point)) { + var point_value = dct_key_point[key_point]; + if (key_point in $scope.character_point) { + $scope.character_point[key_point] += point_value; + } else { + $scope.character_point[key_point] = point_value; + } + } + } + } + } + } + + + if (isDefined($scope.model_char.habilites)) { + var lst_habilites = []; + + for (var i = 0; i < $scope.model_char.habilites.length; i++) { + var obj = $scope.model_char.habilites[i]; + if (isDefined(obj.options)) { + // total_xp += obj.options.length; + // Find the associate point + for (var j = 0; j < obj.options.length; j++) { + var sub_key = "habilites_" + obj.options[j]; + + if (sub_key in $scope.model_database.skill_manual) { + $scope.character_skill.push($scope.model_database.skill_manual[sub_key]); + } + + if (sub_key in $scope.model_database.point) { + lst_habilites.push(sub_key); + } + } + } + } + + for (var i = 0; i < lst_habilites.length; i++) { + var sub_key = lst_habilites[i]; + var dct_key_point = $scope.model_database.point[sub_key]; + + for (var key_point in dct_key_point) { + if (dct_key_point.hasOwnProperty(key_point)) { + var point_value = dct_key_point[key_point]; + + // Exception for salary, multiply PtPA + if (sub_key == "habilites_Salaire" && key_point == "PtPA") { + var total_value = point_value; + if (lst_habilites.indexOf("habilites_Alchimie") > -1) { + total_value += point_value; + } + if (lst_habilites.indexOf("habilites_Enchantement") > -1) { + total_value += point_value; + } + if (lst_habilites.indexOf("habilites_Artisanat") > -1) { + total_value += point_value; + } + if (lst_habilites.indexOf("habilites_Forge") > -1) { + total_value += point_value; + } + if (lst_habilites.indexOf("habilites_Herboristerie") > -1) { + total_value += point_value; + } + if (lst_habilites.indexOf("habilites_Mixture de potions") > -1) { + total_value += point_value; + } + if (lst_habilites.indexOf("habilites_Marchandage_1") > -1) { + total_value += point_value; + } + if (lst_habilites.indexOf("habilites_Marchandage_2") > -1) { + total_value += point_value; + } + if (lst_habilites.indexOf("habilites_Marchandage_3") > -1) { + total_value += point_value; + } + if (lst_habilites.indexOf("habilites_Marchandage_4") > -1) { + total_value += point_value; + } + if (lst_habilites.indexOf("habilites_Marchandage_5") > -1) { + total_value += point_value; + } + if (lst_habilites.indexOf("habilites_Sp\u00e9cialiste I - Herboristerie") > -1) { + total_value += point_value; + } + if (lst_habilites.indexOf("habilites_Sp\u00e9cialiste I - Artisanat") > -1) { + total_value += point_value; + } + if (lst_habilites.indexOf("habilites_Sp\u00e9cialiste I - Enchantement") > -1) { + total_value += point_value; + } + if (lst_habilites.indexOf("habilites_Sp\u00e9cialiste I - Forge") > -1) { + total_value += point_value; + } + if (lst_habilites.indexOf("habilites_Sp\u00e9cialiste I - Alchimie") > -1) { + total_value += point_value; + } + if (lst_habilites.indexOf("habilites_Sp\u00e9cialiste I - Mixture de Potion") > -1) { + total_value += point_value; + } + if (lst_habilites.indexOf("habilites_Sp\u00e9cialiste II - Herboristerie") > -1) { + total_value += point_value; + } + if (lst_habilites.indexOf("habilites_Sp\u00e9cialiste II - Artisanat") > -1) { + total_value += point_value; + } + if (lst_habilites.indexOf("habilites_Sp\u00e9cialiste II - Enchantement") > -1) { + total_value += point_value; + } + if (lst_habilites.indexOf("habilites_Sp\u00e9cialiste II - Alchimie") > -1) { + total_value += point_value; + } + if (lst_habilites.indexOf("habilites_Sp\u00e9cialiste II - Forge") > -1) { + total_value += point_value; + } + if (lst_habilites.indexOf("habilites_Sp\u00e9cialiste II - Mixture de Potion") > -1) { + total_value += point_value; + } + point_value = total_value; + } + + if (key_point in $scope.character_point) { + $scope.character_point[key_point] += point_value; + } else { + $scope.character_point[key_point] = point_value; + } + } + } + } + } + + if (isDefined($scope.model_char.technique_maitre)) { + for (var i = 0; i < $scope.model_char.technique_maitre.length; i++) { + var obj = $scope.model_char.technique_maitre[i]; + if (isDefined(obj.options)) { + // total_xp += obj.options.length; + // Find the associate point + for (var j = 0; j < obj.options.length; j++) { + var sub_key = "technique_maitre_" + obj.options[j]; + + if (sub_key in $scope.model_database.skill_manual) { + $scope.character_skill.push($scope.model_database.skill_manual[sub_key]); + } + + if (sub_key in $scope.model_database.point) { + var dct_key_point = $scope.model_database.point[sub_key]; + $scope.count_master_tech += 1; + + for (var key_point in dct_key_point) { + if (dct_key_point.hasOwnProperty(key_point)) { + var point_value = dct_key_point[key_point]; + if (key_point in $scope.character_point) { + $scope.character_point[key_point] += point_value; + } else { + $scope.character_point[key_point] = point_value; + } + } + } + } + } + } + } + } + + // if (isDefined($scope.model_char.merite)) { + // for (var i = 0; i < $scope.model_char.merite.length; i++) { + // if (isUndefined($scope.model_char.merite[i]) || !$scope.model_char.merite[i]) { + // continue; + // } + // // Find the associate point + // var sub_key = "merite_" + $scope.model_char.merite[i].sub_merite; + // + // if (sub_key in $scope.model_database.skill_manual) { + // $scope.character_merite.push($scope.model_database.skill_manual[sub_key]); + // } + // + // if (sub_key in $scope.model_database.point) { + // var dct_key_point = $scope.model_database.point[sub_key]; + // + // for (var key_point in dct_key_point) { + // if (dct_key_point.hasOwnProperty(key_point)) { + // var point_value = dct_key_point[key_point]; + // if (key_point in $scope.character_point) { + // $scope.character_point[key_point] += point_value; + // } else { + // $scope.character_point[key_point] = point_value; + // } + // } + // } + // } + // } + // } + + if (isDefined($scope.model_char.merite_jeu_1)) { + for (var i = 0; i < $scope.model_char.merite_jeu_1.length; i++) { + if (isUndefined($scope.model_char.merite_jeu_1[i]) || !$scope.model_char.merite_jeu_1[i]) { + continue; + } + // Find the associate point + var sub_key = "merite_" + $scope.model_char.merite_jeu_1[i].sub_merite; + + if (sub_key in $scope.model_database.skill_manual) { + $scope.character_merite.push($scope.model_database.skill_manual[sub_key]); + } + + if (sub_key in $scope.model_database.point) { + var dct_key_point = $scope.model_database.point[sub_key]; + + for (var key_point in dct_key_point) { + if (dct_key_point.hasOwnProperty(key_point)) { + var point_value = dct_key_point[key_point]; + if (key_point in $scope.character_reduce_point) { + $scope.character_reduce_point[key_point] += point_value; + } else { + $scope.character_reduce_point[key_point] = point_value; + } + } + } + } + } + } + + if (isDefined($scope.model_char.merite_jeu_2)) { + for (var i = 0; i < $scope.model_char.merite_jeu_2.length; i++) { + if (isUndefined($scope.model_char.merite_jeu_2[i]) || !$scope.model_char.merite_jeu_2[i]) { + continue; + } + // Find the associate point + var sub_key = "merite_" + $scope.model_char.merite_jeu_2[i].sub_merite; + + if (sub_key in $scope.model_database.skill_manual) { + $scope.character_merite.push($scope.model_database.skill_manual[sub_key]); + } + + if (sub_key in $scope.model_database.point) { + var dct_key_point = $scope.model_database.point[sub_key]; + + for (var key_point in dct_key_point) { + if (dct_key_point.hasOwnProperty(key_point)) { + var point_value = dct_key_point[key_point]; + if (key_point in $scope.character_reduce_point) { + $scope.character_reduce_point[key_point] += point_value; + } else { + $scope.character_reduce_point[key_point] = point_value; + } + } + } + } + } + } + + if (isDefined($scope.model_char.merite_jeu_3)) { + for (var i = 0; i < $scope.model_char.merite_jeu_3.length; i++) { + if (isUndefined($scope.model_char.merite_jeu_3[i]) || !$scope.model_char.merite_jeu_3[i]) { + continue; + } + // Find the associate point + var sub_key = "merite_" + $scope.model_char.merite_jeu_3[i].sub_merite; + + if (sub_key in $scope.model_database.skill_manual) { + $scope.character_merite.push($scope.model_database.skill_manual[sub_key]); + } + + if (sub_key in $scope.model_database.point) { + var dct_key_point = $scope.model_database.point[sub_key]; + + for (var key_point in dct_key_point) { + if (dct_key_point.hasOwnProperty(key_point)) { + var point_value = dct_key_point[key_point]; + if (key_point in $scope.character_reduce_point) { + $scope.character_reduce_point[key_point] += point_value; + } else { + $scope.character_reduce_point[key_point] = point_value; + } + } + } + } + } + } + + if (isDefined($scope.model_char.merite_jeu_4)) { + for (var i = 0; i < $scope.model_char.merite_jeu_4.length; i++) { + if (isUndefined($scope.model_char.merite_jeu_4[i]) || !$scope.model_char.merite_jeu_4[i]) { + continue; + } + // Find the associate point + var sub_key = "merite_" + $scope.model_char.merite_jeu_4[i].sub_merite; + + if (sub_key in $scope.model_database.skill_manual) { + $scope.character_merite.push($scope.model_database.skill_manual[sub_key]); + } + + if (sub_key in $scope.model_database.point) { + var dct_key_point = $scope.model_database.point[sub_key]; + + for (var key_point in dct_key_point) { + if (dct_key_point.hasOwnProperty(key_point)) { + var point_value = dct_key_point[key_point]; + if (key_point in $scope.character_point) { + $scope.character_point[key_point] += point_value; + } else { + $scope.character_point[key_point] = point_value; + } + } + } + } + } + } + + if (isDefined($scope.model_char.esclave)) { + for (var i = 0; i < $scope.model_char.esclave.length; i++) { + if (isUndefined($scope.model_char.esclave[i]) || !$scope.model_char.esclave[i]) { + continue; + } + // Find the associate point + var sub_key = "esclave_" + $scope.model_char.esclave[i].sub_esclave; + + if (sub_key in $scope.model_database.skill_manual) { + $scope.character_esclave.push($scope.model_database.skill_manual[sub_key]); + } + + if (sub_key in $scope.model_database.point) { + var dct_key_point = $scope.model_database.point[sub_key]; + + for (var key_point in dct_key_point) { + if (dct_key_point.hasOwnProperty(key_point)) { + var point_value = dct_key_point[key_point]; + if (key_point in $scope.character_point) { + $scope.character_point[key_point] += point_value; + } else { + $scope.character_point[key_point] = point_value; + } + } + } + } + } + } + + if (isDefined($scope.model_char.marche)) { + for (var i = 0; i < $scope.model_char.marche.length; i++) { + if (isUndefined($scope.model_char.marche[i]) || !$scope.model_char.marche[i]) { + continue; + } + // Find the associate point + var sub_key = "marche_" + $scope.model_char.marche[i].sub_marche; + + if (sub_key in $scope.model_database.skill_manual) { + $scope.character_marche.push($scope.model_database.skill_manual[sub_key]); + } + + if (sub_key in $scope.model_database.point) { + var dct_key_point = $scope.model_database.point[sub_key]; + + for (var key_point in dct_key_point) { + if (dct_key_point.hasOwnProperty(key_point)) { + var point_value = dct_key_point[key_point]; + if (key_point in $scope.character_point) { + $scope.character_point[key_point] += point_value; + } else { + $scope.character_point[key_point] = point_value; + } + } + } + } + } + } + + // xp + var total_xp = 0; + if ($scope.character_point.hasOwnProperty("PtXp")) { + $scope.xp_spend = -($scope.character_point["PtXp"]); + } else { + $scope.xp_spend = 0; + } + total_xp -= $scope.xp_spend; + $scope.xp_receive = $scope.xp_default; + if (isDefined($scope.model_user["xp_gn_1"]) && $scope.model_user.xp_gn_1) { + $scope.xp_receive++; + } + if (isDefined($scope.model_user["xp_gn_2"]) && $scope.model_user.xp_gn_2) { + $scope.xp_receive++; + } + if (isDefined($scope.model_user["xp_gn_3"]) && $scope.model_user.xp_gn_3) { + $scope.xp_receive++; + } + if (isDefined($scope.model_user["xp_gn_4"]) && $scope.model_user.xp_gn_4) { + $scope.xp_receive++; + } + total_xp += $scope.xp_receive; + $scope.xp_total = total_xp; + + // merite + var total_merite = 0; + if ($scope.character_point.hasOwnProperty("PtMerite")) { + $scope.merite_spend = -($scope.character_point["PtMerite"]); + } else { + $scope.merite_spend = 0; + } + + // Remove merite about old game + var reduce_total_merite = 0; + if ($scope.character_reduce_point.hasOwnProperty("PtMerite")) { + reduce_total_merite = -($scope.character_reduce_point["PtMerite"]); + } + + if ($scope.model_user.hasOwnProperty("total_point_merite")) { + $scope.merite_receive = $scope.model_user["total_point_merite"]; + } else { + $scope.merite_receive = 0; + } + $scope.merite_receive -= reduce_total_merite; + + total_merite -= $scope.merite_spend; + total_merite += $scope.merite_receive; + $scope.merite_total = total_merite; + + $scope.count_sous_ecole = 0; + if ($scope.model_char.hasOwnProperty("sous_ecole")) { + for (var i = 0; i < $scope.model_char["sous_ecole"].length; i++) { + var obj = $scope.model_char["sous_ecole"][i]; + if (obj.hasOwnProperty("sous_ecole")) { + $scope.count_sous_ecole += 1; + } + } + } + $scope.capacity_sous_ecole = $scope.get_character_point('PtSousEcoleMagieMax'); + $scope.diff_sous_ecole = $scope.capacity_sous_ecole - $scope.count_sous_ecole; + + // New player + if ($scope.xp_receive == $scope.xp_default) { + if (!$scope.character_point.hasOwnProperty("PtPA")) { + $scope.character_point["PtPA"] = 50; + } else { + $scope.character_point["PtPA"] += 50; + } + $scope.character_skill.push("Nouveau joueur +50 PA.") + } + + // Validate count master tech + if ($scope.count_master_tech > ($scope.xp_receive - $scope.xp_default + 1)) { + $scope.validated_count_master_tech = false; + } else { + $scope.validated_count_master_tech = true; + } + }; + $scope.$watch("player", function (value) { if (!value) { return; @@ -110,6 +772,27 @@ characterApp.controller("character_ctrl", ["$scope", "$q", "$http", "$window", / if (!isDefined(firstChar.rituel)) { $scope.model_char.rituel = []; } + if (!isDefined(firstChar.sous_ecole)) { + $scope.model_char.sous_ecole = []; + } + if (!isDefined(firstChar.merite_jeu_1)) { + $scope.model_char.merite_jeu_1 = []; + } + if (!isDefined(firstChar.merite_jeu_2)) { + $scope.model_char.merite_jeu_2 = []; + } + if (!isDefined(firstChar.merite_jeu_3)) { + $scope.model_char.merite_jeu_3 = []; + } + if (!isDefined(firstChar.merite_jeu_4)) { + $scope.model_char.merite_jeu_4 = []; + } + if (!isDefined(firstChar.esclave)) { + $scope.model_char.esclave = []; + } + if (!isDefined(firstChar.marche)) { + $scope.model_char.marche = []; + } if (!isDefined(firstChar.xp_naissance)) { $scope.model_char.xp_naissance = $scope.xp_default; } @@ -124,6 +807,13 @@ characterApp.controller("character_ctrl", ["$scope", "$q", "$http", "$window", / $scope.model_char.habilites = [{}]; $scope.model_char.technique_maitre = []; $scope.model_char.rituel = []; + $scope.model_char.sous_ecole = []; + $scope.model_char.merite_jeu_1 = []; + $scope.model_char.merite_jeu_2 = []; + $scope.model_char.merite_jeu_3 = []; + $scope.model_char.merite_jeu_4 = []; + $scope.model_char.esclave = []; + $scope.model_char.marche = []; $scope.model_char.xp_naissance = $scope.xp_default; $scope.model_char.xp_autre = 0; @@ -132,137 +822,137 @@ characterApp.controller("character_ctrl", ["$scope", "$q", "$http", "$window", / $scope.get_html_qr_code(); }, true); - $scope.$watch("character", function (value) { - $scope.cs_character = $scope.character; - $scope.fill_cs_character_habilites(); - }, true); +// $scope.$watch("character", function (value) { +// $scope.cs_character = $scope.character; +// // $scope.fill_cs_character_habilites(); +// }, true); - $scope.characterSheetPrintOptionChange = function (value) { - if ($scope.cs_setting == "filled") { - $scope.cs_player = $scope.player; - $scope.cs_character = $scope.character; - $scope.fill_cs_character_habilites(); - console.log($scope.getSheetOutput($scope.cs_character.endurance.total)); - } else { - $scope.cs_player = {}; - $scope.cs_character = {}; - $scope.cs_character_habilites = []; - } - }; +// $scope.characterSheetPrintOptionChange = function (value) { +// if ($scope.cs_setting == "filled") { +// $scope.cs_player = $scope.player; +// $scope.cs_character = $scope.character; +// // $scope.fill_cs_character_habilites(); +// console.log($scope.getSheetOutput($scope.cs_character.endurance.total)); +// } else { +// $scope.cs_player = {}; +// $scope.cs_character = {}; +// $scope.cs_character_habilites = []; +// } +// }; - $scope.fill_cs_character_habilites = function () { - // lvl 1 : 4 disciplines - // lvl 2 : 2 habilités - // lvl 3 : 3 options - // var max_discipline = 4; - // var max_unique_discipline = 2; - // var max_hability = 2; - // var max_unique_hability = 1; - var i_discipline = 0; - - var dct_model = [ - { - "discipline": "", - "hab_A": "", - "hab_A_1": "", - "hab_A_2": "", - "hab_A_3": "", - "hab_B": "", - "hab_B_1": "", - "hab_B_2": "", - "hab_B_3": "" - }, - { - "discipline": "", - "hab_A": "", - "hab_A_1": "", - "hab_A_2": "", - "hab_A_3": "", - "hab_B": "", - "hab_B_1": "", - "hab_B_2": "", - "hab_B_3": "" - }, - { - "discipline": "", - "hab_A": "", - "hab_A_1": "", - "hab_A_2": "", - "hab_A_3": "", - "hab_B": "", - "hab_B_1": "", - "hab_B_2": "", - "hab_B_3": "" - }, - { - "discipline": "", - "hab_A": "", - "hab_A_1": "", - "hab_A_2": "", - "hab_A_3": "", - "hab_B": "", - "hab_B_1": "", - "hab_B_2": "", - "hab_B_3": "" - } - ]; - - if ($scope.character && $scope.character.habilites) { - $scope.character.habilites.forEach(function (value) { - var option_0 = $scope.getSheetOutput(value.options[0]); - var option_1 = $scope.getSheetOutput(value.options[1]); - var option_2 = $scope.getSheetOutput(value.options[2]); - var find = false; - // validate if exist - for (var i = 0; i < i_discipline; i++) { - - if (dct_model[i].discipline == value.discipline) { - // check if repeating ability - if (!dct_model[i].hab_A) { - // fill free space - dct_model[i].hab_A = value.habilite; - dct_model[i].hab_A_1 = option_0; - dct_model[i].hab_A_2 = option_1; - dct_model[i].hab_A_3 = option_2; - - find = true; - break; - } else if (!dct_model[i].hab_B) { - // fill free space - dct_model[i].hab_B = value.habilite; - dct_model[i].hab_B_1 = option_0; - dct_model[i].hab_B_2 = option_1; - dct_model[i].hab_B_3 = option_2; - - find = true; - break; - } - // no free space, discipline will be recreate in !find section - } - } - if (!find) { - // not exist - // TODO add validation here - dct_model[i_discipline].discipline = value.discipline; - dct_model[i_discipline].hab_A = value.habilite; - dct_model[i_discipline].hab_A_1 = option_0; - dct_model[i_discipline].hab_A_2 = option_1; - dct_model[i_discipline].hab_A_3 = option_2; - - i_discipline++; - } - }); - } +// $scope.fill_cs_character_habilites = function () { +// // lvl 1 : 4 disciplines +// // lvl 2 : 2 habilités +// // lvl 3 : 3 options +// // var max_discipline = 4; +// // var max_unique_discipline = 2; +// // var max_hability = 2; +// // var max_unique_hability = 1; +// var i_discipline = 0; +// +// var dct_model = [ +// { +// "discipline": "", +// "hab_A": "", +// "hab_A_1": "", +// "hab_A_2": "", +// "hab_A_3": "", +// "hab_B": "", +// "hab_B_1": "", +// "hab_B_2": "", +// "hab_B_3": "" +// }, +// { +// "discipline": "", +// "hab_A": "", +// "hab_A_1": "", +// "hab_A_2": "", +// "hab_A_3": "", +// "hab_B": "", +// "hab_B_1": "", +// "hab_B_2": "", +// "hab_B_3": "" +// }, +// { +// "discipline": "", +// "hab_A": "", +// "hab_A_1": "", +// "hab_A_2": "", +// "hab_A_3": "", +// "hab_B": "", +// "hab_B_1": "", +// "hab_B_2": "", +// "hab_B_3": "" +// }, +// { +// "discipline": "", +// "hab_A": "", +// "hab_A_1": "", +// "hab_A_2": "", +// "hab_A_3": "", +// "hab_B": "", +// "hab_B_1": "", +// "hab_B_2": "", +// "hab_B_3": "" +// } +// ]; +// +// if ($scope.character && $scope.character.habilites) { +// $scope.character.habilites.forEach(function (value) { +// var option_0 = $scope.getSheetOutput(value.options[0]); +// var option_1 = $scope.getSheetOutput(value.options[1]); +// var option_2 = $scope.getSheetOutput(value.options[2]); +// var find = false; +// // validate if exist +// for (var i = 0; i < i_discipline; i++) { +// +// if (dct_model[i].discipline == value.discipline) { +// // check if repeating ability +// if (!dct_model[i].hab_A) { +// // fill free space +// dct_model[i].hab_A = value.habilite; +// dct_model[i].hab_A_1 = option_0; +// dct_model[i].hab_A_2 = option_1; +// dct_model[i].hab_A_3 = option_2; +// +// find = true; +// break; +// } else if (!dct_model[i].hab_B) { +// // fill free space +// dct_model[i].hab_B = value.habilite; +// dct_model[i].hab_B_1 = option_0; +// dct_model[i].hab_B_2 = option_1; +// dct_model[i].hab_B_3 = option_2; +// +// find = true; +// break; +// } +// // no free space, discipline will be recreate in !find section +// } +// } +// if (!find) { +// // not exist +// // TODO add validation here +// dct_model[i_discipline].discipline = value.discipline; +// dct_model[i_discipline].hab_A = value.habilite; +// dct_model[i_discipline].hab_A_1 = option_0; +// dct_model[i_discipline].hab_A_2 = option_1; +// dct_model[i_discipline].hab_A_3 = option_2; +// +// i_discipline++; +// } +// }); +// } +// +// $scope.cs_character_habilites = dct_model; +// }; - $scope.cs_character_habilites = dct_model; - }; - - //get the string to output on the character sheet +//get the string to output on the character sheet $scope.getSheetOutput = function (value) { return isDefined(value) ? value.toString() : ""; }; - //fills the checks array with booleans used as models to determine whether checkboxes are checked or not +//fills the checks array with booleans used as models to determine whether checkboxes are checked or not $scope.setChecks = function () { $scope.cs_checks = []; @@ -378,106 +1068,35 @@ characterApp.controller("character_ctrl", ["$scope", "$q", "$http", "$window", / window.print(); }; - $scope.countTotalXp = function () { - if ($scope.character === null || $scope.model_char === null) { - return 0; - } - var total_xp = $scope.model_char.xp_naissance + $scope.model_char.xp_autre; - if (isDefined($scope.model_char.xp_gn_1_2016)) { - total_xp += $scope.model_char.xp_gn_1_2016; + $scope.get_status_validation = function () { + // need to fix if some negative value + // xp is preferred to use all point + if ($scope.xp_total < 0 || $scope.merite_total < 0 || $scope.diff_sous_ecole < 0 || !$scope.model_char.name || !$scope.model_char.faction || !$scope.validated_count_master_tech) { + return -1; + } else if ($scope.xp_total > 0 || $scope.diff_sous_ecole > 0) { + return 1; } - if (isDefined($scope.model_char.xp_gn_2_2016)) { - total_xp += $scope.model_char.xp_gn_2_2016; - } - if (isDefined($scope.model_char.xp_gn_3_2016)) { - total_xp += $scope.model_char.xp_gn_3_2016; - } - if (isDefined($scope.model_char.xp_gn_4_2016)) { - total_xp += $scope.model_char.xp_gn_4_2016; - } - if (isDefined($scope.model_char.xp_donjon_1_2017)) { - total_xp += $scope.model_char.xp_donjon_1_2017; - } - if (isDefined($scope.model_char.xp_gn_1_2017)) { - total_xp += $scope.model_char.xp_gn_1_2017; - } - if (isDefined($scope.model_char.xp_gn_2_2017)) { - total_xp += $scope.model_char.xp_gn_2_2017; - } - if (isDefined($scope.model_char.xp_gn_3_2017)) { - total_xp += $scope.model_char.xp_gn_3_2017; - } - if (isDefined($scope.model_char.xp_gn_4_2017)) { - total_xp += $scope.model_char.xp_gn_4_2017; - } - return total_xp; + return 0; }; - $scope.countTotalCostXp = function () { - if ($scope.character === null || $scope.model_char === null) { + $scope.get_character_point = function (name) { + if (!$scope.character_point.hasOwnProperty(name)) { return 0; } - var total_xp = 0; - if (isDefined($scope.model_char.energie)) { - total_xp += $scope.model_char.energie.length; - } - if (isDefined($scope.model_char.endurance)) { - total_xp += $scope.model_char.endurance.length; - } - if (isDefined($scope.model_char.habilites)) { - for (var i = 0; i < $scope.model_char.habilites.length; i++) { - var obj = $scope.model_char.habilites[i]; - if (isDefined(obj.options)) { - total_xp += obj.options.length; - } - } - } - if (isDefined($scope.model_char.technique_maitre)) { - for (var i = 0; i < $scope.model_char.technique_maitre.length; i++) { - if ($scope.model_char.technique_maitre[i]) { - total_xp += 1; - } - } - } - return total_xp; - }; - - $scope.diffTotalXp = function () { - return $scope.countTotalXp() - $scope.countTotalCostXp() - }; - - $scope.showDiffTotalXp = function () { - var diff = $scope.diffTotalXp(); - if (diff > 0) { - return "+" + diff; - } - return diff; + return $scope.character_point[name]; }; $scope.get_html_qr_code = function () { var typeNumber = 5; var errorCorrectionLevel = 'L'; var qr = qrcode(typeNumber, errorCorrectionLevel); - var data = $window.location.origin + "/character#/?id_player=" + $scope.player.id + var data = $window.location.origin + "/character#/?id_player=" + $scope.player.id; $scope.url_qr_code = data; qr.addData(data); qr.make(); $scope.html_qr_code = qr.createImgTag(); }; - // socket.onmessage = function (e) { - // $scope.message = JSON.parse(e.data); - // console.log($scope.message); - // $scope.$apply(); - // }; - -// For admin page -// $http.get("/cmd/character_view").success( -// function (response/*, status, headers, config*/) { -// $scope.ddb_user = response.data; -// } -// ); - $scope.is_main = $window.location.hash.substring($window.location.hash.length - 4) == "#!/"; if ($scope.is_main) { $scope.player_id_from_get = ""; @@ -508,14 +1127,14 @@ characterApp.controller("character_ctrl", ["$scope", "$q", "$http", "$window", / console.log(response.data); var data = response.data; // special effect, if only one character, select first one - if (data.length >= 1) { - $scope.player = data[0]; - $scope.character = data[0].character[0]; - $scope.setCharacterData(data[0]); - $scope.player = data[0]; - $scope.setCharacterData($scope.character); - - $scope.$apply(); - } + if (data.length >= 1 && !$scope.is_admin) { + $scope.player = data[0]; + $scope.character = data[0].character[0]; + $scope.setCharacterData(data[0]); + $scope.player = data[0]; + $scope.setCharacterData($scope.character); + + $scope.$apply(); + } }); }]); diff --git a/src/web/resources/js/tl_module/character_ctrl/tl_character_schema.js b/src/web/resources/js/tl_module/character_ctrl/tl_character_schema.js deleted file mode 100644 index 4c1dc931..00000000 --- a/src/web/resources/js/tl_module/character_ctrl/tl_character_schema.js +++ /dev/null @@ -1,822 +0,0 @@ -function TL_Schema($scope) { - $scope.schema_user = { - type: "object", - title: "Joueur", - properties: { - nickname: { - title: "Surnom du joueur", - type: "string", - minLength: 2 - }, - id: { - type: "string" - }, - name: { - title: "Prénom et Nom du joueur", - type: "string", - minLength: 2 - }, - email: { - title: "Courriel", - type: "string", - pattern: "^\\S+@\\S+$" - }, - comment: { - title: "Commentaire", - type: "string" - }, - comment_admin: { - title: "ADMIN commentaire", - type: "string" - }, - passe_saison_2017: { - type: "boolean", - } - }, - required: ["name"] - }; - - $scope.schema_char = { - type: "object", - title: "Joueur", - properties: { - name: { - title: "Nom du personnage", - type: "string" - }, - id: { - type: "string" - }, - xp_naissance: { - type: "integer", - }, - xp_gn_1_2016: { - type: "boolean", - }, - xp_gn_2_2016: { - type: "boolean", - }, - xp_gn_3_2016: { - type: "boolean", - }, - xp_gn_4_2016: { - type: "boolean", - }, - xp_donjon_1_2017: { - type: "boolean", - }, - xp_gn_1_2017: { - type: "boolean", - }, - xp_gn_2_2017: { - type: "boolean", - }, - xp_gn_3_2017: { - type: "boolean", - }, - xp_gn_4_2017: { - type: "boolean", - }, - xp_autre: { - type: "integer", - }, - faction: { - title: "Faction", - type: "string" - }, - sous_faction: { - title: "Sous-faction", - type: "string" - }, - endurance: { - title: "Endurance", - type: "array", - items: {type: "string"} - }, - energie: { - title: "Énergie", - type: "array", - items: {type: "string"} - }, - habilites: { - type: "array", - items: { - type: "object", - properties: { - - discipline: { - title: "Discipline", - type: "string" - }, - - habilite: { - title: "Habilité", - type: "string" - }, - - options: { - title: "Option", - type: "array", - items: {type: "string"} - } - } - } - }, - rituel_ecole: { - title: "Rituel/École", - type: "array", - items: {type: "string"} - }, - rituel: { - type: "array", - title: "Rituel", - maxItems: 100, - minItems: 0, - uniqueItems: true, - items: { - title: "Rituel", - type: "string" - } - }, - technique_maitre: { - type: "array", - title: "Techniques de Maitre", - maxItems: 10, - minItems: 0, - uniqueItems: true, - items: { - title: "Technique de Maitre", - type: "string" - } - } - } -// }, -// required: ["faction", "xp_naissance", "xp_autre"] - }; - - if ($scope.is_admin) { - $scope.form_user = [ - { - key: "name", - placeholder: "Votre nom entier (prénom et nom)" - }, - { - key: "nickname", - placeholder: "Votre surnom - facultatif" - }, - { - key: "email", - placeholder: "Votre courriel" - }, - { - key: "comment", - type: "textarea", - placeholder: "" - }, - { - key: "comment_admin", - type: "textarea", - placeholder: "" - }, - { - key: "passe_saison_2017", - placeholder: "Avez-vous acheté la passe saison 2017?" - } - ]; - $scope.form_char = [ - { - key: "name", - placeholder: "Votre nom de joueur" - }, - { - key: "xp_naissance", - placeholder: "Défaut 6 xp pour nouveau joueur." - }, - { - key: "xp_gn_1_2016", - placeholder: "Êtes-vous venu au jeu du 27 mai 2016?" - }, - { - key: "xp_gn_2_2016", - placeholder: "Êtes-vous venu au jeu du 29 juillet 2016?" - }, - { - key: "xp_gn_3_2016", - placeholder: "Êtes-vous venu au jeu du 26 août 2016?" - }, - { - key: "xp_gn_4_2016", - placeholder: "Êtes-vous venu au jeu du 30 septembre 2016?" - }, - { - key: "xp_donjon_1_2017", - placeholder: "Êtes-vous venu au donjon du 8 avril 2017?" - }, - { - key: "xp_gn_1_2017", - placeholder: "Êtes-vous venu au donjon du 5 mai 2017?" - }, - { - key: "xp_gn_2_2017", - placeholder: "Êtes-vous venu au donjon du 7 juillet 2017?" - }, - { - key: "xp_gn_3_2017", - placeholder: "Êtes-vous venu au donjon du 1 septembre 2017?" - }, - { - key: "xp_gn_4_2017", - placeholder: "Êtes-vous venu au donjon du 6 octobre 2017?" - }, - { - key: "xp_autre", - placeholder: "Peut-être des points de mérites ou autre." - }, - { - key: "faction", - type: "select", - titleMap: [ - {value: "Vanican", name: "Vanican"}, - {value: "Canavim", name: "Canavim"}, - {value: "Vallam", name: "Vallam"}, - {value: "Sarsare", name: "Sarsare"} - ] - }, - { - key: "sous_faction", - type: "select", - titleMap: [ - {value: "empty", name: "- Aucune sous-faction -"}, - {value: "Sanglier", name: "Sanglier"}, - {value: "Faucheur", name: "Faucheur"}, - {value: "Balmont", name: "Balmont"}, - {value: "Druide", name: "Druide"}, - {value: "Smith", name: "Smith"}, - {value: "Angbar", name: "Angbar"}, - {value: "La Meute", name: "La Meute"} - ] - }, - { - key: "endurance", - type: "strapselect", - placeholder: "", - options: { - multiple: "true" - }, - titleMap: [ - {value: "Endurance_1", name: "+1"}, - {value: "Endurance_2", name: "+1"}, - {value: "Endurance_3", name: "+1"} - ] - }, - { - key: "energie", - type: "strapselect", - placeholder: "", - options: { - multiple: "true" - }, - titleMap: [ - {value: "Energie_1", name: "+2"}, - {value: "Energie_2", name: "+2"}, - {value: "Energie_3", name: "+2"} - ] - }, - { - key: "habilites", - add: "Ajouter Discipline", - style: { - add: "btn-success" - }, - items: [ - { - key: "habilites[].discipline", - type: "strapselect", - placeholder: "", - options: { - inlineMaxLength: 5 - }, - titleMap: [ - {value: "Combattante", name: "Combattante"}, - {value: "Sournoise", name: "Sournoise"}, - {value: "Magique", name: "Magique"}, - {value: "Professionnelle", name: "Professionnelle"} - ] - }, - { - key: "habilites[].habilite", - type: "strapselect", - options: { - filterTriggers: ["model.habilites[arrayIndex].discipline"], - filter: "model.habilites[arrayIndex].discipline==item.category", - inlineMaxLength: 5, - maxLength: 3 - }, - placeholder: "", - titleMap: [ - {value: "Discipline", name: "Discipline", category: "Combattante"}, - {value: "Karma", name: "Karma", category: "Combattante"}, - {value: "Offense", name: "Offense", category: "Combattante"}, - {value: "Défense", name: "Défense", category: "Combattante"}, - - {value: "Alchimie", name: "Alchimie", category: "Sournoise"}, - {value: "Embuscade", name: "Embuscade", category: "Sournoise"}, - {value: "Fourberie", name: "Fourberie", category: "Sournoise"}, - {value: "Travail de précision", name: "Travail de précision", category: "Sournoise"}, - - {value: "Artisanat Arcane", name: "Artisanat Arcane", category: "Magique"}, - {value: "Rituel", name: "Rituel", category: "Magique"}, - {value: "Sorcellerie", name: "Sorcellerie", category: "Magique"}, - {value: "Thaumaturgie", name: "Thaumaturgie", category: "Magique"}, - - {value: "Baratin", name: "Baratin", category: "Professionnelle"}, - {value: "Marchandage", name: "Marchandage", category: "Professionnelle"}, - {value: "Médecine", name: "Médecine", category: "Professionnelle"}, - {value: "Métier", name: "Métier", category: "Professionnelle"} - ] - }, - { - key: "habilites[].options", - type: "strapselect", - // onChange: function (modelValue, form) { - // if (modelValue.length > 3) { - // // var set1 = new Set(modelValue); - // // var set2 = new Set(this.last_value); - // // var difference = new Set([...set1].filter(x => !set2.has(x))); - // // console.log(difference); - // // var difference = []; - // // jQuery.grep(this.last_value, function (el) { - // // if (jQuery.inArray(el, modelValue) == -1) difference.push(el); - // // }); - // // console.log(difference); - // $scope.model.habilites[arrayIndex].option = this.last_value; - // } else { - // this.last_value = modelValue; - // } - // }, - options: { - multiple: "true", - filterTriggers: ["model.habilites[arrayIndex].habilite"], - filter: "model.habilites[arrayIndex].habilite==item.category", - inlineMaxLength: 5 - }, - placeholder: "", - titleMap: [ - {value: "Résilience", name: "Résilience", category: "Discipline"}, - {value: "Endurcie", name: "Endurcie", category: "Discipline"}, - {value: "Loyauté", name: "Loyauté", category: "Discipline"}, - {value: "Doctrine", name: "Doctrine", category: "Discipline"}, - {value: "Vigilance", name: "Vigilance", category: "Discipline"}, - - {value: "Karma_1", name: "+2", category: "Karma"}, - {value: "Karma_2", name: "+2", category: "Karma"}, - {value: "Karma_3", name: "+2", category: "Karma"}, - {value: "Karma_4", name: "+2", category: "Karma"}, - {value: "Karma_5", name: "+2", category: "Karma"}, - - {value: "Assaut", name: "Assaut", category: "Offense"}, - {value: "Jambette", name: "Jambette", category: "Offense"}, - {value: "Désarmement", name: "Désarmement", category: "Offense"}, - {value: "Coupe-souffle", name: "Coupe-souffle", category: "Offense"}, - {value: "Charge", name: "Charge", category: "Offense"}, - - {value: "Esquive", name: "Esquive", category: "Défense"}, - {value: "Déflexion", name: "Déflexion", category: "Défense"}, - {value: "Déviation", name: "Déviation", category: "Défense"}, - {value: "Santé", name: "Santé", category: "Défense"}, - {value: "Second Souffle", name: "Second Souffle", category: "Défense"}, - - {value: "Alchimie_1", name: "4", category: "Alchimie"}, - {value: "Alchimie_2", name: "+2", category: "Alchimie"}, - {value: "Alchimie_3", name: "+2", category: "Alchimie"}, - {value: "Alchimie_4", name: "+2", category: "Alchimie"}, - {value: "Alchimie_5", name: "+2", category: "Alchimie"}, - - {value: "Camouflage", name: "Camouflage", category: "Embuscade"}, - {value: "Dissimulation", name: "Dissimulation", category: "Embuscade"}, - {value: "Capture", name: "Capture", category: "Embuscade"}, - {value: "Piège", name: "Piège", category: "Embuscade"}, - {value: "Aveuglement", name: "Aveuglement", category: "Embuscade"}, - - {value: "Attaque sournoise", name: "Attaque sournoise", category: "Fourberie"}, - {value: "Coup bas", name: "Coup bas", category: "Fourberie"}, - {value: "Coup sonnant", name: "Coup sonnant", category: "Fourberie"}, - {value: "Coupe-jarret", name: "Coupe-jarret", category: "Fourberie"}, - {value: "Stylet", name: "Stylet", category: "Fourberie"}, - - {value: "Serrurier", name: "Serrurier", category: "Travail de précision"}, - {value: "Évasion", name: "Évasion", category: "Travail de précision"}, - {value: "Désamorçage", name: "Désamorçage", category: "Travail de précision"}, - {value: "Torture", name: "Torture", category: "Travail de précision"}, - {value: "Vol à la tire", name: "Vol à la tire", category: "Travail de précision"}, - - {value: "Mixture de potions", name: "Mixture de potions", category: "Artisanat Arcane"}, - {value: "Enchantement", name: "Enchantement", category: "Artisanat Arcane"}, - {value: "Infusion", name: "Infusion", category: "Artisanat Arcane"}, - {value: "Réparation", name: "Réparation", category: "Artisanat Arcane"}, - {value: "Disjonction", name: "Disjonction", category: "Artisanat Arcane"}, - - {value: "Rituel_1", name: "6", category: "Rituel"}, - {value: "Rituel_2", name: "+3", category: "Rituel"}, - {value: "Rituel_3", name: "+3", category: "Rituel"}, - {value: "Rituel_4", name: "+3", category: "Rituel"}, - {value: "Rituel_5", name: "+3", category: "Rituel"}, - - {value: "Frénésie", name: "Frénésie", category: "Sorcellerie"}, - {value: "Terreur", name: "Terreur", category: "Sorcellerie"}, - {value: "Noirceur", name: "Noirceur", category: "Sorcellerie"}, - {value: "Silence", name: "Silence", category: "Sorcellerie"}, - {value: "Éclair", name: "Éclair", category: "Sorcellerie"}, - - {value: "Guérison", name: "Guérison", category: "Thaumaturgie"}, - {value: "Réanimation", name: "Réanimation", category: "Thaumaturgie"}, - {value: "Réssurection", name: "Réssurection", category: "Thaumaturgie"}, - {value: "Liberté", name: "Liberté", category: "Thaumaturgie"}, - {value: "Voix", name: "Voix", category: "Thaumaturgie"}, - - {value: "Diplomatie", name: "Diplomatie", category: "Baratin"}, - {value: "Mensonge", name: "Mensonge", category: "Baratin"}, - {value: "Revenu", name: "Revenu", category: "Baratin"}, - {value: "Verbomoteur", name: "Verbomoteur", category: "Baratin"}, - {value: "Discours", name: "Discours", category: "Baratin"}, - - {value: "Marchandage_1", name: "4", category: "Marchandage"}, - {value: "Marchandage_2", name: "+2", category: "Marchandage"}, - {value: "Marchandage_3", name: "+2", category: "Marchandage"}, - {value: "Marchandage_4", name: "+2", category: "Marchandage"}, - {value: "Marchandage_5", name: "+2", category: "Marchandage"}, - - {value: "Opération", name: "Opération", category: "Médecine"}, - {value: "Suture", name: "Suture", category: "Médecine"}, - {value: "Psychiatrie", name: "Psychiatrie", category: "Médecine"}, - {value: "Relaxation", name: "Relaxation", category: "Médecine"}, - {value: "Pharmacie", name: "Pharmacie", category: "Médecine"}, - - {value: "Artisinat", name: "Artisinat", category: "Métier"}, - {value: "Forge", name: "Forge", category: "Métier"}, - {value: "Herboristerie", name: "Herboristerie", category: "Métier"}, - {value: "Spécialiste I - Herboristerie", name: "Spécialiste I - Herboristerie", category: "Métier"}, - {value: "Spécialiste I - Artisanat", name: "Spécialiste I - Artisanat", category: "Métier"}, - {value: "Spécialiste I - Enchantement", name: "Spécialiste I - Enchantement", category: "Métier"}, - {value: "Spécialiste I - Forge", name: "Spécialiste I - Forge", category: "Métier"}, - { - value: "Spécialiste I - Mixture de Potion", - name: "Spécialiste I - Mixture de Potion", - category: "Métier" - }, - {value: "Spécialiste II - Herboristerie", name: "Spécialiste II - Herboristerie", category: "Métier"}, - {value: "Spécialiste II - Artisanat", name: "Spécialiste II - Artisanat", category: "Métier"}, - {value: "Spécialiste II - Enchantement", name: "Spécialiste II - Enchantement", category: "Métier"}, - {value: "Spécialiste II - Forge", name: "Spécialiste II - Forge", category: "Métier"}, - { - value: "Spécialiste II - Mixture de Potion", - name: "Spécialiste II - Mixture de Potion", - category: "Métier" - } - ] - } - ] - }, - { - key: "rituel_ecole", - type: "strapselect", - placeholder: "", - options: { - multiple: "true" - }, - titleMap: [ - {value: "Démonologie", name: "Démonologie"}, - {value: "Nécromancie", name: "Nécromancie"}, - {value: "Nature", name: "Nature"}, - {value: "Protection", name: "Protection"}, - {value: "Élémentaire", name: "Élémentaire"} - ] - }, - { - key: "rituel", - add: "Ajout d'un rituel", - style: { - add: "btn-success" - }, - }, - { - key: "technique_maitre", - add: "Ajouter Technique de maitre", - style: { - add: "btn-success" - }, - }, - { - type: "submit", - style: "btn-info", - title: "Enregistrer" - } - ]; - } else { - $scope.form_user = [ - { - key: "name", - placeholder: "Votre nom entier (prénom et nom)" - }, - { - key: "nickname", - placeholder: "Votre surnom - facultatif" - }, - { - key: "email", - placeholder: "Votre courriel" - }, - { - key: "comment", - type: "textarea", - placeholder: "Je ne sais pas quoi écrire." - } - ]; - - $scope.form_char = [ - { - key: "name", - placeholder: "Votre nom de joueur" - }, - { - key: "faction", - type: "select", - titleMap: [ - {value: "Vanican", name: "Vanican"}, - {value: "Canavim", name: "Canavim"}, - {value: "Vallam", name: "Vallam"}, - {value: "Sarsare", name: "Sarsare"} - ] - }, - { - key: "sous_faction", - type: "select", - titleMap: [ - {value: "empty", name: "- Aucune sous-faction -"}, - {value: "Sanglier", name: "Sanglier"}, - {value: "Faucheur", name: "Faucheur"}, - {value: "Balmont", name: "Balmont"}, - {value: "Druide", name: "Druide"}, - {value: "Smith", name: "Smith"}, - {value: "Angbar", name: "Angbar"}, - {value: "La Meute", name: "La Meute"} - ] - }, - { - key: "endurance", - type: "strapselect", - placeholder: "", - options: { - multiple: "true" - }, - titleMap: [ - {value: "Endurance_1", name: "+1"}, - {value: "Endurance_2", name: "+1"}, - {value: "Endurance_3", name: "+1"} - ] - }, - { - key: "energie", - type: "strapselect", - placeholder: "", - options: { - multiple: "true" - }, - titleMap: [ - {value: "Energie_1", name: "+2"}, - {value: "Energie_2", name: "+2"}, - {value: "Energie_3", name: "+2"} - ] - }, - { - key: "habilites", - add: "Ajouter Discipline", - style: { - add: "btn-success" - }, - items: [ - { - key: "habilites[].discipline", - type: "strapselect", - placeholder: "", - options: { - inlineMaxLength: 5 - }, - titleMap: [ - {value: "Combattante", name: "Combattante"}, - {value: "Sournoise", name: "Sournoise"}, - {value: "Magique", name: "Magique"}, - {value: "Professionnelle", name: "Professionnelle"} - ] - }, - { - key: "habilites[].habilite", - type: "strapselect", - options: { - filterTriggers: ["model.habilites[arrayIndex].discipline"], - filter: "model.habilites[arrayIndex].discipline==item.category", - inlineMaxLength: 5, - maxLength: 3 - }, - placeholder: "", - titleMap: [ - {value: "Discipline", name: "Discipline", category: "Combattante"}, - {value: "Karma", name: "Karma", category: "Combattante"}, - {value: "Offense", name: "Offense", category: "Combattante"}, - {value: "Défense", name: "Défense", category: "Combattante"}, - - {value: "Alchimie", name: "Alchimie", category: "Sournoise"}, - {value: "Embuscade", name: "Embuscade", category: "Sournoise"}, - {value: "Fourberie", name: "Fourberie", category: "Sournoise"}, - {value: "Travail de précision", name: "Travail de précision", category: "Sournoise"}, - - {value: "Artisanat Arcane", name: "Artisanat Arcane", category: "Magique"}, - {value: "Rituel", name: "Rituel", category: "Magique"}, - {value: "Sorcellerie", name: "Sorcellerie", category: "Magique"}, - {value: "Thaumaturgie", name: "Thaumaturgie", category: "Magique"}, - - {value: "Baratin", name: "Baratin", category: "Professionnelle"}, - {value: "Marchandage", name: "Marchandage", category: "Professionnelle"}, - {value: "Médecine", name: "Médecine", category: "Professionnelle"}, - {value: "Métier", name: "Métier", category: "Professionnelle"} - ] - }, - { - key: "habilites[].options", - type: "strapselect", - // onChange: function (modelValue, form) { - // if (modelValue.length > 3) { - // // var set1 = new Set(modelValue); - // // var set2 = new Set(this.last_value); - // // var difference = new Set([...set1].filter(x => !set2.has(x))); - // // console.log(difference); - // // var difference = []; - // // jQuery.grep(this.last_value, function (el) { - // // if (jQuery.inArray(el, modelValue) == -1) difference.push(el); - // // }); - // // console.log(difference); - // $scope.model.habilites[arrayIndex].option = this.last_value; - // } else { - // this.last_value = modelValue; - // } - // }, - options: { - multiple: "true", - filterTriggers: ["model.habilites[arrayIndex].habilite"], - filter: "model.habilites[arrayIndex].habilite==item.category", - inlineMaxLength: 5 - }, - placeholder: "", - titleMap: [ - {value: "Résilience", name: "Résilience", category: "Discipline"}, - {value: "Endurcie", name: "Endurcie", category: "Discipline"}, - {value: "Loyauté", name: "Loyauté", category: "Discipline"}, - {value: "Doctrine", name: "Doctrine", category: "Discipline"}, - {value: "Vigilance", name: "Vigilance", category: "Discipline"}, - - {value: "Karma_1", name: "+2", category: "Karma"}, - {value: "Karma_2", name: "+2", category: "Karma"}, - {value: "Karma_3", name: "+2", category: "Karma"}, - {value: "Karma_4", name: "+2", category: "Karma"}, - {value: "Karma_5", name: "+2", category: "Karma"}, - - {value: "Assaut", name: "Assaut", category: "Offense"}, - {value: "Jambette", name: "Jambette", category: "Offense"}, - {value: "Désarmement", name: "Désarmement", category: "Offense"}, - {value: "Coupe-souffle", name: "Coupe-souffle", category: "Offense"}, - {value: "Charge", name: "Charge", category: "Offense"}, - - {value: "Esquive", name: "Esquive", category: "Défense"}, - {value: "Déflexion", name: "Déflexion", category: "Défense"}, - {value: "Déviation", name: "Déviation", category: "Défense"}, - {value: "Santé", name: "Santé", category: "Défense"}, - {value: "Second Souffle", name: "Second Souffle", category: "Défense"}, - - {value: "Alchimie_1", name: "4", category: "Alchimie"}, - {value: "Alchimie_2", name: "+2", category: "Alchimie"}, - {value: "Alchimie_3", name: "+2", category: "Alchimie"}, - {value: "Alchimie_4", name: "+2", category: "Alchimie"}, - {value: "Alchimie_5", name: "+2", category: "Alchimie"}, - - {value: "Camouflage", name: "Camouflage", category: "Embuscade"}, - {value: "Dissimulation", name: "Dissimulation", category: "Embuscade"}, - {value: "Capture", name: "Capture", category: "Embuscade"}, - {value: "Piège", name: "Piège", category: "Embuscade"}, - {value: "Aveuglement", name: "Aveuglement", category: "Embuscade"}, - - {value: "Attaque sournoise", name: "Attaque sournoise", category: "Fourberie"}, - {value: "Coup bas", name: "Coup bas", category: "Fourberie"}, - {value: "Coup sonnant", name: "Coup sonnant", category: "Fourberie"}, - {value: "Coupe-jarret", name: "Coupe-jarret", category: "Fourberie"}, - {value: "Stylet", name: "Stylet", category: "Fourberie"}, - - {value: "Serrurier", name: "Serrurier", category: "Travail de précision"}, - {value: "Évasion", name: "Évasion", category: "Travail de précision"}, - {value: "Désamorçage", name: "Désamorçage", category: "Travail de précision"}, - {value: "Torture", name: "Torture", category: "Travail de précision"}, - {value: "Vol à la tire", name: "Vol à la tire", category: "Travail de précision"}, - - {value: "Mixture de potions", name: "Mixture de potions", category: "Artisanat Arcane"}, - {value: "Enchantement", name: "Enchantement", category: "Artisanat Arcane"}, - {value: "Infusion", name: "Infusion", category: "Artisanat Arcane"}, - {value: "Réparation", name: "Réparation", category: "Artisanat Arcane"}, - {value: "Disjonction", name: "Disjonction", category: "Artisanat Arcane"}, - - {value: "Rituel_1", name: "6", category: "Rituel"}, - {value: "Rituel_2", name: "+3", category: "Rituel"}, - {value: "Rituel_3", name: "+3", category: "Rituel"}, - {value: "Rituel_4", name: "+3", category: "Rituel"}, - {value: "Rituel_5", name: "+3", category: "Rituel"}, - - {value: "Frénésie", name: "Frénésie", category: "Sorcellerie"}, - {value: "Terreur", name: "Terreur", category: "Sorcellerie"}, - {value: "Noirceur", name: "Noirceur", category: "Sorcellerie"}, - {value: "Silence", name: "Silence", category: "Sorcellerie"}, - {value: "Éclair", name: "Éclair", category: "Sorcellerie"}, - - {value: "Guérison", name: "Guérison", category: "Thaumaturgie"}, - {value: "Réanimation", name: "Réanimation", category: "Thaumaturgie"}, - {value: "Réssurection", name: "Réssurection", category: "Thaumaturgie"}, - {value: "Liberté", name: "Liberté", category: "Thaumaturgie"}, - {value: "Voix", name: "Voix", category: "Thaumaturgie"}, - - {value: "Diplomatie", name: "Diplomatie", category: "Baratin"}, - {value: "Mensonge", name: "Mensonge", category: "Baratin"}, - {value: "Revenu", name: "Revenu", category: "Baratin"}, - {value: "Verbomoteur", name: "Verbomoteur", category: "Baratin"}, - {value: "Discours", name: "Discours", category: "Baratin"}, - - {value: "Marchandage_1", name: "4", category: "Marchandage"}, - {value: "Marchandage_2", name: "+2", category: "Marchandage"}, - {value: "Marchandage_3", name: "+2", category: "Marchandage"}, - {value: "Marchandage_4", name: "+2", category: "Marchandage"}, - {value: "Marchandage_5", name: "+2", category: "Marchandage"}, - - {value: "Opération", name: "Opération", category: "Médecine"}, - {value: "Suture", name: "Suture", category: "Médecine"}, - {value: "Psychiatrie", name: "Psychiatrie", category: "Médecine"}, - {value: "Relaxation", name: "Relaxation", category: "Médecine"}, - {value: "Pharmacie", name: "Pharmacie", category: "Médecine"}, - - {value: "Artisinat", name: "Artisinat", category: "Métier"}, - {value: "Forge", name: "Forge", category: "Métier"}, - {value: "Herboristerie", name: "Herboristerie", category: "Métier"}, - {value: "Spécialiste I - Herboristerie", name: "Spécialiste I - Herboristerie", category: "Métier"}, - {value: "Spécialiste I - Artisanat", name: "Spécialiste I - Artisanat", category: "Métier"}, - {value: "Spécialiste I - Enchantement", name: "Spécialiste I - Enchantement", category: "Métier"}, - {value: "Spécialiste I - Forge", name: "Spécialiste I - Forge", category: "Métier"}, - { - value: "Spécialiste I - Mixture de Potion", - name: "Spécialiste I - Mixture de Potion", - category: "Métier" - }, - {value: "Spécialiste II - Herboristerie", name: "Spécialiste II - Herboristerie", category: "Métier"}, - {value: "Spécialiste II - Artisanat", name: "Spécialiste II - Artisanat", category: "Métier"}, - {value: "Spécialiste II - Enchantement", name: "Spécialiste II - Enchantement", category: "Métier"}, - {value: "Spécialiste II - Forge", name: "Spécialiste II - Forge", category: "Métier"}, - { - value: "Spécialiste II - Mixture de Potion", - name: "Spécialiste II - Mixture de Potion", - category: "Métier" - } - ] - } - ] - }, - { - key: "rituel_ecole", - type: "strapselect", - placeholder: "", - options: { - multiple: "true" - }, - titleMap: [ - {value: "Démonologie", name: "Démonologie"}, - {value: "Nécromancie", name: "Nécromancie"}, - {value: "Nature", name: "Nature"}, - {value: "Protection", name: "Protection"}, - {value: "Élémentaire", name: "Élémentaire"} - ] - }, - { - key: "rituel", - add: "Ajout d'un rituel", - style: { - add: "btn-success" - }, - }, - { - key: "technique_maitre", - add: "Ajouter Technique de maitre", - style: { - add: "btn-success" - }, - }, - { - type: "submit", - style: "btn-info", - title: "Enregistrer" - } - ]; - } -} \ No newline at end of file diff --git a/src/web/resources/js/tl_module/editor_ctrl/editor_ctrl.js b/src/web/resources/js/tl_module/editor_ctrl/editor_ctrl.js index 4a11e98b..44078d51 100644 --- a/src/web/resources/js/tl_module/editor_ctrl/editor_ctrl.js +++ b/src/web/resources/js/tl_module/editor_ctrl/editor_ctrl.js @@ -6,7 +6,7 @@ characterApp.controller("editor_ctrl", ["$scope", "$q", "$http", "$window", /*"$ $scope.model_editor = { is_ctrl_ready: false, - modulestate: { + module_state: { has_error: false, error: "" }, @@ -17,7 +17,9 @@ characterApp.controller("editor_ctrl", ["$scope", "$q", "$http", "$window", /*"$ user_has_writer_perm: false, has_access_perm: false, email_google_service: "", - can_generate: false + can_generate: false, + last_local_doc_update: 0, + string_last_local_doc_update: "" }, is_updating_file_url: false, @@ -52,6 +54,14 @@ characterApp.controller("editor_ctrl", ["$scope", "$q", "$http", "$window", /*"$ }; $scope.init_model(); + $scope.$watch("model_editor.info.last_local_doc_update", function (value) { + var date_updated = new Date(); + date_updated.setTime(value); + // console.debug(value); + // console.debug(date_updated.toString()); + $scope.model_editor.info.string_last_local_doc_update = date_updated.toString(); + }, true); + // Get editor info $scope.update_editor = function (e) { $scope.init_model(); @@ -60,14 +70,15 @@ characterApp.controller("editor_ctrl", ["$scope", "$q", "$http", "$window", /*"$ method: "get", url: "/cmd/editor/get_info", headers: {"Content-Type": "application/json; charset=UTF-8"}, - timeout: 5000 + timeout: 60000 }).then(function (response/*, status, headers, config*/) { $scope.model_editor.info = response.data; + console.info(response.data); $scope.model_editor.is_ctrl_ready = true; if ("error" in $scope.model_editor.info) { - $scope.model_editor.modulestate.has_error = true; - $scope.model_editor.modulestate.error = $scope.model_editor.info.error; + $scope.model_editor.module_state.has_error = true; + $scope.model_editor.module_state.error = $scope.model_editor.info.error; } }, function errorCallback(response) { console.error(response); @@ -88,7 +99,7 @@ characterApp.controller("editor_ctrl", ["$scope", "$q", "$http", "$window", /*"$ $scope.update_editor(); // Send request to receive writer permission - $scope.send_writingpermission = function (e) { + $scope.send_writing_permission = function (e) { if ($scope.model_editor.is_sharing_doc) { return; } @@ -99,7 +110,7 @@ characterApp.controller("editor_ctrl", ["$scope", "$q", "$http", "$window", /*"$ method: "post", url: "/cmd/editor/add_generator_share", headers: {"Content-Type": "application/json; charset=UTF-8"}, - timeout: 5000 + timeout: 60000 }).then(function (response/*, status, headers, config*/) { console.info(response); $scope.model_editor.info.user_has_writer_perm = true; @@ -146,7 +157,7 @@ characterApp.controller("editor_ctrl", ["$scope", "$q", "$http", "$window", /*"$ method: "post", url: "/cmd/editor/generate_and_save", headers: {"Content-Type": "application/json; charset=UTF-8"}, - timeout: 10000 + timeout: 60000 }).then(function (response/*, status, headers, config*/) { console.info(response); $scope.model_editor.is_generating_doc = false; @@ -193,7 +204,7 @@ characterApp.controller("editor_ctrl", ["$scope", "$q", "$http", "$window", /*"$ url: "/cmd/editor/update_file_url", data: data, headers: {"Content-Type": "application/json; charset=UTF-8"}, - timeout: 5000 + timeout: 60000 }).then(function (response/*, status, headers, config*/) { console.info(response); $scope.model_editor.is_updating_file_url = false; diff --git a/src/web/resources/js/tl_module/lore_ctrl/lore_ctrl.js b/src/web/resources/js/tl_module/lore_ctrl/lore_ctrl.js index c48b43ab..58a317e8 100644 --- a/src/web/resources/js/tl_module/lore_ctrl/lore_ctrl.js +++ b/src/web/resources/js/tl_module/lore_ctrl/lore_ctrl.js @@ -220,12 +220,13 @@ characterApp.controller("lore_ctrl", ["$scope", "$q", "$http", "$window", "$loca $http({ method: "get", - url: "/cmd/lore", + url: "/cmd/manual", headers: {"Content-Type": "application/json; charset=UTF-8"}, // data: $httpParamSerializerJQLike(data), timeout: 5000 }).then(function (response/*, status, headers, config*/) { $scope.lore = response.data.lore; + console.info(response.data); var key = "/filter="; if ($location.path().substring(0, key.length) == key) { diff --git a/src/web/resources/js/tl_module/manual_ctrl/manual_ctrl.js b/src/web/resources/js/tl_module/manual_ctrl/manual_ctrl.js index 903b05a9..d4ef798f 100644 --- a/src/web/resources/js/tl_module/manual_ctrl/manual_ctrl.js +++ b/src/web/resources/js/tl_module/manual_ctrl/manual_ctrl.js @@ -226,6 +226,7 @@ characterApp.controller("manual_ctrl", ["$scope", "$q", "$http", "$window", "$lo timeout: 5000 }).then(function (response/*, status, headers, config*/) { $scope.manual = response.data.manual; + console.info(response.data); var key = "/filter="; if ($location.path().substring(0, key.length) == key) { diff --git a/src/web/resources/js/tl_module/profile_ctrl/profile_ctrl.js b/src/web/resources/js/tl_module/profile_ctrl/profile_ctrl.js index 0b92de69..1daee38f 100644 --- a/src/web/resources/js/tl_module/profile_ctrl/profile_ctrl.js +++ b/src/web/resources/js/tl_module/profile_ctrl/profile_ctrl.js @@ -31,6 +31,7 @@ characterApp.controller("profile_ctrl", ["$scope", "$q", "$http", "$window", /*" timeout: 5000 }).then(function (response/*, status, headers, config*/) { $scope.model_profile.info = response.data; + console.info(response.data); }); }; @@ -65,7 +66,7 @@ characterApp.controller("profile_ctrl", ["$scope", "$q", "$http", "$window", /*" data: data, timeout: 5000 }).then(function (response/*, status, headers, config*/) { - console.debug(response.data); + console.info(response.data); // Reset the loading $scope.model_profile.add_password.loading = false; @@ -134,7 +135,7 @@ characterApp.controller("profile_ctrl", ["$scope", "$q", "$http", "$window", /*" data: data, timeout: 5000 }).then(function (response/*, status, headers, config*/) { - console.debug(response.data); + console.info(response.data); // Reset the loading $scope.model_profile.update_password.loading = false; diff --git a/src/web/resources/js/tl_module/tl_module.js b/src/web/resources/js/tl_module/tl_module.js index 015f555e..1291c9b0 100644 --- a/src/web/resources/js/tl_module/tl_module.js +++ b/src/web/resources/js/tl_module/tl_module.js @@ -1,6 +1,17 @@ 'use strict'; -var characterApp = angular.module('creation_personnage_TL', ['monospaced.qrcode', 'ngSanitize', 'ngRoute', 'schemaForm', 'mgcrea.ngStrap', 'ngPrint']); +var characterApp = angular.module('creation_personnage_TL', ['monospaced.qrcode', 'ngSanitize', 'ngRoute', 'schemaForm', 'mgcrea.ngStrap', 'ngPrint', 'angularMoment']); + +characterApp.filter('UTCToNow', ['moment', function (moment) { + return function (input, format) { + if (format) { + return moment.utc(input).local().format('dddd, MMMM Do YYYY, h:mm:ss a'); + } else { + return moment.utc(input).local(); + } + }; + }] +); characterApp.config(['$routeProvider', function ($routeProvider) { // $routeProvider.when('/login', {templateUrl: 'templates/login.html', login: true}); @@ -11,4 +22,4 @@ characterApp.config(['$routeProvider', function ($routeProvider) { // $routeProvider.when('/view1', {templateUrl: 'partials/partial1.html', controller: 'MyCtrl1'}); // $routeProvider.when('/view2', {templateUrl: 'partials/partial2.html', controller: 'MyCtrl2'}); $routeProvider.otherwise({redirectTo: '/'}); -}]) +}]); diff --git a/src/web/web.py b/src/web/web.py index 432178f4..7fe2a088 100644 --- a/src/web/web.py +++ b/src/web/web.py @@ -15,7 +15,6 @@ import sys from py_class.db import DB from py_class.manual import Manual -from py_class.lore import Lore from py_class.doc_generator.doc_generator_gspread import DocGeneratorGSpread from py_class.auth_keys import AuthKeys from py_class.project_archive import ProjectArchive @@ -79,7 +78,6 @@ def main(parse_arg): "use_internet_static": parse_arg.use_internet_static, "db": DB(parse_arg), "manual": Manual(parse_arg), - "lore": Lore(parse_arg), "doc_generator_gspread": DocGeneratorGSpread(parse_arg), "project_archive": ProjectArchive(parse_arg), "disable_character": parse_arg.disable_character, @@ -128,9 +126,11 @@ def main(parse_arg): tornado.web.url(r"/cmd/character_view/?", handlers.CharacterViewHandler, name='character_view', kwargs=settings), tornado.web.url(r"/cmd/manual/?", handlers.ManualHandler, name='cmd_manual', kwargs=settings), - tornado.web.url(r"/cmd/lore/?", handlers.LoreHandler, name='cmd_lore', kwargs=settings), + tornado.web.url(r"/cmd/manual_admin/?", handlers.ManualAdminHandler, name='cmd_manual_admin', kwargs=settings), tornado.web.url(r"/cmd/stat/total_season_pass/?", handlers.StatSeasonPass, name='cmd_stat_total_season_pass', kwargs=settings), + tornado.web.url(r"/cmd/character_approbation/?", handlers.CharacterApprobationHandler, + name='cmd_character_approbation', kwargs=settings), # Profile tornado.web.url(r"/cmd/profile/update_password/?", handlers.ProfileCmdUpdatePasswordHandler,